From 0fcf8bb97ced8df552cd0283b4ea009b6ca42623 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 21 Oct 2021 16:24:40 +0200 Subject: added tangential and guided fill --- lib/extensions/__init__.py | 2 + lib/extensions/base.py | 12 +---- lib/extensions/cleanup.py | 4 +- lib/extensions/params.py | 81 +++++++++++++++++++++++++++---- lib/extensions/selection_to_guide_line.py | 67 +++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 lib/extensions/selection_to_guide_line.py (limited to 'lib/extensions') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index b6e0d1d1..933720c9 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -39,6 +39,7 @@ from .print_pdf import Print from .remove_embroidery_settings import RemoveEmbroiderySettings from .reorder import Reorder from .selection_to_pattern import SelectionToPattern +from .selection_to_guide_line import SelectionToGuideLine from .simulator import Simulator from .stitch_plan_preview import StitchPlanPreview from .zip import Zip @@ -52,6 +53,7 @@ __all__ = extensions = [StitchPlanPreview, Zip, Flip, SelectionToPattern, + SelectionToGuideLine, ObjectCommands, ObjectCommandsToggleVisibility, LayerCommands, diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 75a07c5a..56385458 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -10,7 +10,6 @@ from collections.abc import MutableMapping import inkex from lxml import etree -from lxml.etree import Comment from stringcase import snakecase from ..commands import is_command, layer_commands @@ -20,8 +19,7 @@ from ..i18n import _ from ..patterns import is_pattern from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, - SVG_GROUP_TAG, SVG_MASK_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -131,10 +129,6 @@ class InkstitchExtension(inkex.Effect): def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901 nodes = [] - - if node.tag == Comment: - return [] - element = EmbroideryElement(node) if element.has_command('ignore_object'): @@ -147,9 +141,7 @@ class InkstitchExtension(inkex.Effect): if (node.tag in EMBROIDERABLE_TAGS or node.tag == SVG_GROUP_TAG) and element.get_style('display', 'inline') is None: return [] - # defs, masks and clippaths can contain embroiderable elements - # but should never be rendered directly. - if node.tag in [SVG_DEFS_TAG, SVG_MASK_TAG, SVG_CLIPPATH_TAG]: + if node.tag == SVG_DEFS_TAG: return [] # command connectors with a fill color set, will glitch into the elements list diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index a38818b8..ae95041b 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, errormsg -from ..elements import Fill, Stroke +from ..elements import AutoFill, Stroke from ..i18n import _ from .base import InkstitchExtension @@ -38,7 +38,7 @@ class Cleanup(InkstitchExtension): return for element in self.elements: - if (isinstance(element, Fill) and self.rm_fill and element.shape.area < self.fill_threshold): + if (isinstance(element, AutoFill) and self.rm_fill and element.shape.area < self.fill_threshold): element.node.getparent().remove(element.node) count += 1 if (isinstance(element, Stroke) and self.rm_stroke and diff --git a/lib/extensions/params.py b/lib/extensions/params.py index c96b9691..8021d5d7 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,15 +7,15 @@ import os import sys -from collections import defaultdict +from collections import defaultdict,namedtuple from copy import copy -from itertools import groupby +from itertools import groupby,zip_longest import wx from wx.lib.scrolledpanel import ScrolledPanel from ..commands import is_command, is_command_symbol -from ..elements import (AutoFill, Clone, EmbroideryElement, Fill, Polyline, +from ..elements import (AutoFill, Clone, EmbroideryElement, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..gui import PresetsPanel, SimulatorPreview, WarningPanel @@ -25,6 +25,14 @@ from ..utils import get_resource_dir from .base import InkstitchExtension +#ChoiceWidgets = namedtuple("ChoiceWidgets", "param widget last_initialized_choice") + + + +def grouper(iterable_obj, count, fillvalue=None): + args = [iter(iterable_obj)] * count + return zip_longest(*args, fillvalue=fillvalue) + class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) @@ -38,6 +46,8 @@ class ParamsTab(ScrolledPanel): self.dependent_tabs = [] self.parent_tab = None self.param_inputs = {} + self.choice_widgets = defaultdict(list) + self.dict_of_choices = {} self.paired_tab = None self.disable_notify_pair = False @@ -113,6 +123,19 @@ class ParamsTab(ScrolledPanel): if event: event.Skip() + def update_choice_state(self, event=None): + input = event.GetEventObject() + selection = input.GetSelection() + + param = self.inputs_to_params[input] + + self.update_choice_widgets((param, selection)) + self.settings_grid.Layout() + self.Layout() + + if event: + event.Skip() + def pair_changed(self, value): # print self.name, "pair_changed", value new_value = not value @@ -245,7 +268,30 @@ class ParamsTab(ScrolledPanel): # end wxGlade pass - def __do_layout(self): + #choice tuple is None or contains ("choice widget param name", "actual selection") + def update_choice_widgets(self, choice_tuple = None): + if choice_tuple == None: #update all choices + for choice in self.dict_of_choices.values(): + self.update_choice_widgets((choice["param"].name, choice["widget"].GetSelection())) + else: + choice = self.dict_of_choices[choice_tuple[0]] + last_selection = choice["last_initialized_choice"] + current_selection = choice["widget"].GetSelection() + if last_selection != -1 and last_selection != current_selection: #Hide the old widgets + for widget in self.choice_widgets[(choice["param"].name, last_selection)]: + widget.Hide() + #self.settings_grid.Detach(widget) + + #choice_index = self.settings_grid.GetChildren().index(self.settings_grid.GetItem(choice["widget"])) #TODO: is there a better way to get the index in the sizer? + for widgets in grouper(self.choice_widgets[choice_tuple], 4): + widgets[0].Show(True) + widgets[1].Show(True) + widgets[2].Show(True) + widgets[3].Show(True) + choice["last_initialized_choice"] = current_selection + + def __do_layout(self, only_settings_grid=False): + # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) @@ -266,14 +312,20 @@ class ParamsTab(ScrolledPanel): box.Add(toggle_sizer, proportion=0, flag=wx.BOTTOM, border=10) for param in self.params: - self.settings_grid.Add(self.create_change_indicator(param.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - + col1 = self.create_change_indicator(param.name) description = wx.StaticText(self, label=param.description) description.SetToolTip(param.tooltip) + + if param.select_items != None: + col1.Hide() + description.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([col1, description]) + #else: + self.settings_grid.Add(col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) if param.type == 'boolean': - if len(param.values) > 1: input = wx.CheckBox(self, style=wx.CHK_3STATE) input.Set3StateValue(wx.CHK_UNDETERMINED) @@ -287,6 +339,8 @@ class ParamsTab(ScrolledPanel): input = wx.Choice(self, wx.ID_ANY, choices=param.options) input.SetSelection(int(param.values[0])) input.Bind(wx.EVT_CHOICE, self.changed) + input.Bind(wx.EVT_CHOICE, self.update_choice_state) + self.dict_of_choices[param.name] = {"param": param, "widget": input, "last_initialized_choice": 1} elif len(param.values) > 1: input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(str(value) for value in param.values), style=wx.CB_DROPDOWN) input.Bind(wx.EVT_COMBOBOX, self.changed) @@ -298,13 +352,22 @@ class ParamsTab(ScrolledPanel): self.param_inputs[param.name] = input + col4 = wx.StaticText(self, label=param.unit or "") + + if param.select_items != None: + input.Hide() + col4.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([input, col4]) + #else: self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) - self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) self.SetSizer(box) + self.update_choice_widgets() self.Layout() @@ -521,7 +584,7 @@ class Params(InkstitchExtension): else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": classes.append(AutoFill) - classes.append(Fill) + #classes.append(Fill) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py new file mode 100644 index 00000000..85a44bb1 --- /dev/null +++ b/lib/extensions/selection_to_guide_line.py @@ -0,0 +1,67 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex +from lxml import etree + +from ..i18n import _ +from ..svg.tags import SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_DEFS_TAG +from .base import InkstitchExtension + + +class SelectionToGuideLine(InkstitchExtension): + + def effect(self): + if not self.get_elements(): + return + + if not self.svg.selected: + inkex.errormsg(_("Please select one object to be marked as a guide line.")) + return + + if len(self.get_nodes())!=1: + inkex.errormsg(_("Please select only one object to be marked as a guide line.")) + return + + for guide_line in self.get_nodes(): + if guide_line.tag in (SVG_PATH_TAG, SVG_POLYLINE_TAG): + self.set_marker(guide_line) + + def set_marker(self, node): + xpath = ".//marker[@id='inkstitch-guide-line-marker']" + guide_line_marker = self.document.xpath(xpath) + + if not guide_line_marker: + # get or create def element + defs = self.document.find(SVG_DEFS_TAG) + if defs is None: + defs = etree.SubElement(self.document, SVG_DEFS_TAG) + + # insert marker + marker = """ + + + + + """ # noqa: E501 + defs.append(etree.fromstring(marker)) + + # attach marker to node + style = node.get('style') or '' + style = style.split(";") + style = [i for i in style if not i.startswith('marker-start')] + style.append('marker-start:url(#inkstitch-guide-line-marker)') + node.set('style', ";".join(style)) -- cgit v1.3.1 From 125db3f83b3b330df757f7cc0faf6489b3cb348d Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 29 Oct 2021 16:18:22 +0200 Subject: Applied style guide --- lib/elements/auto_fill.py | 118 +++-- lib/elements/clone.py | 5 +- lib/elements/element.py | 22 +- lib/elements/utils.py | 5 +- lib/extensions/params.py | 125 +++-- lib/extensions/selection_to_guide_line.py | 8 +- lib/patterns.py | 5 +- lib/stitches/ConnectAndSamplePattern.py | 834 ++++++++++++++++++++---------- lib/stitches/DebuggingMethods.py | 56 +- lib/stitches/LineStringSampling.py | 446 +++++++++------- lib/stitches/PointTransfer.py | 277 +++++----- lib/stitches/StitchPattern.py | 266 ++++++---- lib/stitches/auto_fill.py | 140 +++-- lib/stitches/constants.py | 53 +- lib/stitches/fill.py | 53 +- 15 files changed, 1489 insertions(+), 924 deletions(-) (limited to 'lib/extensions') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 87bdb010..81abf7ad 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -12,7 +12,6 @@ import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity -from ..stitches import legacy_fill from ..i18n import _ from ..stitch_plan import StitchGroup from ..stitches import auto_fill @@ -21,12 +20,12 @@ from ..utils import cache, version from .element import param from .element import EmbroideryElement from ..patterns import get_patterns -#from .fill import Fill from .validation import ValidationWarning from ..utils import Point as InkstitchPoint from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL + class SmallShapeWarning(ValidationWarning): name = _("Small Fill") description = _("This fill object is so small that it would probably look better as running stitch or satin column. " @@ -50,38 +49,42 @@ class AutoFill(EmbroideryElement): element_name = _("AutoFill") @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index = 1) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - + return self.get_boolean_param('auto_fill', True) + @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index = 2) + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, options=[_("Closest point"), _("Inner to Outer")],select_items=[('fill_method',1)], sort_index = 2) + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) @property - @param('join_style', _('Join Style'), type='dropdown', default=0, options=[_("Round"), _("Mitered"), _("Beveled")],select_items=[('fill_method',1)], sort_index = 2) + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) def join_style(self): return self.get_int_param('join_style', 0) @property - @param('interlaced', _('Interlaced'), type='boolean', default=True,select_items=[('fill_method',1),('fill_method',2)], sort_index = 2) + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) def interlaced(self): return self.get_boolean_param('interlaced', True) @property @param('angle', _('Angle of lines of stitches'), - tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=0) @cache def angle(self): @@ -99,8 +102,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def skip_last(self): return self.get_boolean_param("skip_last", False) @@ -112,8 +115,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The flip option can help you with routing your stitch path. ' 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -123,7 +126,7 @@ class AutoFill(EmbroideryElement): _('Spacing between rows'), tooltip=_('Distance between rows of stitches.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=0.25) def row_spacing(self): @@ -136,9 +139,10 @@ class AutoFill(EmbroideryElement): @property @param('max_stitch_length_mm', _('Maximum fill stitch length'), - tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=3.0) def max_stitch_length(self): @@ -147,10 +151,11 @@ class AutoFill(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=4) def staggers(self): return max(self.get_int_param("staggers", 4), 1) @@ -162,10 +167,10 @@ class AutoFill(EmbroideryElement): # ensure path length for i, path in enumerate(paths): if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + paths[i] = [(path[0][0], path[0][1]), (path[0][0] + + 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] return paths - @property @cache def outline(self): @@ -176,19 +181,16 @@ class AutoFill(EmbroideryElement): def outline_length(self): return self.outline.length - @property - def flip(self): - return False - @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 4) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -200,7 +202,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -211,7 +214,8 @@ class AutoFill(EmbroideryElement): if underlay_angles is not None: underlay_angles = underlay_angles.strip().split(',') try: - underlay_angles = [math.radians(float(angle)) for angle in underlay_angles] + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] except (TypeError, ValueError): return default_value else: @@ -243,7 +247,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -266,12 +271,13 @@ class AutoFill(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, - sort_index = 5, - select_items=[('fill_method',0),('fill_method',2)]) + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) def expand(self): return self.get_float_param('expand_mm', 0) @@ -283,8 +289,8 @@ class AutoFill(EmbroideryElement): 'are not visible. This gives them a jagged appearance.'), type='boolean', default=True, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 6) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) def underpath(self): return self.get_boolean_param('underpath', True) @@ -308,7 +314,8 @@ class AutoFill(EmbroideryElement): # from the first. So let's at least make sure the "first" thing is the # biggest path. paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + paths.sort(key=lambda point_list: shgeo.Polygon( + point_list).area, reverse=True) # Very small holes will cause a shape to be rendered as an outline only # they are too small to be rendered and only confuse the auto_fill algorithm. # So let's ignore them @@ -397,7 +404,7 @@ class AutoFill(EmbroideryElement): color=self.color, tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( - self.underlay_shape, + self.underlay_shape, None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, @@ -410,8 +417,8 @@ class AutoFill(EmbroideryElement): underpath=self.underlay_underpath)) stitch_groups.append(underlay) starting_point = underlay.stitches[-1] - - if self.fill_method == 0: #Auto Fill + + if self.fill_method == 0: # Auto Fill stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), @@ -429,30 +436,31 @@ class AutoFill(EmbroideryElement): ending_point, self.underpath)) stitch_groups.append(stitch_group) - elif self.fill_method == 1: #Tangential Fill + elif self.fill_method == 1: # Tangential Fill polygons = list(self.fill_shape) if not starting_point: - starting_point = (0,0) + starting_point = (0, 0) for poly in polygons: connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, self.interlaced, self.tangential_strategy, shgeo.Point(starting_point)) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) stitch_groups.append(stitch_group) - elif self.fill_method == 2: #Guided Auto Fill - lines = get_patterns(self.node,"#inkstitch-guide-line-marker") + elif self.fill_method == 2: # Guided Auto Fill + lines = get_patterns(self.node, "#inkstitch-guide-line-marker") lines = lines['stroke_patterns'] if not lines or lines[0].is_empty: - inkex.errormsg(_("No line marked as guide line found within the same group as patch")) + inkex.errormsg( + _("No line marked as guide line found within the same group as patch")) else: stitch_group = StitchGroup( color=self.color, diff --git a/lib/elements/clone.py b/lib/elements/clone.py index bcecf3f0..15e7591c 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -14,7 +14,6 @@ from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, from ..utils import cache from .auto_fill import AutoFill from .element import EmbroideryElement, param -#from .fill import Fill from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -79,9 +78,9 @@ class Clone(EmbroideryElement): else: elements = [] if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke", self.node) is not None: if not is_command(element.node): diff --git a/lib/elements/element.py b/lib/elements/element.py index b8728f60..ef70510d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -33,7 +33,6 @@ class Param(object): self.tooltip = tooltip self.sort_index = sort_index self.select_items = select_items - #print("IN PARAM: ", self.values) def __repr__(self): return "Param(%s)" % vars(self) @@ -164,7 +163,8 @@ class EmbroideryElement(object): # Of course, transforms may also involve rotation, skewing, and translation. # All except translation can affect how wide the stroke appears on the screen. - node_transform = inkex.transforms.Transform(get_node_transform(self.node)) + node_transform = inkex.transforms.Transform( + get_node_transform(self.node)) # First, figure out the translation component of the transform. Using a zero # vector completely cancels out the rotation, scale, and skew components. @@ -198,7 +198,8 @@ class EmbroideryElement(object): @property @param('ties', _('Allow lock stitches'), - tooltip=_('Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), + tooltip=_( + 'Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), type='dropdown', # Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither # L10N options to allow lock stitch before and after objects @@ -256,7 +257,8 @@ class EmbroideryElement(object): d = self.node.get("d", "") if not d: - self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id"))) + self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict( + id=self.node.get("id"))) return inkex.paths.Path(d).to_superpath() @@ -266,7 +268,8 @@ class EmbroideryElement(object): @property def shape(self): - raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + raise NotImplementedError( + "INTERNAL ERROR: %s must implement shape()", self.__class__) @property @cache @@ -316,7 +319,8 @@ class EmbroideryElement(object): return self.get_boolean_param('stop_after', False) def to_stitch_groups(self, last_patch): - raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__) + raise NotImplementedError( + "%s must implement to_stitch_groups()" % self.__class__.__name__) def embroider(self, last_patch): self.validate() @@ -329,8 +333,10 @@ class EmbroideryElement(object): patch.force_lock_stitches = self.force_lock_stitches if patches: - patches[-1].trim_after = self.has_command("trim") or self.trim_after - patches[-1].stop_after = self.has_command("stop") or self.stop_after + patches[-1].trim_after = self.has_command( + "trim") or self.trim_after + patches[-1].stop_after = self.has_command( + "stop") or self.stop_after return patches diff --git a/lib/elements/utils.py b/lib/elements/utils.py index f858cc81..9fec8b63 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -11,7 +11,6 @@ from .auto_fill import AutoFill from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill from .image import ImageObject from .pattern import PatternObject from .polyline import Polyline @@ -41,9 +40,9 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke"): if not is_command(element.node): diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 8021d5d7..30f6ba1d 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,9 +7,9 @@ import os import sys -from collections import defaultdict,namedtuple +from collections import defaultdict from copy import copy -from itertools import groupby,zip_longest +from itertools import groupby, zip_longest import wx from wx.lib.scrolledpanel import ScrolledPanel @@ -25,14 +25,11 @@ from ..utils import get_resource_dir from .base import InkstitchExtension -#ChoiceWidgets = namedtuple("ChoiceWidgets", "param widget last_initialized_choice") - - - def grouper(iterable_obj, count, fillvalue=None): args = [iter(iterable_obj)] * count return zip_longest(*args, fillvalue=fillvalue) + class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) @@ -56,14 +53,16 @@ class ParamsTab(ScrolledPanel): if toggles: self.toggle = toggles[0] self.params.remove(self.toggle) - self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description) + self.toggle_checkbox = wx.CheckBox( + self, label=self.toggle.description) value = any(self.toggle.values) if self.toggle.inverse: value = not value self.toggle_checkbox.SetValue(value) - self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state) + self.toggle_checkbox.Bind( + wx.EVT_CHECKBOX, self.update_toggle_state) self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed) self.param_inputs[self.toggle.name] = self.toggle_checkbox @@ -76,7 +75,8 @@ class ParamsTab(ScrolledPanel): self.settings_grid.AddGrowableCol(1, 2) self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) - self.pencil_icon = wx.Image(os.path.join(get_resource_dir("icons"), "pencil_20x20.png")).ConvertToBitmap() + self.pencil_icon = wx.Image(os.path.join(get_resource_dir( + "icons"), "pencil_20x20.png")).ConvertToBitmap() self.__set_properties() self.__do_layout() @@ -230,19 +230,25 @@ class ParamsTab(ScrolledPanel): if len(self.nodes) == 1: description = _("These settings will be applied to 1 object.") else: - description = _("These settings will be applied to %d objects.") % len(self.nodes) + description = _( + "These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + description += "\n • " + \ + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: if len(self.dependent_tabs) == 1: - description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + description += "\n • " + \ + _("Disabling this tab will disable the following %d tabs.") % len( + self.dependent_tabs) else: - description += "\n • " + _("Disabling this tab will disable the following tab.") + description += "\n • " + \ + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + description += "\n • " + \ + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -268,21 +274,21 @@ class ParamsTab(ScrolledPanel): # end wxGlade pass - #choice tuple is None or contains ("choice widget param name", "actual selection") - def update_choice_widgets(self, choice_tuple = None): - if choice_tuple == None: #update all choices + # choice tuple is None or contains ("choice widget param name", "actual selection") + def update_choice_widgets(self, choice_tuple=None): + if choice_tuple is None: # update all choices for choice in self.dict_of_choices.values(): - self.update_choice_widgets((choice["param"].name, choice["widget"].GetSelection())) + self.update_choice_widgets( + (choice["param"].name, choice["widget"].GetSelection())) else: choice = self.dict_of_choices[choice_tuple[0]] - last_selection = choice["last_initialized_choice"] + last_selection = choice["last_initialized_choice"] current_selection = choice["widget"].GetSelection() - if last_selection != -1 and last_selection != current_selection: #Hide the old widgets + if last_selection != -1 and last_selection != current_selection: # Hide the old widgets for widget in self.choice_widgets[(choice["param"].name, last_selection)]: widget.Hide() - #self.settings_grid.Detach(widget) - - #choice_index = self.settings_grid.GetChildren().index(self.settings_grid.GetItem(choice["widget"])) #TODO: is there a better way to get the index in the sizer? + # self.settings_grid.Detach(widget) + for widgets in grouper(self.choice_widgets[choice_tuple], 4): widgets[0].Show(True) widgets[1].Show(True) @@ -295,20 +301,24 @@ class ParamsTab(ScrolledPanel): # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) - summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects")) + summary_box = wx.StaticBox( + self, wx.ID_ANY, label=_("Inkscape objects")) sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL) self.description = wx.StaticText(self) self.update_description() self.description.SetLabel(self.description_text) self.description_container = box self.Bind(wx.EVT_SIZE, self.resized) - sizer.Add(self.description, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) + sizer.Add(self.description, proportion=0, + flag=wx.EXPAND | wx.ALL, border=5) box.Add(sizer, proportion=0, flag=wx.ALL, border=5) if self.toggle: toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) - toggle_sizer.Add(self.create_change_indicator(self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) - toggle_sizer.Add(self.toggle_checkbox, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + toggle_sizer.Add(self.create_change_indicator( + self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) + toggle_sizer.Add(self.toggle_checkbox, proportion=0, + flag=wx.ALIGN_CENTER_VERTICAL) box.Add(toggle_sizer, proportion=0, flag=wx.BOTTOM, border=10) for param in self.params: @@ -316,14 +326,16 @@ class ParamsTab(ScrolledPanel): description = wx.StaticText(self, label=param.description) description.SetToolTip(param.tooltip) - if param.select_items != None: + if param.select_items is not None: col1.Hide() description.Hide() for item in param.select_items: self.choice_widgets[item].extend([col1, description]) - #else: - self.settings_grid.Add(col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) + # else: + self.settings_grid.Add( + col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | + wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) if param.type == 'boolean': if len(param.values) > 1: @@ -340,9 +352,11 @@ class ParamsTab(ScrolledPanel): input.SetSelection(int(param.values[0])) input.Bind(wx.EVT_CHOICE, self.changed) input.Bind(wx.EVT_CHOICE, self.update_choice_state) - self.dict_of_choices[param.name] = {"param": param, "widget": input, "last_initialized_choice": 1} + self.dict_of_choices[param.name] = { + "param": param, "widget": input, "last_initialized_choice": 1} elif len(param.values) > 1: - input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(str(value) for value in param.values), style=wx.CB_DROPDOWN) + input = wx.ComboBox(self, wx.ID_ANY, choices=sorted( + str(value) for value in param.values), style=wx.CB_DROPDOWN) input.Bind(wx.EVT_COMBOBOX, self.changed) input.Bind(wx.EVT_TEXT, self.changed) else: @@ -354,14 +368,16 @@ class ParamsTab(ScrolledPanel): col4 = wx.StaticText(self, label=param.unit or "") - if param.select_items != None: + if param.select_items is not None: input.Hide() col4.Hide() for item in param.select_items: self.choice_widgets[item].extend([input, col4]) - #else: - self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) - self.settings_grid.Add(col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + # else: + self.settings_grid.Add( + input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) + self.settings_grid.Add( + col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} @@ -372,16 +388,20 @@ class ParamsTab(ScrolledPanel): self.Layout() def create_change_indicator(self, param): - indicator = wx.Button(self, style=wx.BORDER_NONE | wx.BU_NOTEXT, size=(28, 28)) - indicator.SetToolTip(_('Click to force this parameter to be saved when you click "Apply and Quit"')) - indicator.Bind(wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) + indicator = wx.Button(self, style=wx.BORDER_NONE | + wx.BU_NOTEXT, size=(28, 28)) + indicator.SetToolTip( + _('Click to force this parameter to be saved when you click "Apply and Quit"')) + indicator.Bind( + wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) self.param_change_indicators[param] = indicator return indicator def enable_change_indicator(self, param): self.param_change_indicators[param].SetBitmapLabel(self.pencil_icon) - self.param_change_indicators[param].SetToolTip(_('This parameter will be saved when you click "Apply and Quit"')) + self.param_change_indicators[param].SetToolTip( + _('This parameter will be saved when you click "Apply and Quit"')) self.changed_inputs.add(self.param_inputs[param]) @@ -407,7 +427,8 @@ class SettingsFrame(wx.Frame): _("Embroidery Params") ) - icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png")) + icon = wx.Icon(os.path.join( + get_resource_dir("icons"), "inkstitch256x256.png")) self.SetIcon(icon) self.notebook = wx.Notebook(self, wx.ID_ANY) @@ -425,7 +446,8 @@ class SettingsFrame(wx.Frame): self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) self.Bind(wx.EVT_CLOSE, self.cancel) - self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings")) + self.use_last_button = wx.Button( + self, wx.ID_ANY, _("Use Last Settings")) self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last) self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) @@ -544,7 +566,8 @@ class SettingsFrame(wx.Frame): for tab in self.tabs: self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.warning_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) - sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + sizer_1.Add(self.notebook, 1, wx.EXPAND | + wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) sizer_3.Add(self.cancel_button, 0, wx.RIGHT, 5) sizer_3.Add(self.use_last_button, 0, wx.RIGHT | wx.BOTTOM, 5) @@ -584,7 +607,7 @@ class Params(InkstitchExtension): else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": classes.append(AutoFill) - #classes.append(Fill) + # classes.append(Fill) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: @@ -611,7 +634,8 @@ class Params(InkstitchExtension): else: getter = 'get_param' - values = [item for item in (getattr(node, getter)(param.name, param.default) for node in nodes) if item is not None] + values = [item for item in (getattr(node, getter)( + param.name, param.default) for node in nodes) if item is not None] return values @@ -677,7 +701,8 @@ class Params(InkstitchExtension): for group, params in self.group_params(params): tab_name = group or cls.element_name - tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, params=list(params), nodes=nodes) + tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, + params=list(params), nodes=nodes) new_tabs.append(tab) if group == "": @@ -697,14 +722,16 @@ class Params(InkstitchExtension): def effect(self): try: app = wx.App() - frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) + frame = SettingsFrame( + tabs_factory=self.create_tabs, on_cancel=self.cancel) # position left, center current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((int(display_size[0]), int(display_size[3]/2 - frame_size[1]/2))) + frame.SetPosition((int(display_size[0]), int( + display_size[3]/2 - frame_size[1]/2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py index 85a44bb1..e11cdb4e 100644 --- a/lib/extensions/selection_to_guide_line.py +++ b/lib/extensions/selection_to_guide_line.py @@ -18,11 +18,13 @@ class SelectionToGuideLine(InkstitchExtension): return if not self.svg.selected: - inkex.errormsg(_("Please select one object to be marked as a guide line.")) + inkex.errormsg( + _("Please select one object to be marked as a guide line.")) return - if len(self.get_nodes())!=1: - inkex.errormsg(_("Please select only one object to be marked as a guide line.")) + if len(self.get_nodes()) != 1: + inkex.errormsg( + _("Please select only one object to be marked as a guide line.")) return for guide_line in self.get_nodes(): diff --git a/lib/patterns.py b/lib/patterns.py index b4b60522..789d5f89 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -19,7 +19,7 @@ def is_pattern(node): def apply_patterns(patches, node): - patterns = get_patterns(node,"#inkstitch-pattern-marker") + patterns = get_patterns(node, "#inkstitch-pattern-marker") _apply_fill_patterns(patterns['fill_patterns'], patches) _apply_stroke_patterns(patterns['stroke_patterns'], patches) @@ -32,7 +32,8 @@ def _apply_stroke_patterns(patterns, patches): patch_points.append(stitch) if i == len(patch.stitches) - 1: continue - intersection_points = _get_pattern_points(stitch, patch.stitches[i+1], pattern) + intersection_points = _get_pattern_points( + stitch, patch.stitches[i+1], pattern) for point in intersection_points: patch_points.append(Stitch(point, tags=('pattern_point',))) patch.stitches = patch_points diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 21a56cd6..9b3572d9 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -1,6 +1,6 @@ from shapely.geometry.polygon import LineString, LinearRing -from shapely.geometry import Point, MultiPoint, linestring -from shapely.ops import nearest_points, polygonize +from shapely.geometry import Point, MultiPoint +from shapely.ops import nearest_points from collections import namedtuple from depq import DEPQ import math @@ -8,11 +8,22 @@ from ..stitches import LineStringSampling from ..stitches import PointTransfer from ..stitches import constants -nearest_neighbor_tuple = namedtuple('nearest_neighbor_tuple', ['nearest_point_parent', 'nearest_point_child', 'projected_distance_parent', 'child_node']) +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) -# Cuts a closed line so that the new closed line starts at the point with "distance" to the beginning of the old line. def cut(line, distance): + """ + Cuts a closed line so that the new closed line starts at the + point with "distance" to the beginning of the old line. + """ if distance <= 0.0 or distance >= line.length: return [LineString(line)] coords = list(line.coords) @@ -23,29 +34,41 @@ def cut(line, distance): pd = line.project(Point(p)) if pd == distance: if coords[0] == coords[-1]: - return LineString(coords[i:]+coords[1:i+1]) + return LineString(coords[i:] + coords[1: i + 1]) else: - return LineString(coords[i:]+coords[:i]) + return LineString(coords[i:] + coords[:i]) if pd > distance: cp = line.interpolate(distance) if coords[0] == coords[-1]: - return LineString([(cp.x, cp.y)] + coords[i:]+coords[1:i]+[(cp.x, cp.y)]) + return LineString( + [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] + ) else: - return LineString([(cp.x, cp.y)] + coords[i:]+coords[:i]) - - -#Takes the offsetted curves organized as tree, connects and samples them. -#Strategy: A connection from parent to child is made where both curves come closest together. -#Input: -#-tree: contains the offsetted curves in a hierachical organized data structure. -#-used_offset: used offset when the offsetted curves were generated -#-stitch_distance: maximum allowed distance between two points after sampling -#-close_point: defines the beginning point for stitching (stitching starts always from the undisplaced curve) -#-offset_by_half: If true the resulting points are interlaced otherwise not. -#Returnvalues: -#-All offsetted curves connected to one line and sampled with points obeying stitch_distance and offset_by_half -#-Tag (origin) of each point to analyze why a point was placed at this position -def connect_raster_tree_nearest_neighbor(tree, used_offset, stitch_distance, close_point, offset_by_half): + return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) + + +def connect_raster_tree_nearest_neighbor( + tree, used_offset, stitch_distance, close_point, offset_by_half +): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made where both curves + come closest together. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ current_coords = tree.val abs_offset = abs(used_offset) @@ -60,176 +83,285 @@ def connect_raster_tree_nearest_neighbor(tree, used_offset, stitch_distance, clo if not tree.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item,priority in tree.transferred_point_priority_deque: - new_DEPQ.insert(item, math.fmod( - priority-start_distance+current_coords.length, current_coords.length)) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) tree.transferred_point_priority_deque = new_DEPQ - #print("Gecutted") stitching_direction = 1 - # This list should contain a tuple of nearest points between the current geometry - # and the subgeometry, the projected distance along the current geometry, - # and the belonging subtree node + # This list should contain a tuple of nearest points between + # the current geometry and the subgeometry, the projected + # distance along the current geometry, and the belonging subtree node nearest_points_list = [] - + for subnode in tree.children: point_parent, point_child = nearest_points(current_coords, subnode.val) proj_distance = current_coords.project(point_parent) - nearest_points_list.append(nearest_neighbor_tuple(nearest_point_parent = point_parent, - nearest_point_child = point_child, - projected_distance_parent = proj_distance, - child_node=subnode)) - nearest_points_list.sort(reverse=False, key=lambda tup: tup.projected_distance_parent) + nearest_points_list.append( + nearest_neighbor_tuple( + nearest_point_parent=point_parent, + nearest_point_child=point_child, + proj_distance_parent=proj_distance, + child_node=subnode, + ) + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) if nearest_points_list: - start_distance = min(abs_offset*constants.factor_offset_starting_points, nearest_points_list[0].projected_distance_parent) - end_distance = max(current_coords.length-abs_offset*constants.factor_offset_starting_points, nearest_points_list[-1].projected_distance_parent) + start_distance = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_distance = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) else: - start_distance = abs_offset*constants.factor_offset_starting_points - end_distance = current_coords.length-abs_offset*constants.factor_offset_starting_points - - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, start_distance, # We add/subtract an offset to not sample the same point again (avoid double points for start and end) - end_distance, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) - assert(len(own_coords) == len(own_coords_origin)) + start_distance = abs_offset * constants.factor_offset_starting_points + end_distance = ( + current_coords.length - abs_offset * constants.factor_offset_starting_points + ) + + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + start_distance, # We add/subtract an offset to not sample + # the same point again (avoid double + # points for start and end) + end_distance, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) + assert len(own_coords) == len(own_coords_origin) own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - - #tree.val = LineString(own_coords) - #tree.pointsourcelist = own_coords_origin tree.stitching_direction = stitching_direction tree.already_rastered = True - #Next we need to transfer our rastered points to siblings and childs + # Next we need to transfer our rastered points to siblings and childs to_transfer_point_list = [] to_transfer_point_list_origin = [] - for k in range(1, len(own_coords)-1): #Do not take the first and the last since they are ENTER_LEAVING_POINT points for sure - # if abs(temp[k][0]-5.25) < 0.5 and abs(temp[k][1]-42.9) < 0.5: - # print("HIER gefunden!") - if (not offset_by_half and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED): + for k in range(1, len(own_coords) - 1): + # Do not take the first and the last since they are ENTER_LEAVING_POINT + # points for sure + + if ( + not offset_by_half + and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED + ): continue - if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT: + if ( + own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT + or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT + ): continue to_transfer_point_list.append(Point(own_coords[k])) - point_origin = own_coords_origin[k] + point_origin = own_coords_origin[k] to_transfer_point_list_origin.append(point_origin) - - #since the projection is only in ccw direction towards inner we need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,offset_by_half,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=False, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - - - #We transfer also to the overnext child to get a more straight arrangement of points perpendicular to the stitching lines + # Since the projection is only in ccw direction towards inner we need + # to use "-used_offset" for stitching_direction==-1 + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines if offset_by_half: - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,False,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=True, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) if not nearest_points_list: - #If there is no child (inner geometry) we can simply take our own rastered coords as result + # If there is no child (inner geometry) we can simply take + # our own rastered coords as result result_coords = own_coords result_coords_origin = own_coords_origin else: - #There are childs so we need to merge their coordinates with our own rastered coords + # There are childs so we need to merge their coordinates + + # with our own rastered coords - #To create a closed ring + # To create a closed ring own_coords.append(own_coords[0]) own_coords_origin.append(own_coords_origin[0]) - - #own_coords does not start with current_coords but has an offset (see call of raster_line_string_with_priority_points) + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) total_distance = start_distance - current_item_index = 0 + cur_item = 0 result_coords = [own_coords[0]] - result_coords_origin = [LineStringSampling.PointSource.ENTER_LEAVING_POINT] + result_coords_origin = [ + LineStringSampling.PointSource.ENTER_LEAVING_POINT] for i in range(1, len(own_coords)): - next_distance = math.sqrt((own_coords[i][0]-own_coords[i-1][0])**2 + - (own_coords[i][1]-own_coords[i-1][1])**2) - while (current_item_index < len(nearest_points_list) and - total_distance+next_distance+constants.eps > nearest_points_list[current_item_index].projected_distance_parent): - - item = nearest_points_list[current_item_index] - child_coords, child_coords_origin = connect_raster_tree_nearest_neighbor( - item.child_node, used_offset, stitch_distance, item.nearest_point_child, offset_by_half) - - delta = item.nearest_point_parent.distance(Point(own_coords[i-1])) - if delta > abs_offset*constants.factor_offset_starting_points: + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_nearest_neighbor( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + d = item.nearest_point_parent.distance( + Point(own_coords[i - 1])) + if d > abs_offset * constants.factor_offset_starting_points: result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - # reversing avoids crossing when entering and leaving the child segment + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + # reversing avoids crossing when entering and + # leaving the child segment result_coords.extend(child_coords[::-1]) result_coords_origin.extend(child_coords_origin[::-1]) - - #And here we calculate the point for the leaving - delta = item.nearest_point_parent.distance(Point(own_coords[i])) - if current_item_index < len(nearest_points_list)-1: - delta = min(delta, abs( - nearest_points_list[current_item_index+1].projected_distance_parent-item.projected_distance_parent)) - - if delta > abs_offset*constants.factor_offset_starting_points: - result_coords.append(current_coords.interpolate( - item.projected_distance_parent+abs_offset*constants.factor_offset_starting_points).coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - - current_item_index += 1 - if i < len(own_coords)-1: - if(Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset*constants.factor_offset_remove_points): + # And here we calculate the point for the leaving + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + abs_offset * constants.factor_offset_starting_points + ).coords[0] + ) + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + + cur_item += 1 + if i < len(own_coords) - 1: + if ( + Point(result_coords[-1]).distance(Point(own_coords[i])) + > abs_offset * constants.factor_offset_remove_points + ): result_coords.append(own_coords[i]) result_coords_origin.append(own_coords_origin[i]) - # Since current_coords and temp are rastered differently there accumulate errors regarding the current distance. - # Since a projection of each point in temp would be very time consuming we project only every n-th point which resets the accumulated error every n-th point. + # Since current_coords and temp are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in temp would be very time + # consuming we project only every n-th point which resets + # the accumulated error every n-th point. if i % 20 == 0: total_distance = current_coords.project(Point(own_coords[i])) else: total_distance += next_distance - assert(len(result_coords) == len(result_coords_origin)) + assert len(result_coords) == len(result_coords_origin) return result_coords, result_coords_origin -#Takes a line and calculates the nearest distance along this line to enter the next_line -#Input: -#-travel_line: The "parent" line for which the distance should be minimized to enter next_line -#-next_line: contains the next_line which need to be entered -#-thresh: The distance between travel_line and next_line needs to below thresh to be a valid point for entering -#Output: -#-tuple - the tuple structure is: (nearest point in travel_line, nearest point in next_line) -def get_nearest_points_closer_than_thresh(travel_line, next_line,thresh): - point_list = list(MultiPoint(travel_line.coords)) + +def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): + """ + Takes a line and calculates the nearest distance along this + line to enter the next_line + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter next_line + -next_line: contains the next_line which need to be entered + -thresh: The distance between travel_line and next_line needs + to below thresh to be a valid point for entering + Output: + -tuple - the tuple structure is: + (nearest point in travel_line, nearest point in next_line) + """ + point_list = list(MultiPoint(travel_line.coords)) if point_list[0].distance(next_line) < thresh: return nearest_points(point_list[0], next_line) - for i in range(len(point_list)-1): - line_segment = LineString([point_list[i], point_list[i+1]]) - result = nearest_points(line_segment,next_line) + for i in range(len(point_list) - 1): + line_segment = LineString([point_list[i], point_list[i + 1]]) + result = nearest_points(line_segment, next_line) - if result[0].distance(result[1])< thresh: + if result[0].distance(result[1]) < thresh: return result line_segment = LineString([point_list[-1], point_list[0]]) - result = nearest_points(line_segment,next_line) + result = nearest_points(line_segment, next_line) - if result[0].distance(result[1])< thresh: + if result[0].distance(result[1]) < thresh: return result else: return None -#Takes a line and calculates the nearest distance along this line to enter the childs in children_list -#The method calculates the distances along the line and along the reversed line to find the best direction -#which minimizes the overall distance for all childs. -#Input: -#-travel_line: The "parent" line for which the distance should be minimized to enter the childs -#-children_list: contains the childs of travel_line which need to be entered -#-threshold: The distance between travel_line and a child needs to below threshold to be a valid point for entering -#-preferred_direction: Put a bias on the desired travel direction along travel_line. If equals zero no bias is applied. -# preferred_direction=1 means we prefer the direction of travel_line; preferred_direction=-1 means we prefer the opposite direction. -#Output: -#-stitching direction for travel_line -#-list of tuples (one tuple per child). The tuple structure is: ((nearest point in travel_line, nearest point in child), distance along travel_line, belonging child) -def create_nearest_points_list(travel_line, children_list, threshold, threshold_hard,preferred_direction=0): +def create_nearest_points_list( + travel_line, children_list, threshold, threshold_hard, preferred_direction=0 +): + """ + Takes a line and calculates the nearest distance along this line to + enter the childs in children_list + The method calculates the distances along the line and along the + reversed line to find the best direction which minimizes the overall + distance for all childs. + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter the childs + -children_list: contains the childs of travel_line which need to be entered + -threshold: The distance between travel_line and a child needs to be + below threshold to be a valid point for entering + -preferred_direction: Put a bias on the desired travel direction along + travel_line. If equals zero no bias is applied. + preferred_direction=1 means we prefer the direction of travel_line; + preferred_direction=-1 means we prefer the opposite direction. + Output: + -stitching direction for travel_line + -list of tuples (one tuple per child). The tuple structure is: + ((nearest point in travel_line, nearest point in child), + distance along travel_line, belonging child) + """ + result_list_in_order = [] result_list_reversed_order = [] @@ -238,67 +370,113 @@ def create_nearest_points_list(travel_line, children_list, threshold, threshold_ weight_in_order = 0 weight_reversed_order = 0 for child in children_list: - result = get_nearest_points_closer_than_thresh(travel_line, child.val, threshold) - if result == None: #where holes meet outer borders a distance up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh(travel_line, child.val, threshold_hard) - assert(result != None) + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold_hard + ) + assert result is not None proj = travel_line.project(result[0]) weight_in_order += proj - result_list_in_order.append(nearest_neighbor_tuple(nearest_point_parent = result[0], - nearest_point_child = result[1], - projected_distance_parent = proj, - child_node = child)) - - result = get_nearest_points_closer_than_thresh(travel_line_reversed, child.val, threshold) - if result == None: #where holes meet outer borders a distance up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh(travel_line_reversed, child.val, threshold_hard) - assert(result != None) + result_list_in_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold_hard + ) + assert result is not None proj = travel_line_reversed.project(result[0]) weight_reversed_order += proj - result_list_reversed_order.append(nearest_neighbor_tuple(nearest_point_parent = result[0], - nearest_point_child = result[1], - projected_distance_parent = proj, - child_node = child)) + result_list_reversed_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) if preferred_direction == 1: - weight_in_order=min(weight_in_order/2, max(0, weight_in_order-10*threshold)) + # Reduce weight_in_order to make in order stitching more preferred + weight_in_order = min( + weight_in_order / 2, max(0, weight_in_order - 10 * threshold) + ) if weight_in_order == weight_reversed_order: return (1, result_list_in_order) elif preferred_direction == -1: - weight_reversed_order=min(weight_reversed_order/2, max(0, weight_reversed_order-10*threshold)) + # Reduce weight_reversed_order to make reversed + # stitching more preferred + weight_reversed_order = min( + weight_reversed_order / + 2, max(0, weight_reversed_order - 10 * threshold) + ) if weight_in_order == weight_reversed_order: return (-1, result_list_reversed_order) - if weight_in_order < weight_reversed_order: return (1, result_list_in_order) else: return (-1, result_list_reversed_order) -def calculate_replacing_middle_point(line_segment, abs_offset,max_stich_distance): +def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): + """ + Takes a line segment (consisting of 3 points!) + and calculates a new middle point if the line_segment is + straight enough to be resampled by points max_stitch_distance apart. + Returns None if the middle point is not needed. + """ angles = LineStringSampling.calculate_line_angles(line_segment) - if angles[1] < abs_offset*constants.limiting_angle_straight: - if line_segment.length < max_stich_distance: + if angles[1] < abs_offset * constants.limiting_angle_straight: + if line_segment.length < max_stitch_distance: return None else: - return line_segment.interpolate(line_segment.length-max_stich_distance).coords[0] + return line_segment.interpolate( + line_segment.length - max_stitch_distance + ).coords[0] else: return line_segment.coords[1] -#Takes the offsetted curves organized as tree, connects and samples them. -#Strategy: A connection from parent to child is made as fast as possible to reach the innermost child as fast as possible in order -# to stich afterwards from inner to outer. -#Input: -#-tree: contains the offsetted curves in a hierachical organized data structure. -#-used_offset: used offset when the offsetted curves were generated -#-stitch_distance: maximum allowed distance between two points after sampling -#-close_point: defines the beginning point for stitching (stitching starts always from the undisplaced curve) -#-offset_by_half: If true the resulting points are interlaced otherwise not. -#Returnvalues: -#-All offsetted curves connected to one line and sampled with points obeying stitch_distance and offset_by_half -#-Tag (origin) of each point to analyze why a point was placed at this position -def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): + +def connect_raster_tree_from_inner_to_outer( + tree, used_offset, stitch_distance, close_point, offset_by_half +): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made as fast as possible to + reach the innermost child as fast as possible in order to stitch afterwards + from inner to outer. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with points obeying + stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was placed + at this position + """ current_coords = tree.val abs_offset = abs(used_offset) @@ -314,164 +492,280 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, if not tree.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert(item, math.fmod( - priority-start_distance+current_coords.length, current_coords.length)) + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) tree.transferred_point_priority_deque = new_DEPQ - #We try to use always the opposite stitching direction with respect to the parent to avoid crossings when entering and leaving the child + # We try to use always the opposite stitching direction with respect to the + # parent to avoid crossings when entering and leaving the child parent_stitching_direction = -1 - if tree.parent != None: + if tree.parent is not None: parent_stitching_direction = tree.parent.stitching_direction - #find the nearest point in current_coords and its children and sort it along the stitching direction - stitching_direction, nearest_points_list = create_nearest_points_list(current_coords, tree.children, 1.5*abs_offset,2.05*abs_offset,parent_stitching_direction) - nearest_points_list.sort(reverse=False, key=lambda tup: tup.projected_distance_parent) - - #Have a small offset for the starting and ending to avoid double points at start and end point (since the paths are closed rings) + # Find the nearest point in current_coords and its children and + # sort it along the stitching direction + stitching_direction, nearest_points_list = create_nearest_points_list( + current_coords, + tree.children, + 1.5 * abs_offset, + 2.05 * abs_offset, + parent_stitching_direction, + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + # Have a small offset for the starting and ending to avoid double points + # at start and end point (since the paths are closed rings) if nearest_points_list: - start_offset = min(abs_offset*constants.factor_offset_starting_points, nearest_points_list[0].projected_distance_parent) - end_offset = max(current_coords.length-abs_offset*constants.factor_offset_starting_points, nearest_points_list[-1].projected_distance_parent) + start_offset = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_offset = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) else: - start_offset = abs_offset*constants.factor_offset_starting_points - end_offset = current_coords.length-abs_offset*constants.factor_offset_starting_points - + start_offset = abs_offset * constants.factor_offset_starting_points + end_offset = ( + current_coords.length - abs_offset * constants.factor_offset_starting_points + ) if stitching_direction == 1: - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, start_offset, # We add start_offset to not sample the same point again (avoid double points for start and end) - end_offset, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + start_offset, # We add start_offset to not sample the same + # point again (avoid double points for start + # and end) + end_offset, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) else: - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, current_coords.length-start_offset, # We subtract start_offset to not sample the same point again (avoid double points for start and end) - current_coords.length-end_offset, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) - current_coords.coords = current_coords.coords[::-1] - - #Adjust the points origin for start and end (so that they might not be transferred to childs) - #if own_coords_origin[-1] != LineStringSampling.PointSource.HARD_EDGE: - # own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - #if own_coords_origin[0] != LineStringSampling.PointSource.HARD_EDGE: - # own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - assert(len(own_coords) == len(own_coords_origin)) - - #tree.val = LineString(own_coords) - #tree.pointsourcelist = own_coords_origin + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + current_coords.length - start_offset, # We subtract + # start_offset to not + # sample the same point + # again (avoid double + # points for start + # and end) + current_coords.length - end_offset, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) + current_coords.coords = current_coords.coords[::-1] + + assert len(own_coords) == len(own_coords_origin) + tree.stitching_direction = stitching_direction tree.already_rastered = True - to_transfer_point_list = [] to_transfer_point_list_origin = [] - for k in range(0, len(own_coords)): #TODO: maybe do not take the first and the last since they are ENTER_LEAVING_POINT points for sure - if (not offset_by_half and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT): + for k in range(0, len(own_coords)): + # TODO: maybe do not take the first and the last + # since they are ENTER_LEAVING_POINT points for sure + if ( + not offset_by_half + and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED + or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT + ): continue if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT: continue to_transfer_point_list.append(Point(own_coords[k])) to_transfer_point_list_origin.append(own_coords_origin[k]) - assert(len(to_transfer_point_list) == len(to_transfer_point_list_origin)) - - - #Next we need to transfer our rastered points to siblings and childs - - - #since the projection is only in ccw direction towards inner we need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,offset_by_half,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=False, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - - - #We transfer also to the overnext child to get a more straight arrangement of points perpendicular to the stitching lines + assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) + + # Next we need to transfer our rastered points to siblings and childs + # Since the projection is only in ccw direction towards inner we + # need to use "-used_offset" for stitching_direction==-1 + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines if offset_by_half: - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,False,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=True, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + if not nearest_points_list: - #If there is no child (inner geometry) we can simply take our own rastered coords as result + # If there is no child (inner geometry) we can simply + # take our own rastered coords as result result_coords = own_coords result_coords_origin = own_coords_origin else: - #There are childs so we need to merge their coordinates with our own rastered coords + # There are childs so we need to merge their coordinates + # with our own rastered coords - #Create a closed ring for the following code + # Create a closed ring for the following code own_coords.append(own_coords[0]) own_coords_origin.append(own_coords_origin[0]) - # own_coords does not start with current_coords but has an offset (see call of raster_line_string_with_priority_points) + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) total_distance = start_offset - current_item_index = 0 + cur_item = 0 result_coords = [own_coords[0]] result_coords_origin = [own_coords_origin[0]] for i in range(1, len(own_coords)): - next_distance = math.sqrt((own_coords[i][0]-own_coords[i-1][0])**2 + - (own_coords[i][1]-own_coords[i-1][1])**2) - while (current_item_index < len(nearest_points_list) and - total_distance+next_distance+constants.eps > nearest_points_list[current_item_index].projected_distance_parent): - #The current and the next point in own_coords enclose the nearest point tuple between this geometry and the child geometry. - #Hence we need to insert the child geometry points here before the next point of own_coords. - item = nearest_points_list[current_item_index] - child_coords, child_coords_origin = connect_raster_tree_from_inner_to_outer( - item.child_node, used_offset, stitch_distance, item.nearest_point_child, offset_by_half) - - #Imagine the nearest point of the child is within a long segment of the parent. Without additonal points - #on the parent side this would cause noticeable deviations. Hence we add here points shortly before and after - #the entering of the child to have only minor deviations to the desired shape. - #Here is the point for the entering: - if(Point(result_coords[-1]).distance(item.nearest_point_parent) > constants.factor_offset_starting_points*abs_offset): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + # The current and the next point in own_coords enclose the + # nearest point tuple between this geometry and child + # geometry. Hence we need to insert the child geometry points + # here before the next point of own_coords. + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_from_inner_to_outer( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + # Imagine the nearest point of the child is within a long + # segment of the parent. Without additonal points + # on the parent side this would cause noticeable deviations. + # Hence we add here points shortly before and after + # the entering of the child to have only minor deviations to + # the desired shape. + # Here is the point for the entering: + if ( + Point(result_coords[-1] + ).distance(item.nearest_point_parent) + > constants.factor_offset_starting_points * abs_offset + ): result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #if (abs(result_coords[-1][0]-61.7) < 0.2 and abs(result_coords[-1][1]-105.1) < 0.2): - # print("HIIER FOUNDED3") - - #Check whether the number of points of the connecting lines from child to child can be reduced + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + + # Check whether the number of points of the connecting lines + # from child to child can be reduced if len(child_coords) > 1: - point = calculate_replacing_middle_point(LineString([result_coords[-1],child_coords[0],child_coords[1]]),abs_offset,stitch_distance) - #if (abs(result_coords[-1][0]-8.9) < 0.2 and abs(result_coords[-1][1]-8.9) < 0.2): - # print("HIIER FOUNDED3") - if point != None: - #if (abs(point[0]-17.8) < 0.2 and abs(point[1]-17.8) < 0.2): - # print("HIIER FOUNDED3") + point = calculate_replacing_middle_point( + LineString( + [result_coords[-1], child_coords[0], child_coords[1]] + ), + abs_offset, + stitch_distance, + ) + + if point is not None: result_coords.append(point) result_coords_origin.append(child_coords_origin[0]) - + result_coords.extend(child_coords[1:]) result_coords_origin.extend(child_coords_origin[1:]) else: result_coords.extend(child_coords) result_coords_origin.extend(child_coords_origin) - #And here is the point for the leaving of the child (distance to the own following point should not be too large) - delta = item.nearest_point_parent.distance(Point(own_coords[i])) - if current_item_index < len(nearest_points_list)-1: - delta = min(delta, abs( - nearest_points_list[current_item_index+1].projected_distance_parent-item.projected_distance_parent)) - - if delta > constants.factor_offset_starting_points*abs_offset: - result_coords.append(current_coords.interpolate( - item.projected_distance_parent+2*constants.factor_offset_starting_points*abs_offset).coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #check whether this additional point makes the last point of the child unnecessary - point = calculate_replacing_middle_point(LineString([result_coords[-3],result_coords[-2],result_coords[-1]]),abs_offset,stitch_distance) - if point == None: + # And here is the point for the leaving of the child + # (distance to the own following point should not be too large) + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > constants.factor_offset_starting_points * abs_offset: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + 2 * constants.factor_offset_starting_points * abs_offset + ).coords[0] + ) + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + # Check whether this additional point makes the last point + # of the child unnecessary + point = calculate_replacing_middle_point( + LineString( + [result_coords[-3], result_coords[-2], result_coords[-1]] + ), + abs_offset, + stitch_distance, + ) + if point is None: result_coords.pop(-2) result_coords_origin.pop(-2) - #if (abs(result_coords[-1][0]-61.7) < 0.2 and abs(result_coords[-1][1]-105.1) < 0.2): - # print("HIIER FOUNDED3") - - current_item_index += 1 - if i < len(own_coords)-1: - if(Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset*constants.factor_offset_remove_points): + cur_item += 1 + if i < len(own_coords) - 1: + if ( + Point(result_coords[-1]).distance(Point(own_coords[i])) + > abs_offset * constants.factor_offset_remove_points + ): result_coords.append(own_coords[i]) result_coords_origin.append(own_coords_origin[i]) - # Since current_coords and own_coords are rastered differently there accumulate errors regarding the current distance. - # Since a projection of each point in own_coords would be very time consuming we project only every n-th point which resets the accumulated error every n-th point. + # Since current_coords and own_coords are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in own_coords would be very + # time consuming we project only every n-th point which resets + # the accumulated error every n-th point. if i % 20 == 0: total_distance = current_coords.project(Point(own_coords[i])) else: total_distance += next_distance - assert(len(result_coords) == len(result_coords_origin)) + assert len(result_coords) == len(result_coords_origin) return result_coords, result_coords_origin diff --git a/lib/stitches/DebuggingMethods.py b/lib/stitches/DebuggingMethods.py index d0f65576..e239edba 100644 --- a/lib/stitches/DebuggingMethods.py +++ b/lib/stitches/DebuggingMethods.py @@ -1,14 +1,11 @@ - import matplotlib.pyplot as plt from shapely.geometry import Polygon -from shapely.ops import nearest_points, substring, polygonize from anytree import PreOrderIter -from shapely.geometry.polygon import orient -#import LineStringSampling as Sampler + +# import LineStringSampling as Sampler import numpy as np import matplotlib.collections as mcoll -import matplotlib.path as mpath # def offset_polygons(polys, offset,joinstyle): # if polys.geom_type == 'Polygon': @@ -40,7 +37,7 @@ import matplotlib.path as mpath def plot_MultiPolygon(MultiPoly, plt, colorString): if MultiPoly.is_empty: return - if MultiPoly.geom_type == 'Polygon': + if MultiPoly.geom_type == "Polygon": x2, y2 = MultiPoly.exterior.xy plt.plot(x2, y2, colorString) @@ -56,6 +53,7 @@ def plot_MultiPolygon(MultiPoly, plt, colorString): x2, y2 = inners.coords.xy plt.plot(x2, y2, colorString) + # Test whether there are areas which would currently not be stitched but should be stitched @@ -65,12 +63,13 @@ def subtractResult(poly, rootPoly, offsetThresh): poly2 = poly2.difference(node.val.buffer(offsetThresh, 5, 3, 3)) return poly2 + # Used for debugging - plots all polygon exteriors within an AnyTree which is provided by the root node rootPoly. def drawPoly(rootPoly, colorString): fig, axs = plt.subplots(1, 1) - axs.axis('equal') + axs.axis("equal") plt.gca().invert_yaxis() for node in PreOrderIter(rootPoly): # if(node.id == "hole"): @@ -84,15 +83,26 @@ def drawPoly(rootPoly, colorString): def drawresult(resultcoords, resultcoords_Origin, colorString): fig, axs = plt.subplots(1, 1) - axs.axis('equal') + axs.axis("equal") plt.gca().invert_yaxis() plt.plot(*zip(*resultcoords), colorString) - colormap = np.array(['r', 'g', 'b', 'c', 'm', 'y', 'k', 'gray', 'm']) - labelmap = np.array(['MUST_USE', 'REGULAR_SPACING', 'INITIAL_RASTERING', 'EDGE_NEEDED', 'NOT_NEEDED', - 'ALREADY_TRANSFERRED', 'ADDITIONAL_TRACKING_POINT_NOT_NEEDED', 'EDGE_RASTERING_ALLOWED', 'EDGE_PREVIOUSLY_SHIFTED']) - - for i in range(0, 8+1): + colormap = np.array(["r", "g", "b", "c", "m", "y", "k", "gray", "m"]) + labelmap = np.array( + [ + "MUST_USE", + "REGULAR_SPACING", + "INITIAL_RASTERING", + "EDGE_NEEDED", + "NOT_NEEDED", + "ALREADY_TRANSFERRED", + "ADDITIONAL_TRACKING_POINT_NOT_NEEDED", + "EDGE_RASTERING_ALLOWED", + "EDGE_PREVIOUSLY_SHIFTED", + ] + ) + + for i in range(0, 8 + 1): # if i != Sampler.PointSource.EDGE_NEEDED and i != Sampler.PointSource.INITIAL_RASTERING: # continue selection = [] @@ -102,8 +112,8 @@ def drawresult(resultcoords, resultcoords_Origin, colorString): if len(selection) > 0: plt.scatter(*zip(*selection), c=colormap[i], label=labelmap[i]) - # plt.scatter(*zip(*resultcoords), - # c=colormap[resultcoords_Origin]) + # plt.scatter(*zip(*resultcoords), + # c=colormap[resultcoords_Origin]) axs.legend() plt.show(block=True) @@ -112,8 +122,14 @@ def drawresult(resultcoords, resultcoords_Origin, colorString): def colorline( - x, y, z=None, cmap=plt.get_cmap('copper'), norm=plt.Normalize(0.0, 1.0), - linewidth=3, alpha=1.0): + x, + y, + z=None, + cmap=plt.get_cmap("copper"), + norm=plt.Normalize(0.0, 1.0), + linewidth=3, + alpha=1.0, +): """ http://nbviewer.ipython.org/github/dpsanders/matplotlib-examples/blob/master/colorline.ipynb http://matplotlib.org/examples/pylab_examples/multicolored_line.html @@ -133,14 +149,16 @@ def colorline( z = np.asarray(z) segments = make_segments(x, y) - lc = mcoll.LineCollection(segments, array=z, cmap=cmap, norm=norm, - linewidth=linewidth, alpha=alpha) + lc = mcoll.LineCollection( + segments, array=z, cmap=cmap, norm=norm, linewidth=linewidth, alpha=alpha + ) ax = plt.gca() ax.add_collection(lc) return lc + # Used by colorline diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index 434c6bbf..07106515 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -1,4 +1,3 @@ -from sys import path from shapely.geometry.polygon import LineString from shapely.geometry import Point from shapely.ops import substring @@ -8,33 +7,41 @@ from enum import IntEnum from ..stitches import constants from ..stitches import PointTransfer -#Used to tag the origin of a rastered point +# Used to tag the origin of a rastered point + + class PointSource(IntEnum): - #MUST_USE = 0 # Legacy + # MUST_USE = 0 # Legacy REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance - #INITIAL_RASTERING = 2 #Legacy - EDGE_NEEDED = 3 # point which must be stitched to avoid to large deviations to the desired path - #NOT_NEEDED = 4 #Legacy - #ALREADY_TRANSFERRED = 5 #Legacy - #ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy - #EDGE_RASTERING_ALLOWED = 7 #Legacy - #EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy - ENTER_LEAVING_POINT = 9 #Whether this point is used to enter or leave a child - SOFT_EDGE_INTERNAL = 10 #If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - HARD_EDGE_INTERNAL = 11 #If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - PROJECTED_POINT = 12 #If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT - REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance - #FORBIDDEN_POINT_INTERNAL=14 #Legacy - SOFT_EDGE = 15 #If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - HARD_EDGE = 16 #If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - FORBIDDEN_POINT=17 #Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden - REPLACED_FORBIDDEN_POINT=18 #If one decides to avoid forbidden points new points to the left and to the right as replacement are created - DIRECT = 19 #Calculated by next neighbor projection - OVERNEXT = 20 #Calculated by overnext neighbor projection + # INITIAL_RASTERING = 2 #Legacy + # point which must be stitched to avoid to large deviations to the desired path + EDGE_NEEDED = 3 + # NOT_NEEDED = 4 #Legacy + # ALREADY_TRANSFERRED = 5 #Legacy + # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy + # EDGE_RASTERING_ALLOWED = 7 #Legacy + # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy + ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child + # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + SOFT_EDGE_INTERNAL = 10 + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE_INTERNAL = 11 + # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT + PROJECTED_POINT = 12 + REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance + # FORBIDDEN_POINT_INTERNAL=14 #Legacy + SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE = 16 + FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden + # If one decides to avoid forbidden points new points to the left and to the right as replacement are created + REPLACED_FORBIDDEN_POINT = 18 + DIRECT = 19 # Calculated by next neighbor projection + OVERNEXT = 20 # Calculated by overnext neighbor projection # Calculates the angles between adjacent edges at each interior point -#Note that the first and last values in the return array are zero since for the boundary points no angle calculations were possible +# Note that the first and last values in the return array are zero since for the boundary points no angle calculations were possible def calculate_line_angles(line): Angles = np.zeros(len(line.coords)) for i in range(1, len(line.coords)-1): @@ -42,44 +49,47 @@ def calculate_line_angles(line): vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) vec1length = np.linalg.norm(vec1) vec2length = np.linalg.norm(vec2) - #if vec1length <= 0: + # if vec1length <= 0: # print("HIER FEHLER") - - #if vec2length <=0: + + # if vec2length <=0: # print("HIER FEHLEr") - assert(vec1length >0) - assert(vec2length >0) - scalar_prod=np.dot(vec1, vec2)/(vec1length*vec2length) - scalar_prod = min(max(scalar_prod,-1),1) - #if scalar_prod > 1.0: + assert(vec1length > 0) + assert(vec2length > 0) + scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) + scalar_prod = min(max(scalar_prod, -1), 1) + # if scalar_prod > 1.0: # scalar_prod = 1.0 - #elif scalar_prod < -1.0: + # elif scalar_prod < -1.0: # scalar_prod = -1.0 Angles[i] = math.acos(scalar_prod) return Angles -#Rasters a line between start_distance and end_distance. -#Input: -#-line: The line to be rastered -#-start_distance: The distance along the line from which the rastering should start -#-end_distance: The distance along the line until which the rastering should be done -#-maxstitch_distance: The maximum allowed stitch distance -#-stitching_direction: =1 is stitched along line direction, =-1 if stitched in reversed order. Note that +# Rasters a line between start_distance and end_distance. +# Input: +# -line: The line to be rastered +# -start_distance: The distance along the line from which the rastering should start +# -end_distance: The distance along the line until which the rastering should be done +# -maxstitch_distance: The maximum allowed stitch distance +# -stitching_direction: =1 is stitched along line direction, =-1 if stitched in reversed order. Note that # start_distance > end_distance for stitching_direction = -1 -#-must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -#-abs_offset: used offset between to offsetted curves -#Output: -#-List of tuples with the rastered point coordinates -#-List which defines the point origin for each point according to the PointSource enum. -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, stitching_direction, must_use_points_deque, abs_offset): +# -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque +# is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) +# index of point_origin is the index of the point in the neighboring line +# -abs_offset: used offset between to offsetted curves +# Output: +# -List of tuples with the rastered point coordinates +# -List which defines the point origin for each point according to the PointSource enum. + + +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, + stitching_direction, must_use_points_deque, abs_offset): if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] assert (stitching_direction == -1 and start_distance >= end_distance) or ( stitching_direction == 1 and start_distance <= end_distance) - + deque_points = list(must_use_points_deque) linecoords = line.coords @@ -92,7 +102,8 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, deque_points[i] = (deque_points[i][0], line.length-deque_points[i][1]) else: - deque_points = deque_points[::-1] #Since points with highest priority (=distance along line) are first (descending sorted) + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): @@ -107,95 +118,109 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, path_coords = substring(aligned_line, start_distance, end_distance) - #aligned line is a line without doubled points. I had the strange situation in which the offset "start_distance" from the line beginning resulted in a starting point which was - # already present in aligned_line causing a doubled point. A double point is not allowed in the following calculations so we need to remove it: - if abs(path_coords.coords[0][0]-path_coords.coords[1][0]) OVERNEXT projected point > DIRECT projected point) as termination of this segment + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment # and start point for the next segment (so we do not always take the maximum possible length for a segment) segment_start_index = 0 segment_end_index = 1 forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - #if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: + while segment_end_index < len(merged_point_list): + # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and + # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: # print("GEFUNDEN") - #Collection of points for the current segment + # Collection of points for the current segment current_point_list = [merged_point_list[segment_start_index][0].point] - + while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1]-merged_point_list[segment_start_index][1] + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1]+maxstitch_distance - merged_point_list.insert(segment_end_index,(PointTransfer.projected_point_tuple(point=aligned_line.interpolate(new_distance), point_source=\ - PointSource.REGULAR_SPACING_INTERNAL),new_distance)) - if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: - print("GEFUNDEN") - segment_end_index+=1 + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + # if (abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9) < 0.2): + # print("GEFUNDEN") + segment_end_index += 1 break - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: # print("GEFUNDEN") - - current_point_list.append(merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify(constants.factor_offset_remove_dense_points*abs_offset,preserve_topology=False).coords) - if simplified_len > 2: #not all points have been simplified - so we need to add it + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it break - if merged_point_list[segment_end_index][0].point_source ==PointSource.HARD_EDGE_INTERNAL: - segment_end_index+=1 + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 break - segment_end_index+=1 + segment_end_index += 1 - segment_end_index-=1 + segment_end_index -= 1 - #Now we choose the best fitting point within this segment + # Now we choose the best fitting point within this segment index_overnext = -1 index_direct = -1 index_hard_edge = -1 - iter = segment_start_index+1 + iter = segment_start_index+1 while (iter <= segment_end_index): if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: index_overnext = iter @@ -208,48 +233,48 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, segment_end_index = index_hard_edge else: if index_overnext != -1: - if (index_direct != -1 and index_direct > index_overnext and - (merged_point_list[index_direct][1]-merged_point_list[index_overnext][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext* + if (index_direct != -1 and index_direct > index_overnext and + (merged_point_list[index_direct][1]-merged_point_list[index_overnext][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * (merged_point_list[index_overnext][1]-merged_point_list[segment_start_index][1])): - #We allow to take the direct projected point instead of the overnext projected point if it would result in a - #significant longer segment length + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length segment_end_index = index_direct else: segment_end_index = index_overnext elif index_direct != -1: segment_end_index = index_direct - #Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - #If they are too close ( end_distance for stitching_direction = -1 -#-must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -#-abs_offset: used offset between to offsetted curves -#Output: -#-List of tuples with the rastered point coordinates -#-List which defines the point origin for each point according to the PointSource enum. +# -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque +# is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) +# index of point_origin is the index of the point in the neighboring line +# -abs_offset: used offset between to offsetted curves +# Output: +# -List of tuples with the rastered point coordinates +# -List which defines the point origin for each point according to the PointSource enum. + + def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stitching_direction, must_use_points_deque, abs_offset, offset_by_half): if (line.length < constants.line_lengh_seen_as_one_point): return [line.coords[0]], [PointSource.HARD_EDGE] - + deque_points = list(must_use_points_deque) linecoords = line.coords - if stitching_direction==-1: + if stitching_direction == -1: linecoords = linecoords[::-1] for i in range(len(deque_points)): deque_points[i] = (deque_points[i][0], line.length-deque_points[i][1]) else: - deque_points = deque_points[::-1] #Since points with highest priority (=distance along line) are first (descending sorted) + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] # Ordering in priority queue: # (point, LineStringSampling.PointSource), priority) - aligned_line = LineString(linecoords) #might be different from line for stitching_direction=-1 + # might be different from line for stitching_direction=-1 + aligned_line = LineString(linecoords) angles = calculate_line_angles(aligned_line) - #For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge + # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge angles[0] = 1.1*constants.limiting_angle - angles[-1] = 1.1*constants.limiting_angle + angles[-1] = 1.1*constants.limiting_angle current_distance = 0.0 - #Next we merge the line points and the projected (deque) points into one list + # Next we merge the line points and the projected (deque) points into one list merged_point_list = [] dq_iter = 0 - for point,angle in zip(aligned_line.coords,angles): - #if abs(point[0]-52.9) < 0.2 and abs(point[1]-183.4)< 0.2: + for point, angle in zip(aligned_line.coords, angles): + # if abs(point[0]-52.9) < 0.2 and abs(point[1]-183.4)< 0.2: # print("GEFUNDEN") current_distance = aligned_line.project(Point(point)) while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance: - #We want to avoid setting points at soft edges close to forbidden points + # We want to avoid setting points at soft edges close to forbidden points if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: - #Check whether a previous added point is a soft edge close to the forbidden point - if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and + # Check whether a previous added point is a soft edge close to the forbidden point + if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and abs(merged_point_list[-1][1]-deque_points[dq_iter][1] < abs_offset*constants.factor_offset_forbidden_point)): item = merged_point_list.pop() - merged_point_list.append((PointTransfer.projected_point_tuple(point=item[0].point, point_source=\ - PointSource.FORBIDDEN_POINT),item[1])) + merged_point_list.append((PointTransfer.projected_point_tuple( + point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1])) else: merged_point_list.append(deque_points[dq_iter]) - dq_iter+=1 - #Check whether the current point is close to a forbidden point - if (dq_iter < len(deque_points) and + dq_iter += 1 + # Check whether the current point is close to a forbidden point + if (dq_iter < len(deque_points) and deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and angle < constants.limiting_angle and - abs(deque_points[dq_iter-1][1]-current_distance) < abs_offset*constants.factor_offset_forbidden_point): + abs(deque_points[dq_iter-1][1]-current_distance) < abs_offset*constants.factor_offset_forbidden_point): point_source = PointSource.FORBIDDEN_POINT else: if angle < constants.limiting_angle: point_source = PointSource.SOFT_EDGE_INTERNAL else: point_source = PointSource.HARD_EDGE_INTERNAL - merged_point_list.append((PointTransfer.projected_point_tuple(point=Point(point), point_source=point_source),current_distance)) + merged_point_list.append((PointTransfer.projected_point_tuple( + point=Point(point), point_source=point_source), current_distance)) result_list = [merged_point_list[0]] - - #General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified to a straight line by shapelys simplify method. - #Then, look at the points within this segment and choose the best fitting one (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment # and start point for the next segment (so we do not always take the maximum possible length for a segment) segment_start_index = 0 segment_end_index = 1 forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - #if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: + while segment_end_index < len(merged_point_list): + # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and + # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: # print("GEFUNDEN") - #Collection of points for the current segment + # Collection of points for the current segment current_point_list = [merged_point_list[segment_start_index][0].point] - + while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1]-merged_point_list[segment_start_index][1] + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1]+maxstitch_distance - merged_point_list.insert(segment_end_index,(PointTransfer.projected_point_tuple(point=aligned_line.interpolate(new_distance), point_source=\ - PointSource.REGULAR_SPACING_INTERNAL),new_distance)) - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and 7 + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: # print("GEFUNDEN") - segment_end_index+=1 + segment_end_index += 1 break - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-34.4) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-6.2)< 0.2: + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-34.4) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-6.2)< 0.2: # print("GEFUNDEN") - - current_point_list.append(merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify(constants.factor_offset_remove_dense_points*abs_offset,preserve_topology=False).coords) - if simplified_len > 2: #not all points have been simplified - so we need to add it + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it break - if merged_point_list[segment_end_index][0].point_source ==PointSource.HARD_EDGE_INTERNAL: - segment_end_index+=1 + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 break - segment_end_index+=1 + segment_end_index += 1 - segment_end_index-=1 + segment_end_index -= 1 - #Now we choose the best fitting point within this segment + # Now we choose the best fitting point within this segment index_overnext = -1 index_direct = -1 index_hard_edge = -1 - iter = segment_start_index+1 + iter = segment_start_index+1 while (iter <= segment_end_index): if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: index_overnext = iter @@ -406,48 +444,49 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit index_less_preferred = index_overnext if index_preferred != -1: - if (index_less_preferred != -1 and index_less_preferred > index_preferred and - (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext* + if (index_less_preferred != -1 and index_less_preferred > index_preferred and + (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): - #We allow to take the direct projected point instead of the overnext projected point if it would result in a - #significant longer segment length + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length segment_end_index = index_less_preferred else: segment_end_index = index_preferred elif index_less_preferred != -1: segment_end_index = index_less_preferred - #Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - #If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: new_point_left_proj = result_list[index][1]-distance_left if new_point_left_proj < 0: new_point_left_proj += line.length new_point_right_proj = result_list[index][1]+distance_right if new_point_right_proj > line.length: - new_point_right_proj-=line.length + new_point_right_proj -= line.length point_left = line.interpolate(new_point_left_proj) point_right = line.interpolate(new_point_right_proj) - forbidden_point_distance = result_list[index][0].point.distance(LineString([point_left, point_right])) + forbidden_point_distance = result_list[index][0].point.distance( + LineString([point_left, point_right])) if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: del result_list[index] - result_list.insert(index, (PointTransfer.projected_point_tuple(point=point_right, point_source=\ - PointSource.REPLACED_FORBIDDEN_POINT),new_point_right_proj)) - result_list.insert(index, (PointTransfer.projected_point_tuple(point=point_left, point_source=\ - PointSource.REPLACED_FORBIDDEN_POINT),new_point_left_proj)) - current_index_shift+=1 + result_list.insert(index, (PointTransfer.projected_point_tuple( + point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) + result_list.insert(index, (PointTransfer.projected_point_tuple( + point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) + current_index_shift += 1 break else: - distance_left/=2.0 - distance_right/=2.0 + distance_left /= 2.0 + distance_right /= 2.0 return result_list + if __name__ == "__main__": - line = LineString([(0,0), (1,0), (2,1),(3,0),(4,0)]) + line = LineString([(0, 0), (1, 0), (2, 1), (3, 0), (4, 0)]) print(calculate_line_angles(line)*180.0/math.pi) diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index 998282a3..b4c6c004 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -1,4 +1,4 @@ -from shapely.geometry import Point, MultiPoint +from shapely.geometry import Point, MultiPoint from shapely.geometry.polygon import LineString, LinearRing from collections import namedtuple from shapely.ops import nearest_points @@ -6,11 +6,14 @@ import math from ..stitches import constants from ..stitches import LineStringSampling -projected_point_tuple = namedtuple('projected_point_tuple', ['point', 'point_source']) +projected_point_tuple = namedtuple( + 'projected_point_tuple', ['point', 'point_source']) + +# Calculated the nearest interserction point of "bisectorline" with the coordinates of child (child.val). +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. + -#Calculated the nearest interserction point of "bisectorline" with the coordinates of child (child.val). -#It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -#intersection was found. def calc_transferred_point(bisectorline, child): result = bisectorline.intersection(child.val) if result.is_empty: @@ -24,37 +27,44 @@ def calc_transferred_point(bisectorline, child): resultlist = list(result) desired_point = resultlist[0] if len(resultlist) > 1: - desired_point = nearest_points(result, Point(bisectorline.coords[0]))[0] + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] priority = child.val.project(desired_point) point = desired_point return point, priority -#Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs -# To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. -#Input: -#-treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. -#-used_offset: The used offset when the curves where offsetted -#-offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" -#-max_stitching_distance: The maximum allowed stitch distance between two points -#-to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points can be handled as closed ring -#-to_transfer_points_origin: The origin tag of each point in to_transfer_points -#-overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) -#-transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as forbidden points to the neighbor to avoid a point placing there -#-transfer_to_parent: If True, points will be transferred to the parent -#-transfer_to_sibling: If True, points will be transferred to the siblings -#-transfer_to_child: If True, points will be transferred to the childs -#Output: -#-Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_stitching_distance, to_transfer_points, to_transfer_points_origin=[], - overnext_neighbor = False, transfer_forbidden_points = False, transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): - - assert(len(to_transfer_points)==len(to_transfer_points_origin) or len(to_transfer_points_origin) == 0) +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], + overnext_neighbor=False, transfer_forbidden_points=False, + transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): + """ + Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -to_transfer_points_origin: The origin tag of each point in to_transfer_points + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_parent: If True, points will be transferred to the parent + -transfer_to_sibling: If True, points will be transferred to the siblings + -transfer_to_child: If True, points will be transferred to the childs + Output: + -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert(len(to_transfer_points) == len(to_transfer_points_origin) + or len(to_transfer_points_origin) == 0) assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and (offset_by_half or not offset_by_half and overnext_neighbor)) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) if len(to_transfer_points) == 0: return @@ -71,37 +81,37 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st if transfer_to_child: for child in childs_tuple: - if child.already_rastered == False: + if not child.already_rastered: if not overnext_neighbor: child_list.append(child) if transfer_forbidden_points: child_list_forbidden.append(child) if overnext_neighbor: for subchild in child.children: - if subchild.already_rastered == False: + if not subchild.already_rastered: child_list.append(subchild) if transfer_to_sibling: for sibling in siblings_tuple: - if sibling.already_rastered == False: + if not sibling.already_rastered: if not overnext_neighbor: neighbor_list.append(sibling) if transfer_forbidden_points: neighbor_list_forbidden.append(sibling) if overnext_neighbor: for subchild in sibling.children: - if subchild.already_rastered == False: + if not subchild.already_rastered: neighbor_list.append(subchild) - if transfer_to_parent and treenode.parent != None: - if treenode.parent.already_rastered == False: + if transfer_to_parent and treenode.parent is not None: + if not treenode.parent.already_rastered: if not overnext_neighbor: - neighbor_list.append(treenode.parent) + neighbor_list.append(treenode.parent) if transfer_forbidden_points: - neighbor_list_forbidden.append(treenode.parent) + neighbor_list_forbidden.append(treenode.parent) if overnext_neighbor: - if treenode.parent.parent != None: - if treenode.parent.parent.already_rastered == False: + if treenode.parent.parent is not None: + if not treenode.parent.parent.already_rastered: neighbor_list.append(treenode.parent.parent) if not neighbor_list and not child_list: @@ -126,19 +136,20 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st closed_line = LinearRing(to_transfer_points) bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor*(2.0 if overnext_neighbor else 1.0) + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) bisectorline_length_forbidden_points = abs(used_offset) * \ constants.transfer_point_distance_factor linesign_child = math.copysign(1, used_offset) - i = 0 currentDistance = 0 while i < len(point_list): - assert(point_source_list[i] != LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: + assert(point_source_list[i] != + LineStringSampling.PointSource.ENTER_LEAVING_POINT) + # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: # print("HIIIIIIIIIIIERRR") # We create a bisecting line through the current point @@ -152,7 +163,6 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st normalized_vector_prev_x /= prev_spacing normalized_vector_prev_y /= prev_spacing - normalized_vector_next_x = normalized_vector_next_y = 0 next_spacing = 0 while True: @@ -187,13 +197,15 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st vecy = -linesign_child*bisectorline_length*normalized_vector_next_x if transfer_forbidden_points: - vecx_forbidden_point = linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_x + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x else: vecx *= bisectorline_length/vec_length vecy *= bisectorline_length/vec_length - + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: vecx = -vecx vecy = -vecy @@ -212,55 +224,66 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st originPoint = closed_line.interpolate(off) bisectorline_child = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) + originPoint.coords[0][1]), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) bisectorline_neighbor = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy)]) + originPoint.coords[0][1]), + (originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy)]) bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) for child in child_list: - point, priority = calc_transferred_point(bisectorline_child,child) - if point==None: + point, priority = calc_transferred_point(bisectorline_child, child) + if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for child in child_list_forbidden: - point, priority = calc_transferred_point(bisectorline_forbidden_point_child,child) - if point == None: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_child, child) + if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + for neighbor in neighbor_list: - point, priority = calc_transferred_point(bisectorline_neighbor,neighbor) - if point==None: + point, priority = calc_transferred_point( + bisectorline_neighbor, neighbor) + if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for neighbor in neighbor_list_forbidden: - point, priority = calc_transferred_point(bisectorline_forbidden_point_neighbor,neighbor) - if point == None: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_neighbor, neighbor) + if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) i += 1 currentDistance += next_spacing assert(len(point_list) == len(point_source_list)) -#Calculated the nearest interserction point of "bisectorline" with the coordinates of child. -#It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -#intersection was found. +# Calculated the nearest interserction point of "bisectorline" with the coordinates of child. +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. + + def calc_transferred_point_graph(bisectorline, edge_geometry): result = bisectorline.intersection(edge_geometry) if result.is_empty: @@ -274,41 +297,44 @@ def calc_transferred_point_graph(bisectorline, edge_geometry): resultlist = list(result) desired_point = resultlist[0] if len(resultlist) > 1: - desired_point = nearest_points(result, Point(bisectorline.coords[0]))[0] + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] priority = edge_geometry.project(desired_point) point = desired_point return point, priority -#Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs -# To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. -#Input: -#-treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. -#-used_offset: The used offset when the curves where offsetted -#-offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" -#-max_stitching_distance: The maximum allowed stitch distance between two points -#-to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points can be handled as closed ring -#-to_transfer_points_origin: The origin tag of each point in to_transfer_points -#-overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) -#-transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as forbidden points to the neighbor to avoid a point placing there -#-transfer_to_parent: If True, points will be transferred to the parent -#-transfer_to_sibling: If True, points will be transferred to the siblings -#-transfer_to_child: If True, points will be transferred to the childs -#Output: -#-Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, - overnext_neighbor = False, transfer_forbidden_points = False, transfer_to_previous=True, transfer_to_next=True): + overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): + """ + Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -fill_stitch_graph: Graph data structure of the stitching lines + -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_previous: If True, points will be transferred to the previous edge in the graph + -transfer_to_next: If True, points will be transferred to the next edge in the graph + Output: + -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and (offset_by_half or not offset_by_half and overnext_neighbor)) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) if len(to_transfer_points) == 0: return - # Take only neighbors which have not rastered before # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) previous_edge_list = [] @@ -319,7 +345,8 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if transfer_to_previous: previous_neighbors_tuples = current_edge['previous_neighbors'] for neighbor in previous_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0]][neighbor[-1]]['segment'] + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] if not neighbor_edge['already_rastered']: if not overnext_neighbor: previous_edge_list.append(neighbor_edge) @@ -328,14 +355,16 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if overnext_neighbor: overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] for overnext_neighbor in overnext_previous_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0]][overnext_neighbor[-1]]['segment'] + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] if not overnext_neighbor_edge['already_rastered']: previous_edge_list.append(overnext_neighbor_edge) if transfer_to_next: next_neighbors_tuples = current_edge['next_neighbors'] for neighbor in next_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0]][neighbor[-1]]['segment'] + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] if not neighbor_edge['already_rastered']: if not overnext_neighbor: next_edge_list.append(neighbor_edge) @@ -344,11 +373,11 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if overnext_neighbor: overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] for overnext_neighbor in overnext_next_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0]][overnext_neighbor[-1]]['segment'] + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] if not overnext_neighbor_edge['already_rastered']: next_edge_list.append(overnext_neighbor_edge) - if not previous_edge_list and not next_edge_list: return @@ -357,19 +386,19 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o line = LineString(to_transfer_points) bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor*(2.0 if overnext_neighbor else 1.0) + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) bisectorline_length_forbidden_points = abs(used_offset) * \ constants.transfer_point_distance_factor linesign_child = math.copysign(1, used_offset) - i = 0 currentDistance = 0 while i < len(point_list): - - #if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: + + # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: # print("HIIIIIIIIIIIERRR") # We create a bisecting line through the current point @@ -383,7 +412,6 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o normalized_vector_prev_x /= prev_spacing normalized_vector_prev_y /= prev_spacing - normalized_vector_next_x = normalized_vector_next_y = 0 next_spacing = 0 while True: @@ -416,13 +444,15 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o vecy = -linesign_child*bisectorline_length*normalized_vector_next_x if transfer_forbidden_points: - vecx_forbidden_point = linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_x + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x else: vecx *= bisectorline_length/vec_length vecy *= bisectorline_length/vec_length - + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: vecx = -vecx vecy = -vecy @@ -446,22 +476,25 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o originPoint.coords[0][1]+vecy)]) bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) for edge in previous_edge_list+next_edge_list: - point, priority = calc_transferred_point_graph(bisectorline,edge['geometry']) - if point==None: + point, priority = calc_transferred_point_graph( + bisectorline, edge['geometry']) + if point is None: continue - edge['projected_points'].insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + edge['projected_points'].insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: - point, priority = calc_transferred_point_graph(bisectorline_forbidden_point,edge_forbidden['geometry']) - if point == None: + point, priority = calc_transferred_point_graph( + bisectorline_forbidden_point, edge_forbidden['geometry']) + if point is None: continue - edge_forbidden['projected_points'].insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - + edge_forbidden['projected_points'].insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + i += 1 currentDistance += next_spacing diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index d0a3f7aa..ba3e3031 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -1,6 +1,6 @@ from shapely.geometry.polygon import LinearRing, LineString from shapely.geometry import Polygon, MultiLineString -from shapely.ops import polygonize +from shapely.ops import polygonize from shapely.geometry import MultiPolygon from anytree import AnyNode, PreOrderIter from shapely.geometry.polygon import orient @@ -10,68 +10,90 @@ from ..stitches import ConnectAndSamplePattern from ..stitches import constants - -# Problem: When shapely offsets a LinearRing the start/end point might be handled wrongly since they are only treated as LineString. -# (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) -# This method checks first whether the start/end point form a problematic edge with respect to the offset side. If it is not a problematic -# edge we can use the normal offset_routine. Otherwise we need to perform two offsets: -# -offset the ring -# -offset the start/end point + its two neighbors left and right -# Finally both offsets are merged together to get the correct offset of a LinearRing def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): + """ + Solves following problem: When shapely offsets a LinearRing the + start/end point might be handled wrongly since they + are only treated as LineString. + (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) + This method checks first whether the start/end point form a problematic + edge with respect to the offset side. If it is not a problematic + edge we can use the normal offset_routine. Otherwise we need to + perform two offsets: + -offset the ring + -offset the start/end point + its two neighbors left and right + Finally both offsets are merged together to get the correct + offset of a LinearRing + """ + coords = ring.coords[:] - # check whether edge at index 0 is concave or convex. Only for concave edges we need to spend additional effort + # check whether edge at index 0 is concave or convex. Only for + # concave edges we need to spend additional effort dx_seg1 = dy_seg1 = 0 if coords[0] != coords[-1]: - dx_seg1 = coords[0][0]-coords[-1][0] - dy_seg1 = coords[0][1]-coords[-1][1] + dx_seg1 = coords[0][0] - coords[-1][0] + dy_seg1 = coords[0][1] - coords[-1][1] else: - dx_seg1 = coords[0][0]-coords[-2][0] - dy_seg1 = coords[0][1]-coords[-2][1] - dx_seg2 = coords[1][0]-coords[0][0] - dy_seg2 = coords[1][1]-coords[0][1] + dx_seg1 = coords[0][0] - coords[-2][0] + dy_seg1 = coords[0][1] - coords[-2][1] + dx_seg2 = coords[1][0] - coords[0][0] + dy_seg2 = coords[1][1] - coords[0][1] # use cross product: - crossvalue = dx_seg1*dy_seg2-dy_seg1*dx_seg2 + crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 sidesign = 1 - if side == 'left': + if side == "left": sidesign = -1 - # We do not need to take care of the joint n-0 since we offset along a concave edge: - if sidesign*offset*crossvalue <= 0: + # We do not need to take care of the joint n-0 since we + # offset along a concave edge: + if sidesign * offset * crossvalue <= 0: return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) # We offset along a convex edge so we offset the joint n-0 separately: if coords[0] != coords[-1]: coords.append(coords[0]) offset_ring1 = ring.parallel_offset( - offset, side, resolution, join_style, mitre_limit) + offset, side, resolution, join_style, mitre_limit + ) offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( - offset, side, resolution, join_style, mitre_limit) + offset, side, resolution, join_style, mitre_limit + ) # Next we need to merge the results: - if offset_ring1.geom_type == 'LineString': - return LinearRing(offset_ring2.coords[:]+offset_ring1.coords[1:-1]) + if offset_ring1.geom_type == "LineString": + return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) else: - # We have more than one resulting LineString for offset of the geometry (ring) = offset_ring1. - # Hence we need to find the LineString which belongs to the offset of element 0 in coords =offset_ring2 + # We have more than one resulting LineString for offset of + # the geometry (ring) = offset_ring1. + # Hence we need to find the LineString which belongs to the + # offset of element 0 in coords =offset_ring2 # in order to add offset_ring2 geometry to it: result_list = [] - thresh = constants.offset_factor_for_adjacent_geometry*abs(offset) + thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) for offsets in offset_ring1: - if(abs(offsets.coords[0][0]-coords[0][0]) < thresh and abs(offsets.coords[0][1]-coords[0][1]) < thresh): - result_list.append(LinearRing( - offset_ring2.coords[:]+offsets.coords[1:-1])) + if ( + abs(offsets.coords[0][0] - coords[0][0]) < thresh + and abs(offsets.coords[0][1] - coords[0][1]) < thresh + ): + result_list.append( + LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) + ) else: result_list.append(LinearRing(offsets)) return MultiLineString(result_list) -# Removes all geometries which do not form a "valid" LinearRing (meaning a ring which does not form a straight line) def take_only_valid_linear_rings(rings): - if(rings.geom_type == 'MultiLineString'): + """ + Removes all geometries which do not form a "valid" LinearRing + (meaning a ring which does not form a straight line) + """ + if rings.geom_type == "MultiLineString": new_list = [] for ring in rings: - if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): + if len(ring.coords) > 3 or ( + len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] + ): new_list.append(ring) if len(new_list) == 1: return LinearRing(new_list[0]) @@ -86,138 +108,184 @@ def take_only_valid_linear_rings(rings): return rings -#Since naturally holes have the opposite point ordering than non-holes we make -#all lines within the tree "root" uniform (having all the same ordering direction) def make_tree_uniform_ccw(root): + """ + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree "root" uniform (having all the same + ordering direction) + """ for node in PreOrderIter(root): - if(node.id == 'hole'): + if node.id == "hole": node.val.coords = list(node.val.coords)[::-1] -#Used to define which stitching strategy shall be used +# Used to define which stitching strategy shall be used class StitchingStrategy(IntEnum): CLOSEST_POINT = 0 INNER_TO_OUTER = 1 -# Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. -# These created geometries are afterwards connected to each other and resampled with a maximum stitch_distance. -# The return value is a LineString which should cover the full polygon. -#Input: -#-poly: The shapely polygon which can have holes -#-offset: The used offset for the curves -#-join_style: Join style for the offset - can be round, mitered or bevel (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) -#For examples look at https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png -#-stitch_distance maximum allowed stitch distance between two points -#-offset_by_half: True if the points shall be interlaced -#-strategy: According to StitchingStrategy you can select between different strategies for the connection between parent and childs -#Output: -#-List of point coordinate tuples -#-Tag (origin) of each point to analyze why a point was placed at this position -def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): + +def offset_poly( + poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point +): + """ + Takes a polygon (which can have holes) as input and creates offsetted + versions until the polygon is filled with these smaller offsets. + These created geometries are afterwards connected to each other and + resampled with a maximum stitch_distance. + The return value is a LineString which should cover the full polygon. + Input: + -poly: The shapely polygon which can have holes + -offset: The used offset for the curves + -join_style: Join style for the offset - can be round, mitered or bevel + (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) + For examples look at + https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png + -stitch_distance maximum allowed stitch distance between two points + -offset_by_half: True if the points shall be interlaced + -strategy: According to StitchingStrategy enum class you can select between + different strategies for the connection between parent and childs + -starting_point: Defines the starting point for the stitching + Output: + -List of point coordinate tuples + -Tag (origin) of each point to analyze why a point was placed + at this position + """ ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify( - constants.simplification_threshold, False) - root = AnyNode(id="node", val=ordered_poly.exterior, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + ordered_poly = ordered_poly.simplify(constants.simplification_threshold, False) + root = AnyNode( + id="node", + val=ordered_poly.exterior, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) active_polys = [root] active_holes = [[]] for holes in ordered_poly.interiors: - #print("hole: - is ccw: ", LinearRing(holes).is_ccw) active_holes[0].append( - AnyNode(id="hole", val=holes, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None))) + AnyNode( + id="hole", + val=holes, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) + ) - # counter = 0 - while len(active_polys) > 0: # and counter < 20: - # counter += 1 - # print("New iter") + while len(active_polys) > 0: current_poly = active_polys.pop() current_holes = active_holes.pop() poly_inners = [] - # outer = current_poly.val.parallel_offset(offset,'left', 5, join_style, 10) - outer = offset_linear_ring(current_poly.val, offset, 'left', 5, join_style, 10) + outer = offset_linear_ring( + current_poly.val, + offset, + "left", + resolution=5, + joint_style=join_style, + mitre_limit=10, + ) outer = outer.simplify(constants.simplification_threshold, False) outer = take_only_valid_linear_rings(outer) for j in range(len(current_holes)): - # inner = closeLinearRing(current_holes[j].val,offset/2.0).parallel_offset(offset,'left', 5, join_style, 10) inner = offset_linear_ring( - current_holes[j].val, offset, 'left', 5, join_style, 10) + current_holes[j].val, + offset, + "left", + resolution=5, + joint_style=join_style, + mitre_limit=10, + ) inner = inner.simplify(constants.simplification_threshold, False) inner = take_only_valid_linear_rings(inner) if not inner.is_empty: poly_inners.append(Polygon(inner)) if not outer.is_empty: if len(poly_inners) == 0: - if outer.geom_type == 'LineString': + if outer.geom_type == "LineString": result = Polygon(outer) else: result = MultiPolygon(polygonize(outer)) else: - if outer.geom_type == 'LineString': - result = Polygon(outer).difference( - MultiPolygon(poly_inners)) + if outer.geom_type == "LineString": + result = Polygon(outer).difference(MultiPolygon(poly_inners)) else: - result = MultiPolygon(outer).difference( - MultiPolygon(poly_inners)) + result = MultiPolygon(outer).difference(MultiPolygon(poly_inners)) - if not result.is_empty and result.area > offset*offset/10: + if not result.is_empty and result.area > offset * offset / 10: result_list = [] - if result.geom_type == 'Polygon': + if result.geom_type == "Polygon": result_list = [result] else: result_list = list(result) - # print("New result_list: ", len(result_list)) + for polygon in result_list: polygon = orient(polygon, -1) - if polygon.area < offset*offset/10: + if polygon.area < offset * offset / 10: continue - polygon = polygon.simplify(constants.simplification_threshold, False) + polygon = polygon.simplify( + constants.simplification_threshold, False + ) poly_coords = polygon.exterior - # if polygon.exterior.is_ccw: - # hole.coords = list(hole.coords)[::-1] - #poly_coords = polygon.exterior.simplify(constants.simplification_threshold, False) poly_coords = take_only_valid_linear_rings(poly_coords) if poly_coords.is_empty: continue - #print("node: - is ccw: ", LinearRing(poly_coords).is_ccw) - # if(LinearRing(poly_coords).is_ccw): - # print("Fehler!") - node = AnyNode(id="node", parent=current_poly, - val=poly_coords, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + + node = AnyNode( + id="node", + parent=current_poly, + val=poly_coords, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) active_polys.append(node) hole_node_list = [] for hole in polygon.interiors: hole_node = AnyNode( - id="hole", val=hole, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + id="hole", + val=hole, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) for previous_hole in current_holes: if Polygon(hole).contains(Polygon(previous_hole.val)): previous_hole.parent = hole_node hole_node_list.append(hole_node) active_holes.append(hole_node_list) - for previous_hole in current_holes: # if the previous holes are not contained in the new holes they have been merged with the outer polygon - if previous_hole.parent == None: + for previous_hole in current_holes: + # If the previous holes are not + # contained in the new holes they + # have been merged with the + # outer polygon + if previous_hole.parent is None: previous_hole.parent = current_poly - - #DebuggingMethods.drawPoly(root, 'r-') + # DebuggingMethods.drawPoly(root, 'r-') make_tree_uniform_ccw(root) # print(RenderTree(root)) if strategy == StitchingStrategy.CLOSEST_POINT: - connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( - root, offset, stitch_distance, starting_point, offset_by_half) + ( + connected_line, + connected_line_origin, + ) = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( + root, offset, stitch_distance, starting_point, offset_by_half + ) elif strategy == StitchingStrategy.INNER_TO_OUTER: - connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( - root, offset, stitch_distance, starting_point, offset_by_half) + ( + connected_line, + connected_line_origin, + ) = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( + root, offset, stitch_distance, starting_point, offset_by_half + ) else: - print("Invalid strategy!") - assert(0) + raise ValueError("Invalid stitching stratety!") return connected_line, connected_line_origin diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 71cfd80f..1331ecb2 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -16,7 +16,6 @@ from depq import DEPQ from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM -from ..utils import geometry from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import line_string_to_point_list from .fill import intersect_region_with_grating, intersect_region_with_grating_line, stitch_row @@ -64,11 +63,12 @@ def auto_fill(shape, ending_point=None, underpath=True, offset_by_half=True): - #offset_by_half only relevant for line != None; staggers only relevant for line == None! + # offset_by_half only relevant for line != None; staggers only relevant for line == None! fill_stitch_graph = [] try: - fill_stitch_graph = build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, starting_point, ending_point) + fill_stitch_graph = build_fill_stitch_graph( + shape, line, angle, row_spacing, end_row_spacing, starting_point, ending_point) except ValueError: # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node return fallback(shape, running_stitch_length) @@ -76,10 +76,12 @@ def auto_fill(shape, if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): return fallback(shape, running_stitch_length) - travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) - path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) + travel_graph = build_travel_graph( + fill_stitch_graph, shape, angle, underpath) + path = find_stitch_path( + fill_stitch_graph, travel_graph, starting_point, ending_point) result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, - max_stitch_length, running_stitch_length, staggers, skip_last,line!=None,offset_by_half) + max_stitch_length, running_stitch_length, staggers, skip_last, line is not None, offset_by_half) return result @@ -97,7 +99,8 @@ def which_outline(shape, coords): point = shgeo.Point(*coords) outlines = list(shape.boundary) outline_indices = list(range(len(outlines))) - closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) + closest = min(outline_indices, + key=lambda index: outlines[index].distance(point)) return closest @@ -148,17 +151,18 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st debug.add_layer("auto-fill fill stitch") - if line == None: + if line is None: # Convert the shape into a set of parallel line segments. - rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + rows_of_segments = intersect_region_with_grating( + shape, angle, row_spacing, end_row_spacing) else: - rows_of_segments = intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing) + rows_of_segments = intersect_region_with_grating_line( + shape, line, row_spacing, end_row_spacing) - #segments = [segment for row in rows_of_segments for segment in row] + # segments = [segment for row in rows_of_segments for segment in row] graph = networkx.MultiGraph() - for i in range(len(rows_of_segments)): for segment in rows_of_segments[i]: # First, add the grating segments as edges. We'll use the coordinates @@ -166,16 +170,18 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st # networkx allows us to label nodes with arbitrary data. We'll # mark this one as a grating segment. - #graph.add_edge(*segment, key="segment", underpath_edges=[]) - previous_neighbors_ = [(seg[0],seg[-1]) for seg in rows_of_segments[i-1] if i > 0] - next_neighbors_ = [(seg[0],seg[-1]) for seg in rows_of_segments[(i+1)% len(rows_of_segments)] if i < len(rows_of_segments)-1] + # graph.add_edge(*segment, key="segment", underpath_edges=[]) + previous_neighbors_ = [(seg[0], seg[-1]) + for seg in rows_of_segments[i-1] if i > 0] + next_neighbors_ = [(seg[0], seg[-1]) for seg in rows_of_segments[(i+1) % + len(rows_of_segments)] if i < len(rows_of_segments)-1] - graph.add_edge(segment[0],segment[-1], key="segment", underpath_edges=[], - geometry=shgeo.LineString(segment), previous_neighbors = previous_neighbors_, next_neighbors = next_neighbors_, - projected_points=DEPQ(iterable=None, maxlen=None), already_rastered=False) + graph.add_edge(segment[0], segment[-1], key="segment", underpath_edges=[], + geometry=shgeo.LineString(segment), previous_neighbors=previous_neighbors_, next_neighbors=next_neighbors_, + projected_points=DEPQ(iterable=None, maxlen=None), already_rastered=False) -#fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) +# fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -205,7 +211,8 @@ def insert_node(graph, shape, point): if key == "outline": edges.append(((start, end), data)) - edge, data = min(edges, key=lambda edge_data: shgeo.LineString(edge_data[0]).distance(projected_point)) + edge, data = min(edges, key=lambda edge_data: shgeo.LineString( + edge_data[0]).distance(projected_point)) graph.remove_edge(*edge, key="outline") graph.add_edge(edge[0], node, key="outline", **data) @@ -218,7 +225,8 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): outline_index = which_outline(shape, node) outline_projection = project(shape, node, outline_index) - graph.add_node(node, outline=outline_index, projection=outline_projection) + graph.add_node(node, outline=outline_index, + projection=outline_projection) def add_boundary_travel_nodes(graph, shape): @@ -236,9 +244,11 @@ def add_boundary_travel_nodes(graph, shape): # resolution. A pixel is around a quarter of a millimeter. for i in range(1, int(length)): subpoint = segment.interpolate(i) - graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index) + graph.add_node((subpoint.x, subpoint.y), projection=outline.project( + subpoint), outline=outline_index) - graph.add_node((point.x, point.y), projection=outline.project(point), outline=outline_index) + graph.add_node((point.x, point.y), projection=outline.project( + point), outline=outline_index) prev = point @@ -253,7 +263,8 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False): outline. """ - nodes = list(graph.nodes(data=True)) # returns a list of tuples: [(node, {data}), (node, {data}) ...] + # returns a list of tuples: [(node, {data}), (node, {data}) ...] + nodes = list(graph.nodes(data=True)) nodes.sort(key=lambda node: (node[1]['outline'], node[1]['projection'])) for outline_index, nodes in groupby(nodes, key=lambda node: node[1]['outline']): @@ -318,7 +329,8 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath): graph.add_nodes_from(fill_stitch_graph.nodes(data=True)) if underpath: - boundary_points, travel_edges = build_travel_edges(shape, fill_stitch_angle) + boundary_points, travel_edges = build_travel_edges( + shape, fill_stitch_angle) # This will ensure that a path traveling inside the shape can reach its # target on the outline, which will be one of the points added above. @@ -349,7 +361,7 @@ def get_segments(graph): for start, end, key, data in graph.edges(keys=True, data=True): if key == 'segment': segments.append(data["geometry"]) - #segments.append(shgeo.LineString((start, end))) + # segments.append(shgeo.LineString((start, end))) return segments @@ -371,7 +383,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): # This makes the distance calculations below a bit faster. We're # not looking for high precision anyway. - outline = shape.boundary.simplify(0.5 * PIXELS_PER_MM, preserve_topology=False) + outline = shape.boundary.simplify( + 0.5 * PIXELS_PER_MM, preserve_topology=False) for ls in travel_edges: # In most cases, ls will be a simple line segment. If we're @@ -389,7 +402,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): if segment.crosses(ls): start = segment.coords[0] end = segment.coords[-1] - fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) + fill_stitch_graph[start][end]['segment']['underpath_edges'].append( + edge) # The weight of a travel edge is the length of the line segment. weight = p1.distance(p2) @@ -458,9 +472,12 @@ def build_travel_edges(shape, fill_angle): else: scale = 1.0 - grating1 = travel_grating(shape, fill_angle + math.pi / 4, scale * 2 * PIXELS_PER_MM) - grating2 = travel_grating(shape, fill_angle - math.pi / 4, scale * 2 * PIXELS_PER_MM) - grating3 = travel_grating(shape, fill_angle - math.pi / 2, scale * math.sqrt(2) * PIXELS_PER_MM) + grating1 = travel_grating( + shape, fill_angle + math.pi / 4, scale * 2 * PIXELS_PER_MM) + grating2 = travel_grating( + shape, fill_angle - math.pi / 4, scale * 2 * PIXELS_PER_MM) + grating3 = travel_grating( + shape, fill_angle - math.pi / 2, scale * math.sqrt(2) * PIXELS_PER_MM) debug.add_layer("auto-fill travel") debug.log_line_strings(grating1, "grating1") @@ -471,10 +488,12 @@ def build_travel_edges(shape, fill_angle): for ls in mls for coord in ls.coords] - diagonal_edges = ensure_multi_line_string(grating1.symmetric_difference(grating2)) + diagonal_edges = ensure_multi_line_string( + grating1.symmetric_difference(grating2)) # without this, floating point inaccuracies prevent the intersection points from lining up perfectly. - vertical_edges = ensure_multi_line_string(snap(grating3.difference(grating1), diagonal_edges, 0.005)) + vertical_edges = ensure_multi_line_string( + snap(grating3.difference(grating1), diagonal_edges, 0.005)) return endpoints, chain(diagonal_edges, vertical_edges) @@ -536,7 +555,8 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None last_vertex, last_key = current_vertex, current_key vertex_stack.pop() else: - ignore, next_vertex, next_key = pick_edge(graph.edges(current_vertex, keys=True)) + ignore, next_vertex, next_key = pick_edge( + graph.edges(current_vertex, keys=True)) vertex_stack.append((next_vertex, next_key)) graph.remove_edge(current_vertex, next_vertex, next_key) @@ -565,7 +585,8 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None # relevant in the case that the user specifies an underlay with an inset # value, because the starting point (and possibly ending point) can be # inside the shape. - outline_nodes = [node for node, outline in travel_graph.nodes(data="outline") if outline is not None] + outline_nodes = [node for node, outline in travel_graph.nodes( + data="outline") if outline is not None] real_end = nearest_node(outline_nodes, ending_point) path.append(PathEdge((ending_node, real_end), key="outline")) @@ -639,28 +660,31 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): # stitch. return stitches[1:] -def stitch_line(stitches, stitching_direction, geometry,projected_points, max_stitch_length,row_spacing,skip_last,offset_by_half): - #print(start_point) - #print(geometry[0]) - #if stitching_direction == -1: - # geometry.coords = geometry.coords[::-1] - stitched_line, stitched_line_origin = raster_line_string_with_priority_points_graph(geometry,max_stitch_length,stitching_direction,projected_points,abs(row_spacing),offset_by_half) +def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): + # print(start_point) + # print(geometry[0]) + # if stitching_direction == -1: + # geometry.coords = geometry.coords[::-1] + stitched_line, stitched_line_origin = raster_line_string_with_priority_points_graph( + geometry, max_stitch_length, stitching_direction, projected_points, abs(row_spacing), offset_by_half) stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) - for i in range(1,len(stitched_line)): + for i in range(1, len(stitched_line)): stitches.append(Stitch(*stitched_line[i], tags=('fill_row'))) - + if not skip_last: - if stitching_direction==1: - stitches.append(Stitch(*geometry.coords[-1], tags=('fill_row_end',))) + if stitching_direction == 1: + stitches.append( + Stitch(*geometry.coords[-1], tags=('fill_row_end',))) else: - stitches.append(Stitch(*geometry.coords[0], tags=('fill_row_end',))) + stitches.append( + Stitch(*geometry.coords[0], tags=('fill_row_end',))) @debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, - running_stitch_length, staggers, skip_last, offsetted_line, offset_by_half): +def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, + running_stitch_length, staggers, skip_last, offsetted_line, offset_by_half): path = collapse_sequential_outline_edges(path) stitches = [] @@ -678,18 +702,24 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, projected_points = current_edge['projected_points'] stitching_direction = 1 if (abs(edge[0][0]-path_geometry.coords[0][0])+abs(edge[0][1]-path_geometry.coords[0][1]) > - abs(edge[0][0]-path_geometry.coords[-1][0])+abs(edge[0][1]-path_geometry.coords[-1][1])): + abs(edge[0][0]-path_geometry.coords[-1][0])+abs(edge[0][1]-path_geometry.coords[-1][1])): stitching_direction = -1 - stitch_line(new_stitches, stitching_direction, path_geometry,projected_points, max_stitch_length,row_spacing,skip_last,offset_by_half) + stitch_line(new_stitches, stitching_direction, path_geometry, projected_points, + max_stitch_length, row_spacing, skip_last, offset_by_half) current_edge['already_rastered'] = True - transfer_points_to_surrounding_graph(fill_stitch_graph,current_edge,row_spacing,False,new_stitches,overnext_neighbor=True) - transfer_points_to_surrounding_graph(fill_stitch_graph,current_edge,row_spacing,offset_by_half,new_stitches,overnext_neighbor=False,transfer_forbidden_points=offset_by_half) + transfer_points_to_surrounding_graph( + fill_stitch_graph, current_edge, row_spacing, False, new_stitches, overnext_neighbor=True) + transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, row_spacing, offset_by_half, + new_stitches, overnext_neighbor=False, transfer_forbidden_points=offset_by_half) stitches.extend(new_stitches) else: - stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) - travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) + stitch_row(stitches, edge[0], edge[1], angle, + row_spacing, max_stitch_length, staggers, skip_last) + travel_graph.remove_edges_from( + fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) + stitches.extend( + travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) return stitches diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py index 63746310..162c4cfb 100644 --- a/lib/stitches/constants.py +++ b/lib/stitches/constants.py @@ -3,39 +3,60 @@ import math # Used in the simplify routine of shapely simplification_threshold = 0.01 -# If a transferred point is closer than this value to one of its neighbors, it will be checked whether it can be removed +# If a transferred point is closer than this value to one of its neighbors, +# it will be checked whether it can be removed distance_thresh_remove_transferred_point = 0.15 # If a line segment is shorter than this threshold it is handled as a single point line_lengh_seen_as_one_point = 0.05 -# E.g. to check whether a point is already present in a point list, the point is allowed to be this value in distance apart +# E.g. to check whether a point is already present in a point list, +# the point is allowed to be this value in distance apart point_spacing_to_be_considered_equal = 0.05 -# Adjacent geometry should have points closer than offset*offset_factor_for_adjacent_geometry to be considered adjacent +# Adjacent geometry should have points closer than +# offset*offset_factor_for_adjacent_geometry to be considered adjacent offset_factor_for_adjacent_geometry = 1.5 -# Transfer point distance is used for projecting points from already rastered geometry to adjacent geometry -# (max spacing transfer_point_distance_factor*offset) to get a more regular pattern +# Transfer point distance is used for projecting points from already +# rastered geometry to adjacent geometry +# (max spacing transfer_point_distance_factor*offset) +# to get a more regular pattern transfer_point_distance_factor = 1.5 # Used to handle numerical inaccuracies during comparisons -eps = 1E-3 +eps = 1e-3 -factor_offset_starting_points=0.5 #When entering and leaving a child from a parent we introduce an offset of abs_offset*factor_offset_starting_points so - #that entering and leaving points are not lying above each other. +# When entering and leaving a child from a parent we introduce an offset of +# abs_offset*factor_offset_starting_points +# so that entering and leaving points are not lying above each other. +factor_offset_starting_points = 0.5 -factor_offset_remove_points=0.5 #if points are closer than abs_offset*factor_offset_remove_points one of it is removed +# if points are closer than abs_offset*factor_offset_remove_points one of it is removed +factor_offset_remove_points = 0.5 -fac_offset_edge_shift = 0.25 #if an unshifted relevant edge is closer than abs_offset*fac_offset_edge_shift to the line segment created by the shifted edge, - #the shift is allowed - otherwise the edge must not be shifted. +# if an unshifted relevant edge is closer than +# abs_offset*fac_offset_edge_shift +# to the line segment created by the shifted edge, +# the shift is allowed - otherwise the edge must not be shifted. +fac_offset_edge_shift = 0.25 -limiting_angle = math.pi*15/180.0 #decides whether the point belongs to a hard edge (must use this point during sampling) or soft edge (do not necessarily need to use this point) -limiting_angle_straight = math.pi*0.5/180.0 #angles straighter (smaller) than this are considered as more or less straight (no concrete edges required for path segments having only angles <= this value) +# decides whether the point belongs to a hard edge (must use this point during sampling) +# or soft edge (do not necessarily need to use this point) +limiting_angle = math.pi * 15 / 180.0 +# angles straighter (smaller) than this are considered as more or less straight +# (no concrete edges required for path segments having only angles <= this value) +limiting_angle_straight = math.pi * 0.5 / 180.0 -factor_offset_remove_dense_points=0.2 #if a point distance to the connected line of its two neighbors is smaller than abs_offset times this factor, this point will be removed if the stitching distance will not be exceeded +# if a point distance to the connected line of its two neighbors is smaller than +# abs_offset times this factor, this point will be removed if the stitching distance will not be exceeded +factor_offset_remove_dense_points = 0.2 -factor_offset_forbidden_point = 1.0 #if a soft edge is closer to a forbidden point than abs_offset*this factor it will be marked as forbidden. +# if a soft edge is closer to a forbidden point than abs_offset*this factor it will be marked as forbidden. +factor_offset_forbidden_point = 1.0 -factor_segment_length_direct_preferred_over_overnext = 0.5 #usually overnext projected points are preferred. If an overnext projected point would create a much smaller segment than a direct projected point we might prefer the direct projected point +# usually overnext projected points are preferred. +# If an overnext projected point would create a much smaller segment than a direct +# projected point we might prefer the direct projected point +factor_segment_length_direct_preferred_over_overnext = 0.5 diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 4e1669e9..9a7254e2 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -12,8 +12,10 @@ from ..utils import Point as InkstitchPoint from ..utils import cache from ..stitch_plan import Stitch + def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): - rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip) + rows_of_segments = intersect_region_with_grating( + shape, angle, row_spacing, end_row_spacing, flip) groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing) return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last) @@ -73,7 +75,8 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge stitches.append(beg) - first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers) + first_stitch = adjust_stagger( + beg, angle, row_spacing, max_stitch_length, staggers) # we might have chosen our first stitch just outside this row, so move back in if (first_stitch - beg) * row_direction < 0: @@ -82,13 +85,15 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge offset = (first_stitch - beg).length() while offset < segment_length: - stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row'))) + stitches.append( + Stitch(beg + offset * row_direction, tags=('fill_row'))) offset += max_stitch_length if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM and not skip_last: stitches.append(end) -def extend_line(line, minx,maxx,miny,maxy): + +def extend_line(line, minx, maxx, miny, maxy): line = line.simplify(0.01, False) upper_left = InkstitchPoint(minx, miny) @@ -103,26 +108,30 @@ def extend_line(line, minx,maxx,miny,maxy): point4 = InkstitchPoint(*line.coords[-1]) new_ending_point = point4+(point4-point3).unit()*length - line = LineString([new_starting_point.as_tuple()]+line.coords[1:-1]+[new_ending_point.as_tuple()]) + line = LineString([new_starting_point.as_tuple()] + + line.coords[1:-1]+[new_ending_point.as_tuple()]) def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): - + row_spacing = abs(row_spacing) (minx, miny, maxx, maxy) = shape.bounds upper_left = InkstitchPoint(minx, miny) rows = [] - extend_line(line, minx,maxx,miny,maxy) #extend the line towards the ends to increase probability that all offsetted curves cross the shape + # extend the line towards the ends to increase probability that all offsetted curves cross the shape + extend_line(line, minx, maxx, miny, maxy) line_offsetted = line res = line_offsetted.intersection(shape) while isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)) or (not res.is_empty and len(res.coords) > 1): if isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): - runs = [line_string.coords for line_string in res.geoms if (not line_string.is_empty and len(line_string.coords) > 1)] + runs = [line_string.coords for line_string in res.geoms if ( + not line_string.is_empty and len(line_string.coords) > 1)] else: runs = [res.coords] - runs.sort(key=lambda seg: (InkstitchPoint(*seg[0]) - upper_left).length()) + runs.sort(key=lambda seg: ( + InkstitchPoint(*seg[0]) - upper_left).length()) if flip: runs.reverse() runs = [tuple(reversed(run)) for run in runs] @@ -130,8 +139,8 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0: rows.append(runs) else: - rows.insert(0,runs) - line_offsetted = line_offsetted.parallel_offset(row_spacing,'left',5) + rows.insert(0, runs) + line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) if row_spacing < 0: line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) @@ -139,12 +148,13 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0 and not isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): if (res.is_empty or len(res.coords) == 1): row_spacing = -row_spacing - #print("Set to right") - line_offsetted = line.parallel_offset(row_spacing,'left',5) - line_offsetted.coords = line_offsetted.coords[::-1] #using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this + # print("Set to right") + line_offsetted = line.parallel_offset(row_spacing, 'left', 5) + # using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this + line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - + return rows @@ -174,7 +184,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non # angle degrees clockwise and ask for the new bounding box. The max # and min y tell me how far to go. - _, start, _, end = shapely.affinity.rotate(shape, angle, origin='center', use_radians=True).bounds + _, start, _, end = shapely.affinity.rotate( + shape, angle, origin='center', use_radians=True).bounds # convert start and end to be relative to center (simplifies things later) start -= center.y @@ -211,7 +222,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non runs = [res.coords] if runs: - runs.sort(key=lambda seg: (InkstitchPoint(*seg[0]) - upper_left).length()) + runs.sort(key=lambda seg: ( + InkstitchPoint(*seg[0]) - upper_left).length()) if flip: runs.reverse() @@ -220,7 +232,9 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non rows.append(runs) if end_row_spacing: - current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height) + current_row_y += row_spacing + \ + (end_row_spacing - row_spacing) * \ + ((current_row_y - start) / height) else: current_row_y += row_spacing @@ -237,7 +251,8 @@ def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length if (swap): (beg, end) = (end, beg) - stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last) + stitch_row(stitches, beg, end, angle, row_spacing, + max_stitch_length, staggers, skip_last) swap = not swap -- cgit v1.3.1 From 8d19cdc59d00836fdf3ef037f31e7130771f84d2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 28 Jan 2022 21:54:20 +0100 Subject: adjusted selection_to_guide_line to match new marker creation way --- lib/extensions/selection_to_guide_line.py | 55 ++++--------------------------- lib/marker.py | 2 +- symbols/marker.svg | 17 ++++++++++ 3 files changed, 24 insertions(+), 50 deletions(-) (limited to 'lib/extensions') diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py index e11cdb4e..a0d32601 100644 --- a/lib/extensions/selection_to_guide_line.py +++ b/lib/extensions/selection_to_guide_line.py @@ -4,10 +4,10 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. import inkex -from lxml import etree from ..i18n import _ -from ..svg.tags import SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_DEFS_TAG +from ..marker import set_marker +from ..svg.tags import EMBROIDERABLE_TAGS from .base import InkstitchExtension @@ -18,52 +18,9 @@ class SelectionToGuideLine(InkstitchExtension): return if not self.svg.selected: - inkex.errormsg( - _("Please select one object to be marked as a guide line.")) + inkex.errormsg(_("Please select at least one object to be marked as a guide line.")) return - if len(self.get_nodes()) != 1: - inkex.errormsg( - _("Please select only one object to be marked as a guide line.")) - return - - for guide_line in self.get_nodes(): - if guide_line.tag in (SVG_PATH_TAG, SVG_POLYLINE_TAG): - self.set_marker(guide_line) - - def set_marker(self, node): - xpath = ".//marker[@id='inkstitch-guide-line-marker']" - guide_line_marker = self.document.xpath(xpath) - - if not guide_line_marker: - # get or create def element - defs = self.document.find(SVG_DEFS_TAG) - if defs is None: - defs = etree.SubElement(self.document, SVG_DEFS_TAG) - - # insert marker - marker = """ - - - - - """ # noqa: E501 - defs.append(etree.fromstring(marker)) - - # attach marker to node - style = node.get('style') or '' - style = style.split(";") - style = [i for i in style if not i.startswith('marker-start')] - style.append('marker-start:url(#inkstitch-guide-line-marker)') - node.set('style', ";".join(style)) + for pattern in self.get_nodes(): + if pattern.tag in EMBROIDERABLE_TAGS: + set_marker(pattern, 'start', 'guide-line') diff --git a/lib/marker.py b/lib/marker.py index 4f262abe..1d9145e7 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -33,5 +33,5 @@ def set_marker(node, position, marker): style = node.get('style') or '' style = style.split(";") style = [i for i in style if not i.startswith('marker-%s' % position)] - style.append('marker-%s:url(#inkstitch-pattern-marker)' % position) + style.append('marker-%s:url(#inkstitch-%s-marker)' % (position, marker)) node.set('style', ";".join(style)) diff --git a/symbols/marker.svg b/symbols/marker.svg index b32327bc..28f4f44d 100644 --- a/symbols/marker.svg +++ b/symbols/marker.svg @@ -33,5 +33,22 @@ d="M 4.9673651,5.7245662 C 4.7549848,5.7646159 4.6247356,5.522384 4.6430021,5.3419847 4.6765851,5.0103151 5.036231,4.835347 5.3381858,4.8987426 5.7863901,4.9928495 6.0126802,5.4853625 5.9002872,5.9065088 5.7495249,6.4714237 5.1195537,6.7504036 4.5799191,6.5874894 3.898118,6.3816539 3.5659013,5.6122905 3.7800789,4.9545192 4.0402258,4.1556558 4.9498996,3.7699484 5.7256318,4.035839 6.6416744,4.3498087 7.0810483,5.4003986 6.7631909,6.2939744 6.395633,7.3272552 5.2038143,7.8204128 4.1924535,7.4503931 3.0418762,7.0294421 2.4948761,5.6961604 2.9171752,4.567073 3.3914021,3.2991406 4.8663228,2.6982592 6.1130974,3.1729158 7.4983851,3.7003207 8.1531869,5.3169977 7.6260947,6.6814205 7.0456093,8.1841025 5.2870784,8.8928844 3.8050073,8.3132966 2.1849115,7.6797506 1.4221671,5.7793073 2.0542715,4.1796074 2.7408201,2.4420977 4.7832541,1.6253548 6.5005435,2.310012 8.3554869,3.0495434 9.2262638,5.2339874 8.4890181,7.0688861 8.4256397,7.2266036 8.3515789,7.379984 8.2675333,7.5277183" /> + + + + + + -- cgit v1.3.1 From 82216b184c669d6dea26672e5c0771146e62ca39 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sat, 29 Jan 2022 09:53:50 +0100 Subject: remove some pattern and marker mixups and some style issues --- lib/elements/auto_fill.py | 29 +++++++++---------- lib/elements/utils.py | 4 +-- lib/extensions/base.py | 6 ++-- lib/extensions/params.py | 2 +- lib/marker.py | 41 ++++++++++++++++++++++++++- lib/patterns.py | 50 ++++----------------------------- lib/stitches/ConnectAndSamplePattern.py | 6 ++-- lib/stitches/LineStringSampling.py | 2 +- lib/stitches/PointTransfer.py | 4 +-- lib/stitches/StitchPattern.py | 3 +- lib/stitches/fill.py | 2 +- 11 files changed, 72 insertions(+), 77 deletions(-) (limited to 'lib/extensions') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 3f75180b..614e6887 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -3,27 +3,26 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +import logging import math +import re import sys import traceback -import re -import logging -import inkex +import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity + from ..i18n import _ +from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import auto_fill, fill -from ..stitches import StitchPattern -from ..utils import cache, version -from .element import param -from .element import EmbroideryElement -from ..patterns import get_patterns -from .validation import ValidationWarning -from ..utils import Point as InkstitchPoint +from ..stitches import StitchPattern, auto_fill, fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationWarning class SmallShapeWarning(ValidationWarning): @@ -393,7 +392,8 @@ class AutoFill(EmbroideryElement): else: return None - def to_stitch_groups(self, last_patch): + def to_stitch_groups(self, last_patch): # noqa: C901 + # TODO: split this up do_legacy_fill() etc. stitch_groups = [] starting_point = self.get_starting_point(last_patch) @@ -458,9 +458,8 @@ class AutoFill(EmbroideryElement): stitches=path) stitch_groups.append(stitch_group) elif self.fill_method == 2: # Guided Auto Fill - lines = get_patterns( - self.node, "#inkstitch-guide-line-marker", False, True) - lines = lines['stroke_patterns'] + lines = get_marker_elements(self.node, "guide-line", False, True) + lines = lines['stroke'] if not lines or lines[0].is_empty: inkex.errormsg( _("No line marked as guide line found within the same group as patch")) diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9fec8b63..9b9b8f14 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -4,7 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. from ..commands import is_command -from ..patterns import is_pattern +from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) from .auto_fill import AutoFill @@ -29,7 +29,7 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif is_pattern(node): + elif has_marker(node, 'pattern'): return [PatternObject(node)] elif node.tag in EMBROIDERABLE_TAGS: diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 56385458..cf846324 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -16,7 +16,7 @@ from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone from ..i18n import _ -from ..patterns import is_pattern +from ..marker import has_marker from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) @@ -161,10 +161,10 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not is_pattern(node): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node, 'pattern'): nodes.append(node) # add images, text and patterns for the troubleshoot extension - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_pattern(node)): + elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node, 'pattern')): nodes.append(node) return nodes diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 30f6ba1d..55963625 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -296,7 +296,7 @@ class ParamsTab(ScrolledPanel): widgets[3].Show(True) choice["last_initialized_choice"] = current_selection - def __do_layout(self, only_settings_grid=False): + def __do_layout(self, only_settings_grid=False): # noqa: C901 # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) diff --git a/lib/marker.py b/lib/marker.py index 1d9145e7..3c145145 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -7,10 +7,12 @@ from copy import deepcopy from os import path import inkex +from shapely import geometry as shgeo +from .svg.tags import EMBROIDERABLE_TAGS from .utils import cache, get_bundled_dir -MARKER = ['pattern'] +MARKER = ['pattern', 'guide-line'] def ensure_marker(svg, marker): @@ -35,3 +37,40 @@ def set_marker(node, position, marker): style = [i for i in style if not i.startswith('marker-%s' % position)] style.append('marker-%s:url(#inkstitch-%s-marker)' % (position, marker)) node.set('style', ";".join(style)) + + +def get_marker_elements(node, marker, get_fills=True, get_strokes=True): + from .elements import EmbroideryElement + from .elements.stroke import Stroke + + fills = [] + strokes = [] + xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-%s-marker)')]" % marker + markers = node.xpath(xpath, namespaces=inkex.NSS) + for marker in markers: + if marker.tag not in EMBROIDERABLE_TAGS: + continue + + element = EmbroideryElement(marker) + fill = element.get_style('fill') + stroke = element.get_style('stroke') + + if get_fills and fill is not None: + fill = Stroke(marker).paths + linear_rings = [shgeo.LinearRing(path) for path in fill] + for ring in linear_rings: + fills.append(shgeo.Polygon(ring)) + + if get_strokes and stroke is not None: + stroke = Stroke(marker).paths + line_strings = [shgeo.LineString(path) for path in stroke] + strokes.append(shgeo.MultiLineString(line_strings)) + + return {'fill': fills, 'stroke': strokes} + + +def has_marker(node, marker): + if node.tag not in EMBROIDERABLE_TAGS: + return False + style = node.get('style') or '' + return "marker-start:url(#inkstitch-%s-marker)" % marker in style diff --git a/lib/patterns.py b/lib/patterns.py index 7ec4d082..1650523c 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -3,25 +3,17 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import inkex from shapely import geometry as shgeo +from .marker import get_marker_elements from .stitch_plan import Stitch -from .svg.tags import EMBROIDERABLE_TAGS from .utils import Point -def is_pattern(node): - if node.tag not in EMBROIDERABLE_TAGS: - return False - style = node.get('style') or '' - return "marker-start:url(#inkstitch-pattern-marker)" in style - - def apply_patterns(patches, node): - patterns = get_patterns(node, "#inkstitch-pattern-marker") - _apply_fill_patterns(patterns['fill_patterns'], patches) - _apply_stroke_patterns(patterns['stroke_patterns'], patches) + patterns = get_marker_elements(node, "pattern") + _apply_fill_patterns(patterns['fill'], patches) + _apply_stroke_patterns(patterns['stroke'], patches) def _apply_stroke_patterns(patterns, patches): @@ -32,8 +24,7 @@ def _apply_stroke_patterns(patterns, patches): patch_points.append(stitch) if i == len(patch.stitches) - 1: continue - intersection_points = _get_pattern_points( - stitch, patch.stitches[i+1], pattern) + intersection_points = _get_pattern_points(stitch, patch.stitches[i+1], pattern) for point in intersection_points: patch_points.append(Stitch(point, tags=('pattern_point',))) patch.stitches = patch_points @@ -65,37 +56,6 @@ def _apply_fill_patterns(patterns, patches): patch.stitches = patch_points -def get_patterns(node, marker_id, get_fills=True, get_strokes=True): - from .elements import EmbroideryElement - from .elements.auto_fill import auto_fill - from .elements.stroke import Stroke - - fills = [] - strokes = [] - xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url("+marker_id+")')]" - patterns = node.xpath(xpath, namespaces=inkex.NSS) - for pattern in patterns: - if pattern.tag not in EMBROIDERABLE_TAGS: - continue - - element = EmbroideryElement(pattern) - fill = element.get_style('fill') - stroke = element.get_style('stroke') - - if fill is not None: - fill_pattern = Stroke(pattern).paths - linear_rings = [shgeo.LinearRing(path) for path in fill_pattern] - for ring in linear_rings: - fills.append(shgeo.Polygon(ring)) - - if get_strokes and stroke is not None: - stroke_pattern = Stroke(pattern).paths - line_strings = [shgeo.LineString(path) for path in stroke_pattern] - strokes.append(shgeo.MultiLineString(line_strings)) - - return {'fill_patterns': fills, 'stroke_patterns': strokes} - - def _get_pattern_points(first, second, pattern): points = [] intersection = shgeo.LineString([first, second]).intersection(pattern) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 33a1ba6d..1cf2b2a1 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -7,7 +7,6 @@ import trimesh import numpy as np from scipy import spatial import math -from shapely.geometry import asLineString from anytree import PreOrderIter from ..stitches import LineStringSampling from ..stitches import PointTransfer @@ -52,7 +51,7 @@ def cut(line, distance): return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) -def connect_raster_tree_nearest_neighbor( +def connect_raster_tree_nearest_neighbor( # noqa: C901 tree, used_offset, stitch_distance, close_point, offset_by_half): """ Takes the offsetted curves organized as tree, connects and samples them. @@ -458,8 +457,7 @@ def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distan return line_segment.coords[1] -def connect_raster_tree_from_inner_to_outer( - tree, used_offset, stitch_distance, close_point, offset_by_half): +def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them. Strategy: A connection from parent to child is made as fast as possible to diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index 43f650e6..71660e2d 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -70,7 +70,7 @@ def calculate_line_angles(line): return Angles -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): """ Rasters a line between start_distance and end_distance. diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index b6e4e026..93fe02c5 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -36,7 +36,7 @@ def calc_transferred_point(bisectorline, child): return point, priority -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): """ @@ -305,7 +305,7 @@ def calc_transferred_point_graph(bisectorline, edge_geometry): return point, priority -def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, +def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): """ Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index 62ef2b0f..4a38c0bc 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -159,8 +159,7 @@ def check_and_prepare_tree_for_valid_spiral(root): return True -def offset_poly( - poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): +def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 5afcb228..ceac56d9 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -145,7 +145,7 @@ def repair_non_simple_lines(line): return repaired -def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): +def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): # noqa: C901 row_spacing = abs(row_spacing) (minx, miny, maxx, maxy) = shape.bounds -- cgit v1.3.1 From 3d1600ed039c9078bcb4a28328ab60eb96994dfd Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sun, 30 Jan 2022 15:48:51 +0100 Subject: * autofill to fillstitch * remove too complex warning for fillstitch * some marker adjustments --- lib/elements/__init__.py | 3 +- lib/elements/auto_fill.py | 533 ------------------------------------- lib/elements/clone.py | 38 +-- lib/elements/element.py | 12 +- lib/elements/fill_stitch.py | 624 ++++++++++++++++++++++++++++++++++++++++++++ lib/elements/marker.py | 32 +++ lib/elements/pattern.py | 33 --- lib/elements/utils.py | 13 +- lib/extensions/base.py | 6 +- lib/extensions/cleanup.py | 4 +- lib/extensions/params.py | 5 +- lib/marker.py | 13 +- symbols/marker.svg | 32 +-- 13 files changed, 713 insertions(+), 635 deletions(-) delete mode 100644 lib/elements/auto_fill.py create mode 100644 lib/elements/fill_stitch.py create mode 100644 lib/elements/marker.py delete mode 100644 lib/elements/pattern.py (limited to 'lib/extensions') diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index bb5c95ba..00933f36 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.py @@ -3,11 +3,10 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from .auto_fill import AutoFill from .clone import Clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill +from .fill_stitch import FillStitch from .image import ImageObject from .polyline import Polyline from .satin_column import SatinColumn diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py deleted file mode 100644 index 614e6887..00000000 --- a/lib/elements/auto_fill.py +++ /dev/null @@ -1,533 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import logging -import math -import re -import sys -import traceback - -import inkex -from shapely import geometry as shgeo -from shapely.validation import explain_validity - -from ..i18n import _ -from ..marker import get_marker_elements -from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, fill -from ..svg import PIXELS_PER_MM -from ..svg.tags import INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint -from ..utils import cache, version -from .element import EmbroideryElement, param -from .validation import ValidationWarning - - -class SmallShapeWarning(ValidationWarning): - name = _("Small Fill") - description = _("This fill object is so small that it would probably look better as running stitch or satin column. " - "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " - "the outline instead.") - - -class ExpandWarning(ValidationWarning): - name = _("Expand") - description = _("The expand parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use original size instead.") - - -class UnderlayInsetWarning(ValidationWarning): - name = _("Inset") - description = _("The underlay inset parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use the original size instead.") - - -class AutoFill(EmbroideryElement): - element_name = _("AutoFill") - - @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) - def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) - def fill_method(self): - return self.get_int_param('fill_method', 0) - - @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) - def tangential_strategy(self): - return self.get_int_param('tangential_strategy', 1) - - @property - @param('join_style', _('Join Style'), type='dropdown', default=0, - options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) - def join_style(self): - return self.get_int_param('join_style', 0) - - @property - @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) - def interlaced(self): - return self.get_boolean_param('interlaced', True) - - @property - @param('angle', - _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), - unit='deg', - type='float', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - # SVG spec says the default fill is black - return self.get_style("fill", "#000000") - - @property - @param( - 'skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def skip_last(self): - return self.get_boolean_param("skip_last", False) - - @property - @param( - 'flip', - _('Flip fill (start right-to-left)'), - tooltip=_('The flip option can help you with routing your stitch path. ' - 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', - _('Spacing between rows'), - tooltip=_('Distance between rows of stitches.'), - unit='mm', - sort_index=4, - type='float', - default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', - _('Maximum fill stitch length'), - tooltip=_( - 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), - unit='mm', - sort_index=4, - type='float', - default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - - @property - @param('staggers', - _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), - type='int', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=4) - def staggers(self): - return max(self.get_int_param("staggers", 4), 1) - - @property - @cache - def paths(self): - paths = self.flatten(self.parse_path()) - # ensure path length - for i, path in enumerate(paths): - if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0] + - 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] - return paths - - @property - @cache - def outline(self): - return self.shape.boundary[0] - - @property - @cache - def outline_length(self): - return self.outline.length - - @property - @param('running_stitch_length_mm', - _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), - unit='mm', - type='float', - default=1.5, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=4) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) - def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=True) - - @property - @param('fill_underlay_angle', - _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), - unit='deg', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_angle(self): - underlay_angles = self.get_param('fill_underlay_angle', None) - default_value = [self.angle + math.pi / 2.0] - if underlay_angles is not None: - underlay_angles = underlay_angles.strip().split(',') - try: - underlay_angles = [math.radians( - float(angle)) for angle in underlay_angles] - except (TypeError, ValueError): - return default_value - else: - underlay_angles = default_value - - return underlay_angles - - @property - @param('fill_underlay_row_spacing_mm', - _('Row spacing'), - tooltip=_('default: 3x fill row spacing'), - unit='mm', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_row_spacing(self): - return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 - - @property - @param('fill_underlay_max_stitch_length_mm', - _('Max stitch length'), - tooltip=_('default: equal to fill max stitch length'), - unit='mm', - group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_max_stitch_length(self): - return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length - - @property - @param('fill_underlay_inset_mm', - _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), - unit='mm', - group=_('AutoFill Underlay'), - type='float', - default=0) - def fill_underlay_inset(self): - return self.get_float_param('fill_underlay_inset_mm', 0) - - @property - @param( - 'fill_underlay_skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - group=_('AutoFill Underlay'), - type='boolean', - default=False) - def fill_underlay_skip_last(self): - return self.get_boolean_param("fill_underlay_skip_last", False) - - @property - @param('expand_mm', - _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), - unit='mm', - type='float', - default=0, - sort_index=5, - select_items=[('fill_method', 0), ('fill_method', 2)]) - def expand(self): - return self.get_float_param('expand_mm', 0) - - @property - @param('underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - type='boolean', - default=True, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=6) - def underpath(self): - return self.get_boolean_param('underpath', True) - - @property - @param( - 'underlay_underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - group=_('AutoFill Underlay'), - type='boolean', - default=True) - def underlay_underpath(self): - return self.get_boolean_param('underlay_underpath', True) - - @property - @cache - def shape(self): - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon( - point_list).area, reverse=True) - # Very small holes will cause a shape to be rendered as an outline only - # they are too small to be rendered and only confuse the auto_fill algorithm. - # So let's ignore them - if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: - paths = [path for path in paths if shgeo.Polygon(path).area > 3] - - polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) - - # There is a great number of "crossing border" errors on fill shapes - # If the polygon fails, we can try to run buffer(0) on the polygon in the - # hope it will fix at least some of them - if not self.shape_is_valid(polygon): - why = explain_validity(polygon) - message = re.match(r".+?(?=\[)", why) - if message.group(0) == "Self-intersection": - buffered = polygon.buffer(0) - # we do not want to break apart into multiple objects (possibly in the future?!) - # best way to distinguish the resulting polygon is to compare the area size of the two - # and make sure users will not experience significantly altered shapes without a warning - if math.isclose(polygon.area, buffered.area): - polygon = shgeo.MultiPolygon([buffered]) - - return polygon - - def shape_is_valid(self, shape): - # Shapely will log to stdout to complain about the shape unless we make - # it shut up. - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid = shape.is_valid - - logger.setLevel(level) - - return valid - - def shrink_or_grow_shape(self, amount, validate=False): - if amount: - shape = self.shape.buffer(amount) - # changing the size can empty the shape - # in this case we want to use the original shape rather than returning an error - if shape.is_empty and not validate: - return self.shape - if not isinstance(shape, shgeo.MultiPolygon): - shape = shgeo.MultiPolygon([shape]) - return shape - else: - return self.shape - - @property - def underlay_shape(self): - return self.shrink_or_grow_shape(-self.fill_underlay_inset) - - @property - def fill_shape(self): - return self.shrink_or_grow_shape(self.expand) - - def get_starting_point(self, last_patch): - # If there is a "fill_start" Command, then use that; otherwise pick - # the point closest to the end of the last patch. - - if self.get_command('fill_start'): - return self.get_command('fill_start').target_point - elif last_patch: - return last_patch.stitches[-1] - else: - return None - - def get_ending_point(self): - if self.get_command('fill_end'): - return self.get_command('fill_end').target_point - else: - return None - - def to_stitch_groups(self, last_patch): # noqa: C901 - # TODO: split this up do_legacy_fill() etc. - stitch_groups = [] - - starting_point = self.get_starting_point(last_patch) - ending_point = self.get_ending_point() - - try: - if self.fill_underlay: - for i in range(len(self.fill_underlay_angle)): - underlay = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_underlay"), - stitches=auto_fill( - self.underlay_shape, - None, - self.fill_underlay_angle[i], - self.fill_underlay_row_spacing, - self.fill_underlay_row_spacing, - self.fill_underlay_max_stitch_length, - self.running_stitch_length, - self.staggers, - self.fill_underlay_skip_last, - starting_point, - underpath=self.underlay_underpath)) - stitch_groups.append(underlay) - starting_point = underlay.stitches[-1] - - if self.fill_method == 0: # Auto Fill - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - None, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - self.skip_last, - starting_point, - ending_point, - self.underpath)) - stitch_groups.append(stitch_group) - elif self.fill_method == 1: # Tangential Fill - polygons = list(self.fill_shape) - if not starting_point: - starting_point = (0, 0) - for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, - self.interlaced, - self.tangential_strategy, - shgeo.Point(starting_point)) - path = [InkstitchPoint(*p) for p in connectedLine] - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) - stitch_groups.append(stitch_group) - elif self.fill_method == 2: # Guided Auto Fill - lines = get_marker_elements(self.node, "guide-line", False, True) - lines = lines['stroke'] - if not lines or lines[0].is_empty: - inkex.errormsg( - _("No line marked as guide line found within the same group as patch")) - else: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - lines[0].geoms[0], - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - 0, - self.skip_last, - starting_point, - ending_point, - self.underpath, - self.interlaced)) - stitch_groups.append(stitch_group) - elif self.fill_method == 3: # Legacy Fill - stitch_lists = fill.legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers, - self.skip_last) - for stitch_list in stitch_lists: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=stitch_list) - stitch_groups.append(stitch_group) - - except Exception: - if hasattr(sys, 'gettrace') and sys.gettrace(): - # if we're debugging, let the exception bubble up - raise - - # for an uncaught exception, give a little more info so that they can create a bug report - message = "" - message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") - message += "\n\n" - # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new - message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") - message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" - message += version.get_inkstitch_version() + "\n\n" - message += traceback.format_exc() - - self.fatal(message) - - return stitch_groups - - -def validation_warnings(self): - if self.shape.area < 20: - label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") - yield SmallShapeWarning(self.shape.centroid, label) - - if self.shrink_or_grow_shape(self.expand, True).is_empty: - yield ExpandWarning(self.shape.centroid) - - if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: - yield UnderlayInsetWarning(self.shape.centroid) - - for warning in super(AutoFill, self).validation_warnings(): - yield warning diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 15e7591c..3f133471 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,18 +5,20 @@ from math import atan, degrees +<<<<<<< HEAD from ..commands import is_command, is_command_symbol +======= +import inkex + +from ..commands import is_command_symbol +>>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements -from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, - SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF) +from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG, + XLINK_HREF) from ..utils import cache -from .auto_fill import AutoFill from .element import EmbroideryElement, param -from .polyline import Polyline -from .satin_column import SatinColumn -from .stroke import Stroke from .validation import ObjectTypeWarning, ValidationWarning @@ -67,28 +69,8 @@ class Clone(EmbroideryElement): return self.get_float_param('angle', 0) def clone_to_element(self, node): - # we need to determine if the source element is polyline, stroke, fill or satin - element = EmbroideryElement(node) - - if node.tag == SVG_POLYLINE_TAG: - return [Polyline(node)] - - elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node): - return [SatinColumn(node)] - else: - elements = [] - if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) - if element.get_style("stroke", self.node) is not None: - if not is_command(element.node): - elements.append(Stroke(node)) - if element.get_boolean_param("stroke_first", False): - elements.reverse() - - return elements + from .utils import node_to_elements + return node_to_elements(node) def to_stitch_groups(self, last_patch=None): patches = [] diff --git a/lib/elements/element.py b/lib/elements/element.py index ef70510d..ee4eadbb 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -87,8 +87,11 @@ class EmbroideryElement(object): return params def replace_legacy_param(self, param): - value = self.node.get(param, "").strip() - self.set_param(param[10:], value) + # remove "embroider_" prefix + new_param = param[10:] + if new_param in INKSTITCH_ATTRIBS: + value = self.node.get(param, "").strip() + self.set_param(param[10:], value) del self.node.attrib[param] @cache @@ -266,6 +269,11 @@ class EmbroideryElement(object): def parse_path(self): return apply_transforms(self.path, self.node) + @property + @cache + def paths(self): + return self.flatten(self.parse_path()) + @property def shape(self): raise NotImplementedError( diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py new file mode 100644 index 00000000..ee56abfc --- /dev/null +++ b/lib/elements/fill_stitch.py @@ -0,0 +1,624 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import logging +import math +import re +import sys +import traceback + +from shapely import geometry as shgeo +from shapely.validation import explain_validity + +from ..i18n import _ +from ..marker import get_marker_elements +from ..stitch_plan import StitchGroup +from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..svg import PIXELS_PER_MM +from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationError, ValidationWarning +from shapely.ops import nearest_points + + +class SmallShapeWarning(ValidationWarning): + name = _("Small Fill") + description = _("This fill object is so small that it would probably look better as running stitch or satin column. " + "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " + "the outline instead.") + + +class ExpandWarning(ValidationWarning): + name = _("Expand") + description = _("The expand parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use original size instead.") + + +class UnderlayInsetWarning(ValidationWarning): + name = _("Inset") + description = _("The underlay inset parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use the original size instead.") + +class MissingGuideLineWarning(ValidationWarning): + name = _("Missing Guideline") + description = _('This object is set to "Guided AutoFill", but has no guide line.') + steps_to_solve = [ + _('* Create a stroke object'), + _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') + ] + +class DisjointGuideLineWarning(ValidationWarning): + name = _("Disjointed Guide Line") + description = _("The guide line of this object isn't within the object borders. " + "The guide line works best, if it is within the target element.") + steps_to_solve = [ + _('* Move the guide line into the element') + ] + +class MultipleGuideLineWarning(ValidationWarning): + name = _("Multiple Guide Lines") + description = _("This object has multiple guide lines, but only the first one will be used.") + steps_to_solve = [ + _("* Remove all guide lines, except for one.") + ] + +class UnconnectedError(ValidationError): + name = _("Unconnected") + description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " + "Ink/Stitch doesn't know what order to stitch them in. Please break this " + "object up into separate shapes.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'), + ] + + +class InvalidShapeError(ValidationError): + name = _("Border crosses itself") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') + ] + + +class FillStitch(EmbroideryElement): + element_name = _("FillStitch") + + @property + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) + def fill_method(self): + return self.get_int_param('fill_method', 0) + + @property + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + def tangential_strategy(self): + return self.get_int_param('tangential_strategy', 1) + + @property + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) + def join_style(self): + return self.get_int_param('join_style', 0) + + @property + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + def interlaced(self): + return self.get_boolean_param('interlaced', True) + + @property + @param('angle', + _('Angle of lines of stitches'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + unit='deg', + type='float', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") + + @property + @param( + 'skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def skip_last(self): + return self.get_boolean_param("skip_last", False) + + @property + @param( + 'flip', + _('Flip fill (start right-to-left)'), + tooltip=_('The flip option can help you with routing your stitch path. ' + 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', + _('Spacing between rows'), + tooltip=_('Distance between rows of stitches.'), + unit='mm', + sort_index=4, + type='float', + default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', + _('Maximum fill stitch length'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index=4, + type='float', + default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', + _('Stagger rows this many times before repeating'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + type='int', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=4) + def staggers(self): + return max(self.get_int_param("staggers", 4), 1) + + @property + @cache + def paths(self): + paths = self.flatten(self.parse_path()) + # ensure path length + for i, path in enumerate(paths): + if len(path) < 3: + paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + return paths + + @property + @cache + def shape(self): + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) + + # There is a great number of "crossing border" errors on fill shapes + # If the polygon fails, we can try to run buffer(0) on the polygon in the + # hope it will fix at least some of them + if not self.shape_is_valid(polygon): + why = explain_validity(polygon) + message = re.match(r".+?(?=\[)", why) + if message.group(0) == "Self-intersection": + buffered = polygon.buffer(0) + # if we receive a multipolygon, only use the first one of it + if type(buffered) == shgeo.MultiPolygon: + buffered = buffered[0] + # we do not want to break apart into multiple objects (possibly in the future?!) + # best way to distinguish the resulting polygon is to compare the area size of the two + # and make sure users will not experience significantly altered shapes without a warning + if type(buffered) == shgeo.Polygon and math.isclose(polygon.area, buffered.area, abs_tol=0.5): + polygon = shgeo.MultiPolygon([buffered]) + + return polygon + + def shape_is_valid(self, shape): + # Shapely will log to stdout to complain about the shape unless we make + # it shut up. + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid = shape.is_valid + + logger.setLevel(level) + + return valid + + def validation_errors(self): + if not self.shape_is_valid(self.shape): + why = explain_validity(self.shape) + message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) + + # I Wish this weren't so brittle... + if "Hole lies outside shell" in message: + yield UnconnectedError((x, y)) + else: + yield InvalidShapeError((x, y)) + + def validation_warnings(self): + if self.shape.area < 20: + label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") + yield SmallShapeWarning(self.shape.centroid, label) + + if self.shrink_or_grow_shape(self.expand, True).is_empty: + yield ExpandWarning(self.shape.centroid) + + if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: + yield UnderlayInsetWarning(self.shape.centroid) + + # guided fill warnings + if self.fill_method == 2: + guide_lines = self._get_guide_lines(True) + if not guide_lines or guide_lines[0].is_empty: + yield MissingGuideLineWarning(self.shape.centroid) + elif len(guide_lines) > 1: + yield MultipleGuideLineWarning(self.shape.centroid) + elif guide_lines[0].disjoint(self.shape): + yield DisjointGuideLineWarning(self.shape.centroid) + return None + + for warning in super(FillStitch, self).validation_warnings(): + yield warning + + @property + @cache + def outline(self): + return self.shape.boundary[0] + + @property + @cache + def outline_length(self): + return self.outline.length + + @property + @param('running_stitch_length_mm', + _('Running stitch length (traversal between sections)'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), + unit='mm', + type='float', + default=1.5, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) + def fill_underlay(self): + return self.get_boolean_param("fill_underlay", default=True) + + @property + @param('fill_underlay_angle', + _('Fill angle'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + unit='deg', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_angle(self): + underlay_angles = self.get_param('fill_underlay_angle', None) + default_value = [self.angle + math.pi / 2.0] + if underlay_angles is not None: + underlay_angles = underlay_angles.strip().split(',') + try: + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] + except (TypeError, ValueError): + return default_value + else: + underlay_angles = default_value + + return underlay_angles + + @property + @param('fill_underlay_row_spacing_mm', + _('Row spacing'), + tooltip=_('default: 3x fill row spacing'), + unit='mm', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_row_spacing(self): + return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 + + @property + @param('fill_underlay_max_stitch_length_mm', + _('Max stitch length'), + tooltip=_('default: equal to fill max stitch length'), + unit='mm', + group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_max_stitch_length(self): + return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length + + @property + @param('fill_underlay_inset_mm', + _('Inset'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + unit='mm', + group=_('AutoFill Underlay'), + type='float', + default=0) + def fill_underlay_inset(self): + return self.get_float_param('fill_underlay_inset_mm', 0) + + @property + @param( + 'fill_underlay_skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + group=_('AutoFill Underlay'), + type='boolean', + default=False) + def fill_underlay_skip_last(self): + return self.get_boolean_param("fill_underlay_skip_last", False) + + @property + @param('expand_mm', + _('Expand'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + unit='mm', + type='float', + default=0, + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) + def expand(self): + return self.get_float_param('expand_mm', 0) + + @property + @param('underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + type='boolean', + default=True, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) + def underpath(self): + return self.get_boolean_param('underpath', True) + + @property + @param( + 'underlay_underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + group=_('AutoFill Underlay'), + type='boolean', + default=True) + def underlay_underpath(self): + return self.get_boolean_param('underlay_underpath', True) + + def shrink_or_grow_shape(self, amount, validate=False): + if amount: + shape = self.shape.buffer(amount) + # changing the size can empty the shape + # in this case we want to use the original shape rather than returning an error + if shape.is_empty and not validate: + return self.shape + if not isinstance(shape, shgeo.MultiPolygon): + shape = shgeo.MultiPolygon([shape]) + return shape + else: + return self.shape + + @property + def underlay_shape(self): + return self.shrink_or_grow_shape(-self.fill_underlay_inset) + + @property + def fill_shape(self): + return self.shrink_or_grow_shape(self.expand) + + def get_starting_point(self, last_patch): + # If there is a "fill_start" Command, then use that; otherwise pick + # the point closest to the end of the last patch. + + if self.get_command('fill_start'): + return self.get_command('fill_start').target_point + elif last_patch: + return last_patch.stitches[-1] + else: + return None + + def get_ending_point(self): + if self.get_command('fill_end'): + return self.get_command('fill_end').target_point + else: + return None + + def to_stitch_groups(self, last_patch): + # backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False + if not self.auto_fill or self.fill_method == 3: + return self.do_legacy_fill() + else: + stitch_groups = [] + start = self.get_starting_point(last_patch) + end = self.get_ending_point() + + try: + if self.fill_underlay: + underlay_stitch_groups, start = self.do_underlay(start) + stitch_groups.extend(underlay_stitch_groups) + if self.fill_method == 0: + stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) + if self.fill_method == 1: + stitch_groups.extend(self.do_tangential_fill(last_patch, start)) + elif self.fill_method == 2: + stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) + except Exception: + self.fatal_fill_error() + + return stitch_groups + + def do_legacy_fill(self): + stitch_lists = legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers, + self.skip_last) + return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] + + def do_underlay(self, starting_point): + stitch_groups = [] + for i in range(len(self.fill_underlay_angle)): + underlay = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_underlay"), + stitches=auto_fill( + self.underlay_shape, + None, + self.fill_underlay_angle[i], + self.fill_underlay_row_spacing, + self.fill_underlay_row_spacing, + self.fill_underlay_max_stitch_length, + self.running_stitch_length, + self.staggers, + self.fill_underlay_skip_last, + starting_point, + underpath=self.underlay_underpath)) + stitch_groups.append(underlay) + + starting_point = underlay.stitches[-1] + return [stitch_groups, starting_point] + + def do_auto_fill(self, last_patch, starting_point, ending_point): + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + None, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + self.skip_last, + starting_point, + ending_point, + self.underpath)) + return [stitch_group] + + def do_tangential_fill(self, last_patch, starting_point): + stitch_groups = [] + polygons = list(self.fill_shape) + if not starting_point: + starting_point = (0, 0) + for poly in polygons: + connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, + self.interlaced, + self.tangential_strategy, + shgeo.Point(starting_point)) + path = [InkstitchPoint(*p) for p in connectedLine] + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) + stitch_groups.append(stitch_group) + + return stitch_groups + + def do_guided_fill(self, last_patch, starting_point, ending_point): + guide_line = self._get_guide_lines() + + # No guide line: fallback to normal autofill + if not guide_line: + return self.do_auto_fill(last_patch, starting_point, ending_point) + + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + guide_line.geoms[0], + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + 0, + self.skip_last, + starting_point, + ending_point, + self.underpath, + self.interlaced)) + return [stitch_group] + + @cache + def _get_guide_lines(self, multiple=False): + guide_lines = get_marker_elements(self.node, "guide-line", False, True) + # No or empty guide line + if not guide_lines or guide_lines['stroke'][0].is_empty: + return None + if multiple: + return guide_lines['stroke'] + else: + return guide_lines['stroke'][0] + + def fatal_fill_error(self): + if hasattr(sys, 'gettrace') and sys.gettrace(): + # if we're debugging, let the exception bubble up + raise + + # for an uncaught exception, give a little more info so that they can create a bug report + message = "" + message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") + message += "\n\n" + # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new + message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") + message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" + message += version.get_inkstitch_version() + "\n\n" + message += traceback.format_exc() + + self.fatal(message) diff --git a/lib/elements/marker.py b/lib/elements/marker.py new file mode 100644 index 00000000..574ce91e --- /dev/null +++ b/lib/elements/marker.py @@ -0,0 +1,32 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex + +from ..i18n import _ +from .element import EmbroideryElement +from .validation import ObjectTypeWarning + + +class MarkerWarning(ObjectTypeWarning): + name = _("Marker Element") + description = _("This element will not be embroidered. " + "It will be applied to objects in the same group. Objects in sub-groups will be ignored.") + steps_to_solve = [ + _("Turn back to normal embroidery element mode, remove the marker:"), + _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), + _('* Go to the Stroke style tab'), + _('* Under "Markers" choose the first (empty) option in the first dropdown list.') + ] + + +class MarkerObject(EmbroideryElement): + + def validation_warnings(self): + repr_point = next(inkex.Path(self.parse_path()).end_points) + yield MarkerWarning(repr_point) + + def to_stitch_groups(self, last_patch): + return [] diff --git a/lib/elements/pattern.py b/lib/elements/pattern.py deleted file mode 100644 index 4b92d366..00000000 --- a/lib/elements/pattern.py +++ /dev/null @@ -1,33 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import inkex - -from ..i18n import _ -from .element import EmbroideryElement -from .validation import ObjectTypeWarning - - -class PatternWarning(ObjectTypeWarning): - name = _("Pattern Element") - description = _("This element will not be embroidered. " - "It will appear as a pattern applied to objects in the same group as it. " - "Objects in sub-groups will be ignored.") - steps_to_solve = [ - _("To disable pattern mode, remove the pattern marker:"), - _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), - _('* Go to the Stroke style tab'), - _('* Under "Markers" choose the first (empty) option in the first dropdown list.') - ] - - -class PatternObject(EmbroideryElement): - - def validation_warnings(self): - repr_point = next(inkex.Path(self.parse_path()).end_points) - yield PatternWarning(repr_point) - - def to_stitch_groups(self, last_patch): - return [] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9b9b8f14..561188aa 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -7,12 +7,12 @@ from ..commands import is_command from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) -from .auto_fill import AutoFill +from .fill_stitch import FillStitch from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject from .image import ImageObject -from .pattern import PatternObject +from .marker import MarkerObject from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -29,8 +29,8 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif has_marker(node, 'pattern'): - return [PatternObject(node)] + elif has_marker(node): + return [MarkerObject(node)] elif node.tag in EMBROIDERABLE_TAGS: element = EmbroideryElement(node) @@ -40,10 +40,7 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) + elements.append(FillStitch(node)) if element.get_style("stroke"): if not is_command(element.node): elements.append(Stroke(node)) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index cf846324..949f947e 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -161,10 +161,10 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node, 'pattern'): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node): nodes.append(node) - # add images, text and patterns for the troubleshoot extension - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node, 'pattern')): + # add images, text and elements with a marker for the troubleshoot extension + elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node)): nodes.append(node) return nodes diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index ae95041b..4c350d62 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, errormsg -from ..elements import AutoFill, Stroke +from ..elements import FillStitch, Stroke from ..i18n import _ from .base import InkstitchExtension @@ -38,7 +38,7 @@ class Cleanup(InkstitchExtension): return for element in self.elements: - if (isinstance(element, AutoFill) and self.rm_fill and element.shape.area < self.fill_threshold): + if (isinstance(element, FillStitch) and self.rm_fill and element.shape.area < self.fill_threshold): element.node.getparent().remove(element.node) count += 1 if (isinstance(element, Stroke) and self.rm_stroke and diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 55963625..69a559ce 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -15,7 +15,7 @@ import wx from wx.lib.scrolledpanel import ScrolledPanel from ..commands import is_command, is_command_symbol -from ..elements import (AutoFill, Clone, EmbroideryElement, Polyline, +from ..elements import (FillStitch, Clone, EmbroideryElement, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..gui import PresetsPanel, SimulatorPreview, WarningPanel @@ -606,8 +606,7 @@ class Params(InkstitchExtension): classes.append(Clone) else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": - classes.append(AutoFill) - # classes.append(Fill) + classes.append(FillStitch) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: diff --git a/lib/marker.py b/lib/marker.py index 3c145145..56a43c3b 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -69,8 +69,11 @@ def get_marker_elements(node, marker, get_fills=True, get_strokes=True): return {'fill': fills, 'stroke': strokes} -def has_marker(node, marker): - if node.tag not in EMBROIDERABLE_TAGS: - return False - style = node.get('style') or '' - return "marker-start:url(#inkstitch-%s-marker)" % marker in style +def has_marker(node, marker=list()): + if not marker: + marker = MARKER + for m in marker: + style = node.get('style') or '' + if "marker-start:url(#inkstitch-%s-marker)" % m in style: + return True + return False diff --git a/symbols/marker.svg b/symbols/marker.svg index 28f4f44d..45219073 100644 --- a/symbols/marker.svg +++ b/symbols/marker.svg @@ -34,21 +34,21 @@ - - - - - + refX="10" + refY="5" + orient="auto" + id="inkstitch-guide-line-marker"> + + + + + -- cgit v1.3.1 From d514eac81937bb64815239dd3aa96e38d6556a32 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 2 Feb 2022 21:19:31 +0100 Subject: adjusting namings --- lib/elements/fill_stitch.py | 32 +- lib/extensions/params.py | 4 - lib/stitches/ConnectAndSamplePattern.py | 949 --------------------- lib/stitches/DebuggingMethods.py | 173 ---- lib/stitches/LineStringSampling.py | 354 -------- lib/stitches/PointTransfer.py | 503 ----------- lib/stitches/StitchPattern.py | 420 --------- lib/stitches/auto_fill.py | 14 +- lib/stitches/constants.py | 10 - lib/stitches/fill.py | 8 +- lib/stitches/point_transfer.py | 495 +++++++++++ lib/stitches/sample_linestring.py | 325 +++++++ .../tangential_fill_stitch_line_creator.py | 330 +++++++ .../tangential_fill_stitch_pattern_creator.py | 906 ++++++++++++++++++++ 14 files changed, 2072 insertions(+), 2451 deletions(-) delete mode 100644 lib/stitches/ConnectAndSamplePattern.py delete mode 100644 lib/stitches/DebuggingMethods.py delete mode 100644 lib/stitches/LineStringSampling.py delete mode 100644 lib/stitches/PointTransfer.py delete mode 100644 lib/stitches/StitchPattern.py create mode 100644 lib/stitches/point_transfer.py create mode 100644 lib/stitches/sample_linestring.py create mode 100644 lib/stitches/tangential_fill_stitch_line_creator.py create mode 100644 lib/stitches/tangential_fill_stitch_pattern_creator.py (limited to 'lib/extensions') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 2e67c517..3256c1ea 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,14 +15,13 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import Point as InkstitchPoint from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning -from shapely.ops import nearest_points class SmallShapeWarning(ValidationWarning): @@ -46,8 +45,7 @@ class UnderlayInsetWarning(ValidationWarning): class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _( - 'This object is set to "Guided AutoFill", but has no guide line.') + description = _('This object is set to "Guided AutoFill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') @@ -65,8 +63,7 @@ class DisjointGuideLineWarning(ValidationWarning): class MultipleGuideLineWarning(ValidationWarning): name = _("Multiple Guide Lines") - description = _( - "This object has multiple guide lines, but only the first one will be used.") + description = _("This object has multiple guide lines, but only the first one will be used.") steps_to_solve = [ _("* Remove all guide lines, except for one.") ] @@ -84,8 +81,7 @@ class UnconnectedError(ValidationError): class InvalidShapeError(ValidationError): name = _("Border crosses itself") - description = _( - "Fill: Shape is not valid. This can happen if the border crosses over itself.") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") steps_to_solve = [ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') ] @@ -125,8 +121,7 @@ class FillStitch(EmbroideryElement): @property @param('angle', _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', sort_index=4, @@ -199,8 +194,7 @@ class FillStitch(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', sort_index=4, select_items=[('fill_method', 0), ('fill_method', 3)], @@ -317,8 +311,7 @@ class FillStitch(EmbroideryElement): @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, @@ -335,8 +328,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -380,8 +372,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -404,8 +395,7 @@ class FillStitch(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, @@ -564,7 +554,7 @@ class FillStitch(EmbroideryElement): if not starting_point: starting_point = (0, 0) for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( poly, -self.row_spacing, self.join_style+1, diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 69a559ce..e50d97d0 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -86,7 +86,6 @@ class ParamsTab(ScrolledPanel): # end wxGlade def pair(self, tab): - # print self.name, "paired with", tab.name self.paired_tab = tab self.update_description() @@ -108,7 +107,6 @@ class ParamsTab(ScrolledPanel): def update_toggle_state(self, event=None, notify_pair=True): enable = self.enabled() - # print self.name, "update_toggle_state", enable for child in self.settings_grid.GetChildren(): widget = child.GetWindow() if widget: @@ -137,7 +135,6 @@ class ParamsTab(ScrolledPanel): event.Skip() def pair_changed(self, value): - # print self.name, "pair_changed", value new_value = not value if self.enabled() != new_value: @@ -192,7 +189,6 @@ class ParamsTab(ScrolledPanel): def apply(self): values = self.get_values() for node in self.nodes: - # print >> sys.stderr, "apply: ", self.name, node.id, values for name, value in values.items(): node.set_param(name, value) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py deleted file mode 100644 index 1cf2b2a1..00000000 --- a/lib/stitches/ConnectAndSamplePattern.py +++ /dev/null @@ -1,949 +0,0 @@ -from shapely.geometry.polygon import LineString, LinearRing -from shapely.geometry import Point, MultiPoint -from shapely.ops import nearest_points -from collections import namedtuple -from depq import DEPQ -import trimesh -import numpy as np -from scipy import spatial -import math -from anytree import PreOrderIter -from ..stitches import LineStringSampling -from ..stitches import PointTransfer -from ..stitches import constants - -nearest_neighbor_tuple = namedtuple( - "nearest_neighbor_tuple", - [ - "nearest_point_parent", - "nearest_point_child", - "proj_distance_parent", - "child_node", - ], -) - - -def cut(line, distance): - """ - Cuts a closed line so that the new closed line starts at the - point with "distance" to the beginning of the old line. - """ - if distance <= 0.0 or distance >= line.length: - return [LineString(line)] - coords = list(line.coords) - for i, p in enumerate(coords): - if i > 0 and p == coords[0]: - pd = line.length - else: - pd = line.project(Point(p)) - if pd == distance: - if coords[0] == coords[-1]: - return LineString(coords[i:] + coords[1: i + 1]) - else: - return LineString(coords[i:] + coords[:i]) - if pd > distance: - cp = line.interpolate(distance) - if coords[0] == coords[-1]: - return LineString( - [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] - ) - else: - return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) - - -def connect_raster_tree_nearest_neighbor( # noqa: C901 - tree, used_offset, stitch_distance, close_point, offset_by_half): - """ - Takes the offsetted curves organized as tree, connects and samples them. - Strategy: A connection from parent to child is made where both curves - come closest together. - Input: - -tree: contains the offsetted curves in a hierachical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one line and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - current_coords = tree.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - # We cut the current item so that its index 0 is closest to close_point - start_distance = tree.val.project(close_point) - if start_distance > 0: - current_coords = cut(current_coords, start_distance) - tree.val = current_coords - - if not tree.transferred_point_priority_deque.is_empty(): - new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert( - item, - math.fmod( - priority - start_distance + current_coords.length, - current_coords.length, - ), - ) - tree.transferred_point_priority_deque = new_DEPQ - - stitching_direction = 1 - # This list should contain a tuple of nearest points between - # the current geometry and the subgeometry, the projected - # distance along the current geometry, and the belonging subtree node - nearest_points_list = [] - - for subnode in tree.children: - point_parent, point_child = nearest_points(current_coords, subnode.val) - proj_distance = current_coords.project(point_parent) - nearest_points_list.append( - nearest_neighbor_tuple( - nearest_point_parent=point_parent, - nearest_point_child=point_child, - proj_distance_parent=proj_distance, - child_node=subnode, - ) - ) - nearest_points_list.sort( - reverse=False, key=lambda tup: tup.proj_distance_parent) - - if nearest_points_list: - start_distance = min( - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[0].proj_distance_parent, - ) - end_distance = max( - current_coords.length - - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[-1].proj_distance_parent, - ) - else: - start_distance = abs_offset * constants.factor_offset_starting_points - end_distance = ( - current_coords.length - abs_offset * constants.factor_offset_starting_points - ) - - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - start_distance, # We add/subtract an offset to not sample - # the same point again (avoid double - # points for start and end) - end_distance, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - assert len(own_coords) == len(own_coords_origin) - own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - tree.stitching_direction = stitching_direction - tree.already_rastered = True - - # Next we need to transfer our rastered points to siblings and childs - to_transfer_point_list = [] - to_transfer_point_list_origin = [] - for k in range(1, len(own_coords) - 1): - # Do not take the first and the last since they are ENTER_LEAVING_POINT - # points for sure - - if ( - not offset_by_half - and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED - ): - continue - if ( - own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT - or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT - ): - continue - to_transfer_point_list.append(Point(own_coords[k])) - point_origin = own_coords_origin[k] - to_transfer_point_list_origin.append(point_origin) - - # Since the projection is only in ccw direction towards inner we need - # to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - offset_by_half, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - False, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - if not nearest_points_list: - # If there is no child (inner geometry) we can simply take - # our own rastered coords as result - result_coords = own_coords - result_coords_origin = own_coords_origin - else: - # There are childs so we need to merge their coordinates + - # with our own rastered coords - - # To create a closed ring - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) - - # own_coords does not start with current_coords but has an offset - # (see call of raster_line_string_with_priority_points) - total_distance = start_distance - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [ - LineStringSampling.PointSource.ENTER_LEAVING_POINT] - for i in range(1, len(own_coords)): - next_distance = math.sqrt( - (own_coords[i][0] - own_coords[i - 1][0]) ** 2 - + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 - ) - while ( - cur_item < len(nearest_points_list) - and total_distance + next_distance + constants.eps - > nearest_points_list[cur_item].proj_distance_parent - ): - - item = nearest_points_list[cur_item] - ( - child_coords, - child_coords_origin, - ) = connect_raster_tree_nearest_neighbor( - item.child_node, - used_offset, - stitch_distance, - item.nearest_point_child, - offset_by_half, - ) - - d = item.nearest_point_parent.distance( - Point(own_coords[i - 1])) - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - # reversing avoids crossing when entering and - # leaving the child segment - result_coords.extend(child_coords[::-1]) - result_coords_origin.extend(child_coords_origin[::-1]) - - # And here we calculate the point for the leaving - d = item.nearest_point_parent.distance(Point(own_coords[i])) - if cur_item < len(nearest_points_list) - 1: - d = min( - d, - abs( - nearest_points_list[cur_item + - 1].proj_distance_parent - - item.proj_distance_parent - ), - ) - - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append( - current_coords.interpolate( - item.proj_distance_parent - + abs_offset * constants.factor_offset_starting_points - ).coords[0] - ) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - - cur_item += 1 - if i < len(own_coords) - 1: - if ( - Point(result_coords[-1]).distance(Point(own_coords[i])) - > abs_offset * constants.factor_offset_remove_points - ): - result_coords.append(own_coords[i]) - result_coords_origin.append(own_coords_origin[i]) - - # Since current_coords and temp are rastered differently - # there accumulate errors regarding the current distance. - # Since a projection of each point in temp would be very time - # consuming we project only every n-th point which resets - # the accumulated error every n-th point. - if i % 20 == 0: - total_distance = current_coords.project(Point(own_coords[i])) - else: - total_distance += next_distance - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin - - -def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): - """ - Takes a line and calculates the nearest distance along this - line to enter the next_line - Input: - -travel_line: The "parent" line for which the distance should - be minimized to enter next_line - -next_line: contains the next_line which need to be entered - -thresh: The distance between travel_line and next_line needs - to below thresh to be a valid point for entering - Output: - -tuple - the tuple structure is: - (nearest point in travel_line, nearest point in next_line) - """ - point_list = list(MultiPoint(travel_line.coords)) - - if point_list[0].distance(next_line) < thresh: - return nearest_points(point_list[0], next_line) - - for i in range(len(point_list) - 1): - line_segment = LineString([point_list[i], point_list[i + 1]]) - result = nearest_points(line_segment, next_line) - - if result[0].distance(result[1]) < thresh: - return result - line_segment = LineString([point_list[-1], point_list[0]]) - result = nearest_points(line_segment, next_line) - - if result[0].distance(result[1]) < thresh: - return result - else: - return None - - -def create_nearest_points_list( - travel_line, children_list, threshold, threshold_hard, preferred_direction=0): - """ - Takes a line and calculates the nearest distance along this line to - enter the childs in children_list - The method calculates the distances along the line and along the - reversed line to find the best direction which minimizes the overall - distance for all childs. - Input: - -travel_line: The "parent" line for which the distance should - be minimized to enter the childs - -children_list: contains the childs of travel_line which need to be entered - -threshold: The distance between travel_line and a child needs to be - below threshold to be a valid point for entering - -preferred_direction: Put a bias on the desired travel direction along - travel_line. If equals zero no bias is applied. - preferred_direction=1 means we prefer the direction of travel_line; - preferred_direction=-1 means we prefer the opposite direction. - Output: - -stitching direction for travel_line - -list of tuples (one tuple per child). The tuple structure is: - ((nearest point in travel_line, nearest point in child), - distance along travel_line, belonging child) - """ - - result_list_in_order = [] - result_list_reversed_order = [] - - travel_line_reversed = LinearRing(travel_line.coords[::-1]) - - weight_in_order = 0 - weight_reversed_order = 0 - for child in children_list: - result = get_nearest_points_closer_than_thresh( - travel_line, child.val, threshold - ) - if result is None: - # where holes meet outer borders a distance - # up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh( - travel_line, child.val, threshold_hard - ) - assert result is not None - proj = travel_line.project(result[0]) - weight_in_order += proj - result_list_in_order.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - result = get_nearest_points_closer_than_thresh( - travel_line_reversed, child.val, threshold - ) - if result is None: - # where holes meet outer borders a distance - # up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh( - travel_line_reversed, child.val, threshold_hard - ) - assert result is not None - proj = travel_line_reversed.project(result[0]) - weight_reversed_order += proj - result_list_reversed_order.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - if preferred_direction == 1: - # Reduce weight_in_order to make in order stitching more preferred - weight_in_order = min( - weight_in_order / 2, max(0, weight_in_order - 10 * threshold) - ) - if weight_in_order == weight_reversed_order: - return (1, result_list_in_order) - elif preferred_direction == -1: - # Reduce weight_reversed_order to make reversed - # stitching more preferred - weight_reversed_order = min( - weight_reversed_order / - 2, max(0, weight_reversed_order - 10 * threshold) - ) - if weight_in_order == weight_reversed_order: - return (-1, result_list_reversed_order) - - if weight_in_order < weight_reversed_order: - return (1, result_list_in_order) - else: - return (-1, result_list_reversed_order) - - -def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): - """ - Takes a line segment (consisting of 3 points!) - and calculates a new middle point if the line_segment is - straight enough to be resampled by points max_stitch_distance apart FROM THE END OF line_segment. - Returns None if the middle point is not needed. - """ - angles = LineStringSampling.calculate_line_angles(line_segment) - if angles[1] < abs_offset * constants.limiting_angle_straight: - if line_segment.length < max_stitch_distance: - return None - else: - return line_segment.interpolate( - line_segment.length - max_stitch_distance - ).coords[0] - else: - return line_segment.coords[1] - - -def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 - """ - Takes the offsetted curves organized as tree, connects and samples them. - Strategy: A connection from parent to child is made as fast as possible to - reach the innermost child as fast as possible in order to stitch afterwards - from inner to outer. - Input: - -tree: contains the offsetted curves in a hierachical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one line and sampled with points obeying - stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was placed - at this position - """ - - current_coords = tree.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - start_distance = tree.val.project(close_point) - # We cut the current path so that its index 0 is closest to close_point - if start_distance > 0: - current_coords = cut(current_coords, start_distance) - tree.val = current_coords - - if not tree.transferred_point_priority_deque.is_empty(): - new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert( - item, - math.fmod( - priority - start_distance + current_coords.length, - current_coords.length, - ), - ) - tree.transferred_point_priority_deque = new_DEPQ - - # We try to use always the opposite stitching direction with respect to the - # parent to avoid crossings when entering and leaving the child - parent_stitching_direction = -1 - if tree.parent is not None: - parent_stitching_direction = tree.parent.stitching_direction - - # Find the nearest point in current_coords and its children and - # sort it along the stitching direction - stitching_direction, nearest_points_list = create_nearest_points_list( - current_coords, - tree.children, - 1.5 * abs_offset, - 2.05 * abs_offset, - parent_stitching_direction, - ) - nearest_points_list.sort( - reverse=False, key=lambda tup: tup.proj_distance_parent) - - # Have a small offset for the starting and ending to avoid double points - # at start and end point (since the paths are closed rings) - if nearest_points_list: - start_offset = min( - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[0].proj_distance_parent, - ) - end_offset = max( - current_coords.length - - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[-1].proj_distance_parent, - ) - else: - start_offset = abs_offset * constants.factor_offset_starting_points - end_offset = ( - current_coords.length - abs_offset * constants.factor_offset_starting_points - ) - - if stitching_direction == 1: - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - start_offset, # We add start_offset to not sample the same - # point again (avoid double points for start - # and end) - end_offset, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - else: - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - current_coords.length - start_offset, # We subtract - # start_offset to not - # sample the same point - # again (avoid double - # points for start - # and end) - current_coords.length - end_offset, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - current_coords.coords = current_coords.coords[::-1] - - assert len(own_coords) == len(own_coords_origin) - - tree.stitching_direction = stitching_direction - tree.already_rastered = True - - to_transfer_point_list = [] - to_transfer_point_list_origin = [] - for k in range(0, len(own_coords)): - # TODO: maybe do not take the first and the last - # since they are ENTER_LEAVING_POINT points for sure - if ( - not offset_by_half - and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED - or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT - ): - continue - if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT: - continue - to_transfer_point_list.append(Point(own_coords[k])) - to_transfer_point_list_origin.append(own_coords_origin[k]) - - assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) - - # Next we need to transfer our rastered points to siblings and childs - # Since the projection is only in ccw direction towards inner we - # need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - offset_by_half, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - False, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - if not nearest_points_list: - # If there is no child (inner geometry) we can simply - # take our own rastered coords as result - result_coords = own_coords - result_coords_origin = own_coords_origin - else: - # There are childs so we need to merge their coordinates - # with our own rastered coords - - # Create a closed ring for the following code - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) - - # own_coords does not start with current_coords but has an offset - # (see call of raster_line_string_with_priority_points) - total_distance = start_offset - - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [own_coords_origin[0]] - - for i in range(1, len(own_coords)): - next_distance = math.sqrt( - (own_coords[i][0] - own_coords[i - 1][0]) ** 2 - + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 - ) - while ( - cur_item < len(nearest_points_list) - and total_distance + next_distance + constants.eps - > nearest_points_list[cur_item].proj_distance_parent - ): - # The current and the next point in own_coords enclose the - # nearest point tuple between this geometry and child - # geometry. Hence we need to insert the child geometry points - # here before the next point of own_coords. - item = nearest_points_list[cur_item] - ( - child_coords, - child_coords_origin, - ) = connect_raster_tree_from_inner_to_outer( - item.child_node, - used_offset, - stitch_distance, - item.nearest_point_child, - offset_by_half, - ) - - # Imagine the nearest point of the child is within a long - # segment of the parent. Without additonal points - # on the parent side this would cause noticeable deviations. - # Hence we add here points shortly before and after - # the entering of the child to have only minor deviations to - # the desired shape. - # Here is the point for the entering: - if ( - Point(result_coords[-1] - ).distance(item.nearest_point_parent) - > constants.factor_offset_starting_points * abs_offset - ): - result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - - # Check whether the number of points of the connecting lines - # from child to child can be reduced - if len(child_coords) > 1: - point = calculate_replacing_middle_point( - LineString( - [result_coords[-1], child_coords[0], child_coords[1]] - ), - abs_offset, - stitch_distance, - ) - - if point is not None: - result_coords.append(point) - result_coords_origin.append(child_coords_origin[0]) - - result_coords.extend(child_coords[1:]) - result_coords_origin.extend(child_coords_origin[1:]) - else: - result_coords.extend(child_coords) - result_coords_origin.extend(child_coords_origin) - - # And here is the point for the leaving of the child - # (distance to the own following point should not be too large) - d = item.nearest_point_parent.distance(Point(own_coords[i])) - if cur_item < len(nearest_points_list) - 1: - d = min( - d, - abs( - nearest_points_list[cur_item + - 1].proj_distance_parent - - item.proj_distance_parent - ), - ) - - if d > constants.factor_offset_starting_points * abs_offset: - result_coords.append( - current_coords.interpolate( - item.proj_distance_parent - + 2 * constants.factor_offset_starting_points * abs_offset - ).coords[0] - ) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - # Check whether this additional point makes the last point - # of the child unnecessary - point = calculate_replacing_middle_point( - LineString( - [result_coords[-3], result_coords[-2], result_coords[-1]] - ), - abs_offset, - stitch_distance, - ) - if point is None: - result_coords.pop(-2) - result_coords_origin.pop(-2) - - cur_item += 1 - if i < len(own_coords) - 1: - if ( - Point(result_coords[-1]).distance(Point(own_coords[i])) - > abs_offset * constants.factor_offset_remove_points - ): - result_coords.append(own_coords[i]) - result_coords_origin.append(own_coords_origin[i]) - - # Since current_coords and own_coords are rastered differently - # there accumulate errors regarding the current distance. - # Since a projection of each point in own_coords would be very - # time consuming we project only every n-th point which resets - # the accumulated error every n-th point. - if i % 20 == 0: - total_distance = current_coords.project(Point(own_coords[i])) - else: - total_distance += next_distance - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin - - -# Partly taken from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py -def interpolate_LinearRings(a, b, start=None, step=.005): - """ - Interpolate between two LinearRings - Parameters - ------------- - a : shapely.geometry.Polygon.LinearRing - LinearRing start point will lie on - b : shapely.geometry.Polygon.LinearRing - LinearRing end point will lie on - start : (2,) float, or None - Point to start at - step : float - How far apart should points on - the path be. - Returns - ------------- - path : (n, 2) float - Path interpolated between two LinearRings - """ - - # resample the first LinearRing so every sample is spaced evenly - ra = trimesh.path.traversal.resample_path( - a, step=step) - if not a.is_ccw: - ra = ra[::-1] - - assert trimesh.path.util.is_ccw(ra) - if start is not None: - # find the closest index on LinerRing 'a' - # by creating a KDTree - tree_a = spatial.cKDTree(ra) - index = tree_a.query(start)[1] - ra = np.roll(ra, -index, axis=0) - - # resample the second LinearRing for even spacing - rb = trimesh.path.traversal.resample_path(b, - step=step) - if not b.is_ccw: - rb = rb[::-1] - - # we want points on 'b' that correspond index- wise - # the resampled points on 'a' - tree_b = spatial.cKDTree(rb) - # points on b with corresponding indexes to ra - pb = rb[tree_b.query(ra)[1]] - - # linearly interpolate between 'a' and 'b' - weights = np.linspace(0.0, 1.0, len(ra)).reshape((-1, 1)) - - # start on 'a' and end on 'b' - points = (ra * (1.0 - weights)) + (pb * weights) - - result = LineString(points) - - return result.simplify(constants.simplification_threshold, False) - - -def connect_raster_tree_spiral( - tree, used_offset, stitch_distance, close_point, offset_by_half): - """ - Takes the offsetted curves organized as tree, connects and samples them as a spiral. - It expects that each node in the tree has max. one child - Input: - -tree: contains the offsetted curves in a hierarchical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one spiral and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - abs_offset = abs(used_offset) - if tree.is_leaf: - return LineStringSampling.raster_line_string_with_priority_points( - tree.val, - 0, - tree.val.length, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - result_coords = [] - result_coords_origin = [] - starting_point = close_point.coords[0] - # iterate to the second last level - for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): - ring1 = node.val - ring2 = node.children[0].val - - part_spiral = interpolate_LinearRings( - ring1, ring2, starting_point) - node.val = part_spiral - - for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): - (own_coords, own_coords_origin) = LineStringSampling.raster_line_string_with_priority_points( - node.val, - 0, - node.val.length, - stitch_distance, - node.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - PointTransfer.transfer_points_to_surrounding( - node, - -used_offset, - offset_by_half, - own_coords, - own_coords_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=False, - transfer_to_child=True) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - node, - -used_offset, - False, - own_coords, - own_coords_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=False, - transfer_to_child=True) - - # Check whether starting of own_coords or end of result_coords can be removed - if not result_coords: - result_coords.extend(own_coords) - result_coords_origin.extend(own_coords_origin) - elif len(own_coords) > 0: - if Point(result_coords[-1]).distance(Point(own_coords[0])) > constants.line_lengh_seen_as_one_point: - lineseg = LineString( - [result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) - else: - lineseg = LineString( - [result_coords[-2], result_coords[-1], own_coords[1]]) - (temp_coords, _) = LineStringSampling.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, - DEPQ(), abs_offset, offset_by_half, False) - if len(temp_coords) == 2: # only start and end point of lineseg was needed - result_coords.pop() - result_coords_origin.pop() - result_coords.extend(own_coords[1:]) - result_coords_origin.extend(own_coords_origin[1:]) - elif len(temp_coords) == 3: # one middle point within lineseg was needed - result_coords.pop() - result_coords.append(temp_coords[1]) - result_coords.extend(own_coords[1:]) - result_coords_origin.extend(own_coords_origin[1:]) - else: # all points were needed - result_coords.extend(own_coords) - result_coords_origin.extend(own_coords_origin) - # make sure the next section starts where this - # section of the curve ends - starting_point = result_coords[-1] - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin diff --git a/lib/stitches/DebuggingMethods.py b/lib/stitches/DebuggingMethods.py deleted file mode 100644 index e239edba..00000000 --- a/lib/stitches/DebuggingMethods.py +++ /dev/null @@ -1,173 +0,0 @@ -import matplotlib.pyplot as plt -from shapely.geometry import Polygon - -from anytree import PreOrderIter - -# import LineStringSampling as Sampler -import numpy as np -import matplotlib.collections as mcoll - -# def offset_polygons(polys, offset,joinstyle): -# if polys.geom_type == 'Polygon': -# inners = polys.interiors -# outer = polys.exterior -# polyinners = [] -# for inner in inners: -# inner = inner.parallel_offset(offset,'left', 5, joinstyle, 1) -# polyinners.append(Polygon(inner)) -# outer = outer.parallel_offset(offset,'left', 5, joinstyle, 1) -# return Polygon(outer).difference(MultiPolygon(polyinners)) -# else: -# polyreturns = [] -# for poly in polys: -# inners = poly.interiors -# outer = poly.exterior -# polyinners = [] -# for inner in inners: -# inner = inner.parallel_offset(offset,'left', 5, joinstyle, 1) -# polyinners.append(Polygon(inner)) -# outer = outer.parallel_offset(offset,'left', 5, joinstyle, 1) -# result = Polygon(outer).difference(MultiPolygon(polyinners)) -# polyreturns.append(result) -# return MultiPolygon(polyreturns) - -# For debugging - - -def plot_MultiPolygon(MultiPoly, plt, colorString): - if MultiPoly.is_empty: - return - if MultiPoly.geom_type == "Polygon": - x2, y2 = MultiPoly.exterior.xy - plt.plot(x2, y2, colorString) - - for inners in MultiPoly.interiors: - x2, y2 = inners.coords.xy - plt.plot(x2, y2, colorString) - else: - for poly in MultiPoly: - x2, y2 = poly.exterior.xy - plt.plot(x2, y2, colorString) - - for inners in poly.interiors: - x2, y2 = inners.coords.xy - plt.plot(x2, y2, colorString) - - -# Test whether there are areas which would currently not be stitched but should be stitched - - -def subtractResult(poly, rootPoly, offsetThresh): - poly2 = Polygon(poly) - for node in PreOrderIter(rootPoly): - poly2 = poly2.difference(node.val.buffer(offsetThresh, 5, 3, 3)) - return poly2 - - -# Used for debugging - plots all polygon exteriors within an AnyTree which is provided by the root node rootPoly. - - -def drawPoly(rootPoly, colorString): - fig, axs = plt.subplots(1, 1) - axs.axis("equal") - plt.gca().invert_yaxis() - for node in PreOrderIter(rootPoly): - # if(node.id == "hole"): - # node.val = LinearRing(node.val.coords[::-1]) - print("Bounds:") - print(node.val.bounds) - x2, y2 = node.val.coords.xy - plt.plot(x2, y2, colorString) - plt.show(block=True) - - -def drawresult(resultcoords, resultcoords_Origin, colorString): - fig, axs = plt.subplots(1, 1) - axs.axis("equal") - plt.gca().invert_yaxis() - plt.plot(*zip(*resultcoords), colorString) - - colormap = np.array(["r", "g", "b", "c", "m", "y", "k", "gray", "m"]) - labelmap = np.array( - [ - "MUST_USE", - "REGULAR_SPACING", - "INITIAL_RASTERING", - "EDGE_NEEDED", - "NOT_NEEDED", - "ALREADY_TRANSFERRED", - "ADDITIONAL_TRACKING_POINT_NOT_NEEDED", - "EDGE_RASTERING_ALLOWED", - "EDGE_PREVIOUSLY_SHIFTED", - ] - ) - - for i in range(0, 8 + 1): - # if i != Sampler.PointSource.EDGE_NEEDED and i != Sampler.PointSource.INITIAL_RASTERING: - # continue - selection = [] - for j in range(len(resultcoords)): - if i == resultcoords_Origin[j]: - selection.append(resultcoords[j]) - if len(selection) > 0: - plt.scatter(*zip(*selection), c=colormap[i], label=labelmap[i]) - - # plt.scatter(*zip(*resultcoords), - # c=colormap[resultcoords_Origin]) - axs.legend() - plt.show(block=True) - - -# Just for debugging in order to draw the connected line with color gradient - - -def colorline( - x, - y, - z=None, - cmap=plt.get_cmap("copper"), - norm=plt.Normalize(0.0, 1.0), - linewidth=3, - alpha=1.0, -): - """ - http://nbviewer.ipython.org/github/dpsanders/matplotlib-examples/blob/master/colorline.ipynb - http://matplotlib.org/examples/pylab_examples/multicolored_line.html - Plot a colored line with coordinates x and y - Optionally specify colors in the array z - Optionally specify a colormap, a norm function and a line width - """ - - # Default colors equally spaced on [0,1]: - if z is None: - z = np.linspace(0.0, 1.0, len(x)) - - # Special case if a single number: - if not hasattr(z, "__iter__"): # to check for numerical input -- this is a hack - z = np.array([z]) - - z = np.asarray(z) - - segments = make_segments(x, y) - lc = mcoll.LineCollection( - segments, array=z, cmap=cmap, norm=norm, linewidth=linewidth, alpha=alpha - ) - - ax = plt.gca() - ax.add_collection(lc) - - return lc - - -# Used by colorline - - -def make_segments(x, y): - """ - Create list of line segments from x and y coordinates, in the correct format - for LineCollection: an array of the form numlines x (points per line) x 2 (x - and y) array - """ - points = np.array([x, y]).T.reshape(-1, 1, 2) - segments = np.concatenate([points[:-1], points[1:]], axis=1) - return segments diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py deleted file mode 100644 index 71660e2d..00000000 --- a/lib/stitches/LineStringSampling.py +++ /dev/null @@ -1,354 +0,0 @@ -from shapely.geometry.polygon import LineString -from shapely.geometry import Point -from shapely.ops import substring -import math -import numpy as np -from enum import IntEnum -from ..stitches import constants -from ..stitches import PointTransfer - - -class PointSource(IntEnum): - """ - Used to tag the origin of a rastered point - """ - # MUST_USE = 0 # Legacy - REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance - # INITIAL_RASTERING = 2 #Legacy - # point which must be stitched to avoid to large deviations to the desired path - EDGE_NEEDED = 3 - # NOT_NEEDED = 4 #Legacy - # ALREADY_TRANSFERRED = 5 #Legacy - # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy - # EDGE_RASTERING_ALLOWED = 7 #Legacy - # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy - ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child - # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - SOFT_EDGE_INTERNAL = 10 - # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - HARD_EDGE_INTERNAL = 11 - # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT - PROJECTED_POINT = 12 - REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance - # FORBIDDEN_POINT_INTERNAL=14 #Legacy - SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - HARD_EDGE = 16 - FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden - # If one decides to avoid forbidden points new points to the left and to the right as replacement are created - REPLACED_FORBIDDEN_POINT = 18 - DIRECT = 19 # Calculated by next neighbor projection - OVERNEXT = 20 # Calculated by overnext neighbor projection - - -def calculate_line_angles(line): - """ - Calculates the angles between adjacent edges at each interior point - Note that the first and last values in the return array are zero since for the boundary points no - angle calculations were possible - """ - Angles = np.zeros(len(line.coords)) - for i in range(1, len(line.coords)-1): - vec1 = np.array(line.coords[i])-np.array(line.coords[i-1]) - vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) - vec1length = np.linalg.norm(vec1) - vec2length = np.linalg.norm(vec2) - # if vec1length <= 0: - # print("HIER FEHLER") - - # if vec2length <=0: - # print("HIER FEHLEr") - assert(vec1length > 0) - assert(vec2length > 0) - scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) - scalar_prod = min(max(scalar_prod, -1), 1) - # if scalar_prod > 1.0: - # scalar_prod = 1.0 - # elif scalar_prod < -1.0: - # scalar_prod = -1.0 - Angles[i] = math.acos(scalar_prod) - return Angles - - -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 - must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): - """ - Rasters a line between start_distance and end_distance. - Input: - -line: The line to be rastered - -start_distance: The distance along the line from which the rastering should start - -end_distance: The distance along the line until which the rastering should be done - -maxstitch_distance: The maximum allowed stitch distance - -Note that start_distance > end_distance for stitching_direction = -1 - -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - -abs_offset: used offset between to offsetted curves - -offset_by_half: Whether the points of neighboring lines shall be interlaced or not - -replace_forbidden_points: Whether points marked as forbidden in must_use_points_deque shall be replaced by adjacend points - Output: - -List of tuples with the rastered point coordinates - -List which defines the point origin for each point according to the PointSource enum. - """ - - if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): - return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] - - deque_points = list(must_use_points_deque) - - linecoords = line.coords - - if start_distance > end_distance: - start_distance, end_distance = line.length - \ - start_distance, line.length-end_distance - linecoords = linecoords[::-1] - for i in range(len(deque_points)): - deque_points[i] = (deque_points[i][0], - line.length-deque_points[i][1]) - else: - # Since points with highest priority (=distance along line) are first (descending sorted) - deque_points = deque_points[::-1] - - # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] - while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): - deque_points.pop(0) - while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): - deque_points.pop() - - -# Ordering in priority queue: -# (point, LineStringSampling.PointSource), priority) - # might be different from line for stitching_direction=-1 - aligned_line = LineString(linecoords) - path_coords = substring(aligned_line, - start_distance, end_distance) - - # aligned line is a line without doubled points. - # I had the strange situation in which the offset "start_distance" from the line beginning - # resulted in a starting point which was already present in aligned_line causing a doubled point. - # A double point is not allowed in the following calculations so we need to remove it: - if (abs(path_coords.coords[0][0]-path_coords.coords[1][0]) < constants.eps and - abs(path_coords.coords[0][1]-path_coords.coords[1][1]) < constants.eps): - path_coords.coords = path_coords.coords[1:] - if (abs(path_coords.coords[-1][0]-path_coords.coords[-2][0]) < constants.eps and - abs(path_coords.coords[-1][1]-path_coords.coords[-2][1]) < constants.eps): - path_coords.coords = path_coords.coords[:-1] - - angles = calculate_line_angles(path_coords) - # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge - angles[0] = 1.1*constants.limiting_angle - angles[-1] = 1.1*constants.limiting_angle - - current_distance = 0 - last_point = Point(path_coords.coords[0]) - # Next we merge the line points and the projected (deque) points into one list - merged_point_list = [] - dq_iter = 0 - for point, angle in zip(path_coords.coords, angles): - # if abs(point[0]-7) < 0.2 and abs(point[1]-3.3) < 0.2: - # print("GEFUNDEN") - current_distance += last_point.distance(Point(point)) - last_point = Point(point) - while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance+start_distance: - # We want to avoid setting points at soft edges close to forbidden points - if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: - # Check whether a previous added point is a soft edge close to the forbidden point - if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and - abs(merged_point_list[-1][1]-deque_points[dq_iter][1]+start_distance < abs_offset*constants.factor_offset_forbidden_point)): - item = merged_point_list.pop() - merged_point_list.append((PointTransfer.projected_point_tuple( - point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1]-start_distance)) - else: - merged_point_list.append( - (deque_points[dq_iter][0], deque_points[dq_iter][1]-start_distance)) - # merged_point_list.append(deque_points[dq_iter]) - dq_iter += 1 - # Check whether the current point is close to a forbidden point - if (dq_iter < len(deque_points) and - deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and - angle < constants.limiting_angle and - abs(deque_points[dq_iter-1][1]-current_distance-start_distance) < abs_offset*constants.factor_offset_forbidden_point): - point_source = PointSource.FORBIDDEN_POINT - else: - if angle < constants.limiting_angle: - point_source = PointSource.SOFT_EDGE_INTERNAL - else: - point_source = PointSource.HARD_EDGE_INTERNAL - merged_point_list.append((PointTransfer.projected_point_tuple( - point=Point(point), point_source=point_source), current_distance)) - - result_list = [merged_point_list[0]] - - # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified - # to a straight line by shapelys simplify method. - # Then, look at the points within this segment and choose the best fitting one - # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment - # and start point for the next segment (so we do not always take the maximum possible length for a segment) - segment_start_index = 0 - segment_end_index = 1 - forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and - # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: - # print("GEFUNDEN") - - # Collection of points for the current segment - current_point_list = [merged_point_list[segment_start_index][0].point] - - while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1] - \ - merged_point_list[segment_start_index][1] - if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1] + \ - maxstitch_distance - merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( - point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) - # if (abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and - # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9) < 0.2): - # print("GEFUNDEN") - segment_end_index += 1 - break - # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and - # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: - # print("GEFUNDEN") - - current_point_list.append( - merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify( - constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) - if simplified_len > 2: # not all points have been simplified - so we need to add it - break - - if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: - segment_end_index += 1 - break - segment_end_index += 1 - - segment_end_index -= 1 - - # Now we choose the best fitting point within this segment - index_overnext = -1 - index_direct = -1 - index_hard_edge = -1 - - iter = segment_start_index+1 - while (iter <= segment_end_index): - if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: - index_overnext = iter - elif merged_point_list[iter][0].point_source == PointSource.DIRECT: - index_direct = iter - elif merged_point_list[iter][0].point_source == PointSource.HARD_EDGE_INTERNAL: - index_hard_edge = iter - iter += 1 - if index_hard_edge != -1: - segment_end_index = index_hard_edge - else: - if offset_by_half: - index_preferred = index_overnext - index_less_preferred = index_direct - else: - index_preferred = index_direct - index_less_preferred = index_overnext - - if index_preferred != -1: - if (index_less_preferred != -1 and index_less_preferred > index_preferred and - (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext * - (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): - # We allow to take the direct projected point instead of the overnext projected point if it would result in a - # significant longer segment length - segment_end_index = index_less_preferred - else: - segment_end_index = index_preferred - elif index_less_preferred != -1: - segment_end_index = index_less_preferred - - # Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - # If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: - new_point_left_proj = result_list[index][1]-distance_left - if new_point_left_proj < 0: - new_point_left_proj += line.length - new_point_right_proj = result_list[index][1]+distance_right - if new_point_right_proj > line.length: - new_point_right_proj -= line.length - point_left = line.interpolate(new_point_left_proj) - point_right = line.interpolate(new_point_right_proj) - forbidden_point_distance = result_list[index][0].point.distance( - LineString([point_left, point_right])) - if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: - del result_list[index] - result_list.insert(index, (PointTransfer.projected_point_tuple( - point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) - result_list.insert(index, (PointTransfer.projected_point_tuple( - point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) - current_index_shift += 1 - break - else: - distance_left /= 2.0 - distance_right /= 2.0 - return result_list - - -if __name__ == "__main__": - line = LineString([(0, 0), (1, 0), (2, 1), (3, 0), (4, 0)]) - - print(calculate_line_angles(line)*180.0/math.pi) diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py deleted file mode 100644 index 93fe02c5..00000000 --- a/lib/stitches/PointTransfer.py +++ /dev/null @@ -1,503 +0,0 @@ -from shapely.geometry import Point, MultiPoint -from shapely.geometry.polygon import LineString, LinearRing -from collections import namedtuple -from shapely.ops import nearest_points -import math -from ..stitches import constants -from ..stitches import LineStringSampling - -projected_point_tuple = namedtuple( - 'projected_point_tuple', ['point', 'point_source']) - - -def calc_transferred_point(bisectorline, child): - """ - Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). - It returns the intersection point and its distance along the coordinates of the child or "None, None" if no - intersection was found. - """ - result = bisectorline.intersection(child.val) - if result.is_empty: - return None, None - desired_point = Point() - if result.geom_type == 'Point': - desired_point = result - elif result.geom_type == 'LineString': - desired_point = Point(result.coords[0]) - else: - resultlist = list(result) - desired_point = resultlist[0] - if len(resultlist) > 1: - desired_point = nearest_points( - result, Point(bisectorline.coords[0]))[0] - - priority = child.val.project(desired_point) - point = desired_point - return point, priority - - -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 - overnext_neighbor=False, transfer_forbidden_points=False, - transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): - """ - Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs - To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. - Input: - -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. - -used_offset: The used offset when the curves where offsetted - -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" - -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points - can be handled as closed ring - -to_transfer_points_origin: The origin tag of each point in to_transfer_points - -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) - -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as - forbidden points to the neighbor to avoid a point placing there - -transfer_to_parent: If True, points will be transferred to the parent - -transfer_to_sibling: If True, points will be transferred to the siblings - -transfer_to_child: If True, points will be transferred to the childs - Output: - -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - """ - - assert(len(to_transfer_points) == len(to_transfer_points_origin) - or len(to_transfer_points_origin) == 0) - assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and ( - offset_by_half or not offset_by_half and overnext_neighbor)) - - if len(to_transfer_points) == 0: - return - - # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: - childs_tuple = treenode.children - siblings_tuple = treenode.siblings - # Take only neighbors which have not rastered before - # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) - child_list = [] - child_list_forbidden = [] - neighbor_list = [] - neighbor_list_forbidden = [] - - if transfer_to_child: - for child in childs_tuple: - if not child.already_rastered: - if not overnext_neighbor: - child_list.append(child) - if transfer_forbidden_points: - child_list_forbidden.append(child) - if overnext_neighbor: - for subchild in child.children: - if not subchild.already_rastered: - child_list.append(subchild) - - if transfer_to_sibling: - for sibling in siblings_tuple: - if not sibling.already_rastered: - if not overnext_neighbor: - neighbor_list.append(sibling) - if transfer_forbidden_points: - neighbor_list_forbidden.append(sibling) - if overnext_neighbor: - for subchild in sibling.children: - if not subchild.already_rastered: - neighbor_list.append(subchild) - - if transfer_to_parent and treenode.parent is not None: - if not treenode.parent.already_rastered: - if not overnext_neighbor: - neighbor_list.append(treenode.parent) - if transfer_forbidden_points: - neighbor_list_forbidden.append(treenode.parent) - if overnext_neighbor: - if treenode.parent.parent is not None: - if not treenode.parent.parent.already_rastered: - neighbor_list.append(treenode.parent.parent) - - if not neighbor_list and not child_list: - return - - # Go through all rastered points of treenode and check where they should be transferred to its neighbar - point_list = list(MultiPoint(to_transfer_points)) - point_source_list = to_transfer_points_origin.copy() - - # For a linear ring the last point is the same as the starting point which we delete - # since we do not want to transfer the starting and end point twice - closed_line = LineString(to_transfer_points) - if point_list[0].distance(point_list[-1]) < constants.point_spacing_to_be_considered_equal: - point_list.pop() - if(point_source_list): - point_source_list.pop() - if len(point_list) == 0: - return - else: - # closed line is needed if we offset by half since we need to determine the line - # length including the closing segment - closed_line = LinearRing(to_transfer_points) - - bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor * \ - (2.0 if overnext_neighbor else 1.0) - - bisectorline_length_forbidden_points = abs(used_offset) * \ - constants.transfer_point_distance_factor - - linesign_child = math.copysign(1, used_offset) - - i = 0 - currentDistance = 0 - while i < len(point_list): - assert(point_source_list[i] != - LineStringSampling.PointSource.ENTER_LEAVING_POINT) - # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: - # print("HIIIIIIIIIIIERRR") - - # We create a bisecting line through the current point - normalized_vector_prev_x = ( - point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape - normalized_vector_prev_y = ( - point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) - prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + - normalized_vector_prev_y*normalized_vector_prev_y) - - normalized_vector_prev_x /= prev_spacing - normalized_vector_prev_y /= prev_spacing - - normalized_vector_next_x = normalized_vector_next_y = 0 - next_spacing = 0 - while True: - normalized_vector_next_x = ( - point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) - normalized_vector_next_y = ( - point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) - next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + - normalized_vector_next_y*normalized_vector_next_y) - if next_spacing < constants.line_lengh_seen_as_one_point: - point_list.pop(i) - if(point_source_list): - point_source_list.pop(i) - currentDistance += next_spacing - continue - - normalized_vector_next_x /= next_spacing - normalized_vector_next_y /= next_spacing - break - - vecx = (normalized_vector_next_x+normalized_vector_prev_x) - vecy = (normalized_vector_next_y+normalized_vector_prev_y) - vec_length = math.sqrt(vecx*vecx+vecy*vecy) - - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - # The two sides are (anti)parallel - construct normal vector (bisector) manually: - # If we offset by half we are offseting normal to the next segment - if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): - vecx = linesign_child*bisectorline_length*normalized_vector_next_y - vecy = -linesign_child*bisectorline_length*normalized_vector_next_x - - if transfer_forbidden_points: - vecx_forbidden_point = linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_x - - else: - vecx *= bisectorline_length/vec_length - vecy *= bisectorline_length/vec_length - - if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: - vecx = -vecx - vecy = -vecy - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - assert((vecx*normalized_vector_next_y-vecy * - normalized_vector_next_x)*linesign_child >= 0) - - originPoint = point_list[i] - originPoint_forbidden_point = point_list[i] - if(offset_by_half): - off = currentDistance+next_spacing/2 - if off > closed_line.length: - off -= closed_line.length - originPoint = closed_line.interpolate(off) - - bisectorline_child = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) - - bisectorline_neighbor = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy)]) - - bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - - bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) - - for child in child_list: - point, priority = calc_transferred_point(bisectorline_child, child) - if point is None: - continue - child.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for child in child_list_forbidden: - point, priority = calc_transferred_point( - bisectorline_forbidden_point_child, child) - if point is None: - continue - child.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - for neighbor in neighbor_list: - point, priority = calc_transferred_point( - bisectorline_neighbor, neighbor) - if point is None: - continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for neighbor in neighbor_list_forbidden: - point, priority = calc_transferred_point( - bisectorline_forbidden_point_neighbor, neighbor) - if point is None: - continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - i += 1 - currentDistance += next_spacing - - assert(len(point_list) == len(point_source_list)) - - -# Calculates the nearest interserction point of "bisectorline" with the coordinates of child. -# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -# intersection was found. -def calc_transferred_point_graph(bisectorline, edge_geometry): - result = bisectorline.intersection(edge_geometry) - if result.is_empty: - return None, None - desired_point = Point() - if result.geom_type == 'Point': - desired_point = result - elif result.geom_type == 'LineString': - desired_point = Point(result.coords[0]) - else: - resultlist = list(result) - desired_point = resultlist[0] - if len(resultlist) > 1: - desired_point = nearest_points( - result, Point(bisectorline.coords[0]))[0] - - priority = edge_geometry.project(desired_point) - point = desired_point - return point, priority - - -def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 - overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): - """ - Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) - To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. - Input: - -fill_stitch_graph: Graph data structure of the stitching lines - -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered - -used_offset: The used offset when the curves where offsetted - -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" - -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points - can be handled as closed ring - -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) - -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as - forbidden points to the neighbor to avoid a point placing there - -transfer_to_previous: If True, points will be transferred to the previous edge in the graph - -transfer_to_next: If True, points will be transferred to the next edge in the graph - Output: - -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - """ - - assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and ( - offset_by_half or not offset_by_half and overnext_neighbor)) - - if len(to_transfer_points) == 0: - return - - # Take only neighbors which have not rastered before - # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) - previous_edge_list = [] - previous_edge_list_forbidden = [] - next_edge_list = [] - next_edge_list_forbidden = [] - - if transfer_to_previous: - previous_neighbors_tuples = current_edge['previous_neighbors'] - for neighbor in previous_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0] - ][neighbor[-1]]['segment'] - if not neighbor_edge['already_rastered']: - if not overnext_neighbor: - previous_edge_list.append(neighbor_edge) - if transfer_forbidden_points: - previous_edge_list_forbidden.append(neighbor_edge) - if overnext_neighbor: - overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] - for overnext_neighbor in overnext_previous_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] - ][overnext_neighbor[-1]]['segment'] - if not overnext_neighbor_edge['already_rastered']: - previous_edge_list.append(overnext_neighbor_edge) - - if transfer_to_next: - next_neighbors_tuples = current_edge['next_neighbors'] - for neighbor in next_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0] - ][neighbor[-1]]['segment'] - if not neighbor_edge['already_rastered']: - if not overnext_neighbor: - next_edge_list.append(neighbor_edge) - if transfer_forbidden_points: - next_edge_list_forbidden.append(neighbor_edge) - if overnext_neighbor: - overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] - for overnext_neighbor in overnext_next_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] - ][overnext_neighbor[-1]]['segment'] - if not overnext_neighbor_edge['already_rastered']: - next_edge_list.append(overnext_neighbor_edge) - - if not previous_edge_list and not next_edge_list: - return - - # Go through all rastered points of treenode and check where they should be transferred to its neighbar - point_list = list(MultiPoint(to_transfer_points)) - line = LineString(to_transfer_points) - - bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor * \ - (2.0 if overnext_neighbor else 1.0) - - bisectorline_length_forbidden_points = abs(used_offset) * \ - constants.transfer_point_distance_factor - - linesign_child = math.copysign(1, used_offset) - - i = 0 - currentDistance = 0 - while i < len(point_list): - - # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: - # print("HIIIIIIIIIIIERRR") - - # We create a bisecting line through the current point - normalized_vector_prev_x = ( - point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape - normalized_vector_prev_y = ( - point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) - prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + - normalized_vector_prev_y*normalized_vector_prev_y) - - # if prev_spacing == 0: - # print("HIER FEHLER") - - normalized_vector_prev_x /= prev_spacing - normalized_vector_prev_y /= prev_spacing - - normalized_vector_next_x = normalized_vector_next_y = 0 - next_spacing = 0 - while True: - normalized_vector_next_x = ( - point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) - normalized_vector_next_y = ( - point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) - next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + - normalized_vector_next_y*normalized_vector_next_y) - if next_spacing < constants.line_lengh_seen_as_one_point: - point_list.pop(i) - currentDistance += next_spacing - continue - - normalized_vector_next_x /= next_spacing - normalized_vector_next_y /= next_spacing - break - - vecx = (normalized_vector_next_x+normalized_vector_prev_x) - vecy = (normalized_vector_next_y+normalized_vector_prev_y) - vec_length = math.sqrt(vecx*vecx+vecy*vecy) - - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - # The two sides are (anti)parallel - construct normal vector (bisector) manually: - # If we offset by half we are offseting normal to the next segment - if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): - vecx = linesign_child*bisectorline_length*normalized_vector_next_y - vecy = -linesign_child*bisectorline_length*normalized_vector_next_x - - if transfer_forbidden_points: - vecx_forbidden_point = linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_x - - else: - vecx *= bisectorline_length/vec_length - vecy *= bisectorline_length/vec_length - - if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: - vecx = -vecx - vecy = -vecy - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - assert((vecx*normalized_vector_next_y-vecy * - normalized_vector_next_x)*linesign_child >= 0) - - originPoint = point_list[i] - originPoint_forbidden_point = point_list[i] - if(offset_by_half): - off = currentDistance+next_spacing/2 - if off > line.length: - break - originPoint = line.interpolate(off) - - bisectorline = LineString([(originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) - - bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - - for edge in previous_edge_list+next_edge_list: - point, priority = calc_transferred_point_graph( - bisectorline, edge['geometry']) - if point is None: - continue - edge['projected_points'].insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: - point, priority = calc_transferred_point_graph( - bisectorline_forbidden_point, edge_forbidden['geometry']) - if point is None: - continue - edge_forbidden['projected_points'].insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - i += 1 - currentDistance += next_spacing diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py deleted file mode 100644 index 4a38c0bc..00000000 --- a/lib/stitches/StitchPattern.py +++ /dev/null @@ -1,420 +0,0 @@ -from anytree.render import RenderTree -from shapely.geometry.polygon import LinearRing, LineString -from shapely.geometry import Polygon, MultiLineString -from shapely.ops import polygonize -from shapely.geometry import MultiPolygon -from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter -from shapely.geometry.polygon import orient -from depq import DEPQ -from enum import IntEnum -from ..stitches import ConnectAndSamplePattern -from ..stitches import constants - - -def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): - """ - Solves following problem: When shapely offsets a LinearRing the - start/end point might be handled wrongly since they - are only treated as LineString. - (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) - This method checks first whether the start/end point form a problematic - edge with respect to the offset side. If it is not a problematic - edge we can use the normal offset_routine. Otherwise we need to - perform two offsets: - -offset the ring - -offset the start/end point + its two neighbors left and right - Finally both offsets are merged together to get the correct - offset of a LinearRing - """ - - coords = ring.coords[:] - # check whether edge at index 0 is concave or convex. Only for - # concave edges we need to spend additional effort - dx_seg1 = dy_seg1 = 0 - if coords[0] != coords[-1]: - dx_seg1 = coords[0][0] - coords[-1][0] - dy_seg1 = coords[0][1] - coords[-1][1] - else: - dx_seg1 = coords[0][0] - coords[-2][0] - dy_seg1 = coords[0][1] - coords[-2][1] - dx_seg2 = coords[1][0] - coords[0][0] - dy_seg2 = coords[1][1] - coords[0][1] - # use cross product: - crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 - sidesign = 1 - if side == "left": - sidesign = -1 - - # We do not need to take care of the joint n-0 since we - # offset along a concave edge: - if sidesign * offset * crossvalue <= 0: - return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) - - # We offset along a convex edge so we offset the joint n-0 separately: - if coords[0] != coords[-1]: - coords.append(coords[0]) - offset_ring1 = ring.parallel_offset( - offset, side, resolution, join_style, mitre_limit - ) - offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( - offset, side, resolution, join_style, mitre_limit - ) - - # Next we need to merge the results: - if offset_ring1.geom_type == "LineString": - return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) - else: - # We have more than one resulting LineString for offset of - # the geometry (ring) = offset_ring1. - # Hence we need to find the LineString which belongs to the - # offset of element 0 in coords =offset_ring2 - # in order to add offset_ring2 geometry to it: - result_list = [] - thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) - for offsets in offset_ring1: - if ( - abs(offsets.coords[0][0] - coords[0][0]) < thresh - and abs(offsets.coords[0][1] - coords[0][1]) < thresh - ): - result_list.append( - LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) - ) - else: - result_list.append(LinearRing(offsets)) - return MultiLineString(result_list) - - -def take_only_valid_linear_rings(rings): - """ - Removes all geometries which do not form a "valid" LinearRing - (meaning a ring which does not form a straight line) - """ - if rings.geom_type == "MultiLineString": - new_list = [] - for ring in rings: - if len(ring.coords) > 3 or ( - len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] - ): - new_list.append(ring) - if len(new_list) == 1: - return LinearRing(new_list[0]) - else: - return MultiLineString(new_list) - else: - if len(rings.coords) <= 2: - return LinearRing() - elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]: - return LinearRing() - else: - return rings - - -def make_tree_uniform_ccw(root): - """ - Since naturally holes have the opposite point ordering than non-holes we - make all lines within the tree "root" uniform (having all the same - ordering direction) - """ - for node in PreOrderIter(root): - if node.id == "hole": - node.val.coords = list(node.val.coords)[::-1] - - -# Used to define which stitching strategy shall be used -class StitchingStrategy(IntEnum): - CLOSEST_POINT = 0 - INNER_TO_OUTER = 1 - SPIRAL = 2 - - -def check_and_prepare_tree_for_valid_spiral(root): - """ - Takes a tree consisting of offsetted curves. If a parent has more than one child we - cannot create a spiral. However, to make the routine more robust, we allow more than - one child if only one of the childs has own childs. The other childs are removed in this - routine then. If the routine returns true, the tree will have been cleaned up from unwanted - childs. If the routine returns false even under the mentioned weaker conditions the - tree cannot be connected by one spiral. - """ - for children in LevelOrderGroupIter(root): - if len(children) > 1: - count = 0 - child_with_children = None - for child in children: - if not child.is_leaf: - count += 1 - child_with_children = child - if count > 1: - return False - elif count == 1: - child_with_children.parent.children = [child_with_children] - else: # count == 0 means all childs have no children so we take only the longest child - max_length = 0 - longest_child = None - for child in children: - if child.val.length > max_length: - max_length = child.val.length - longest_child = child - longest_child.parent.children = [longest_child] - return True - - -def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 - """ - Takes a polygon (which can have holes) as input and creates offsetted - versions until the polygon is filled with these smaller offsets. - These created geometries are afterwards connected to each other and - resampled with a maximum stitch_distance. - The return value is a LineString which should cover the full polygon. - Input: - -poly: The shapely polygon which can have holes - -offset: The used offset for the curves - -join_style: Join style for the offset - can be round, mitered or bevel - (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) - For examples look at - https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png - -stitch_distance maximum allowed stitch distance between two points - -offset_by_half: True if the points shall be interlaced - -strategy: According to StitchingStrategy enum class you can select between - different strategies for the connection between parent and childs. In - addition it offers the option "SPIRAL" which creates a real spiral towards inner. - In contrast to the other two options, "SPIRAL" does not end at the starting point - but at the innermost point - -starting_point: Defines the starting point for the stitching - Output: - -List of point coordinate tuples - -Tag (origin) of each point to analyze why a point was placed - at this position - """ - - if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: - raise ValueError( - "Single spiral geometry must not have more than one hole!") - - ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify( - constants.simplification_threshold, False) - root = AnyNode( - id="node", - val=ordered_poly.exterior, - already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), - ) - active_polys = [root] - active_holes = [[]] - - for holes in ordered_poly.interiors: - active_holes[0].append( - AnyNode( - id="hole", - val=holes, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None), - ) - ) - - while len(active_polys) > 0: - current_poly = active_polys.pop() - current_holes = active_holes.pop() - poly_inners = [] - - outer = offset_linear_ring( - current_poly.val, - offset, - "left", - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - outer = outer.simplify(constants.simplification_threshold, False) - outer = take_only_valid_linear_rings(outer) - - for j in range(len(current_holes)): - inner = offset_linear_ring( - current_holes[j].val, - offset, - "left", - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - inner = inner.simplify(constants.simplification_threshold, False) - inner = take_only_valid_linear_rings(inner) - if not inner.is_empty: - poly_inners.append(Polygon(inner)) - if not outer.is_empty: - if len(poly_inners) == 0: - if outer.geom_type == "LineString": - result = Polygon(outer) - else: - result = MultiPolygon(polygonize(outer)) - else: - if outer.geom_type == "LineString": - result = Polygon(outer).difference( - MultiPolygon(poly_inners)) - else: - result = MultiPolygon(outer).difference( - MultiPolygon(poly_inners)) - - if not result.is_empty and result.area > offset * offset / 10: - result_list = [] - if result.geom_type == "Polygon": - result_list = [result] - else: - result_list = list(result) - - for polygon in result_list: - polygon = orient(polygon, -1) - - if polygon.area < offset * offset / 10: - continue - - polygon = polygon.simplify( - constants.simplification_threshold, False - ) - poly_coords = polygon.exterior - poly_coords = take_only_valid_linear_rings(poly_coords) - if poly_coords.is_empty: - continue - - node = AnyNode( - id="node", - parent=current_poly, - val=poly_coords, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None - ), - ) - active_polys.append(node) - hole_node_list = [] - for hole in polygon.interiors: - hole_node = AnyNode( - id="hole", - val=hole, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None - ), - ) - for previous_hole in current_holes: - if Polygon(hole).contains(Polygon(previous_hole.val)): - previous_hole.parent = hole_node - hole_node_list.append(hole_node) - active_holes.append(hole_node_list) - for previous_hole in current_holes: - # If the previous holes are not - # contained in the new holes they - # have been merged with the - # outer polygon - if previous_hole.parent is None: - previous_hole.parent = current_poly - - # DebuggingMethods.drawPoly(root, 'r-') - - make_tree_uniform_ccw(root) - # print(RenderTree(root)) - if strategy == StitchingStrategy.CLOSEST_POINT: - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( - root, offset, stitch_distance, starting_point, offset_by_half) - elif strategy == StitchingStrategy.INNER_TO_OUTER: - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( - root, offset, stitch_distance, starting_point, offset_by_half) - elif strategy == StitchingStrategy.SPIRAL: - if not check_and_prepare_tree_for_valid_spiral(root): - raise ValueError("Geometry cannot be filled with one spiral!") - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_spiral( - root, offset, stitch_distance, starting_point, offset_by_half) - else: - raise ValueError("Invalid stitching stratety!") - - return connected_line, connected_line_origin - - -if __name__ == "__main__": - line1 = LineString([(0, 0), (1, 0)]) - line2 = LineString([(0, 0), (3, 0)]) - - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=root) - - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=child1) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=child1) - child3 = AnyNode( - id="node", - val=line2, - parent=child2) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=child1) - child4 = AnyNode( - id="node", - val=line2, - parent=child2) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index b63f4be1..7af99560 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -20,8 +20,8 @@ from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import line_string_to_point_list from .fill import intersect_region_with_grating, intersect_region_with_grating_line, stitch_row from .running_stitch import running_stitch -from .PointTransfer import transfer_points_to_surrounding_graph -from .LineStringSampling import raster_line_string_with_priority_points +from .point_transfer import transfer_points_to_surrounding_graph +from .sample_linestring import raster_line_string_with_priority_points class PathEdge(object): @@ -165,10 +165,6 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st for i in range(len(rows_of_segments)): for segment in rows_of_segments[i]: - # if abs(segment[0][0]-396.5081896849414) < 0.01: - # print("HIER") - # if segment[0][0] == segment[-1][0] and segment[0][1] == segment[-1][1]: - # print("FEHLER HIER!") # First, add the grating segments as edges. We'll use the coordinates # of the endpoints as nodes, which networkx will add automatically. @@ -666,10 +662,6 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): - # print(start_point) - # print(geometry[0]) - # if stitching_direction == -1: - # geometry.coords = geometry.coords[::-1] if stitching_direction == 1: stitched_line, _ = raster_line_string_with_priority_points( geometry, 0.0, geometry.length, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) @@ -688,8 +680,6 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s else: stitches.append( Stitch(*geometry.coords[0], tags=('fill_row_end',))) - # if stitches[-1].x == stitches[-2].x and stitches[-1].y == stitches[-2].y: - # print("FEHLER") @debug.time diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py index 162c4cfb..012fac7c 100644 --- a/lib/stitches/constants.py +++ b/lib/stitches/constants.py @@ -3,10 +3,6 @@ import math # Used in the simplify routine of shapely simplification_threshold = 0.01 -# If a transferred point is closer than this value to one of its neighbors, -# it will be checked whether it can be removed -distance_thresh_remove_transferred_point = 0.15 - # If a line segment is shorter than this threshold it is handled as a single point line_lengh_seen_as_one_point = 0.05 @@ -35,12 +31,6 @@ factor_offset_starting_points = 0.5 # if points are closer than abs_offset*factor_offset_remove_points one of it is removed factor_offset_remove_points = 0.5 -# if an unshifted relevant edge is closer than -# abs_offset*fac_offset_edge_shift -# to the line segment created by the shifted edge, -# the shift is allowed - otherwise the edge must not be shifted. -fac_offset_edge_shift = 0.25 - # decides whether the point belongs to a hard edge (must use this point during sampling) # or soft edge (do not necessarily need to use this point) limiting_angle = math.pi * 15 / 180.0 diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index ceac56d9..b5f86641 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -176,8 +176,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing rows.append(runs) else: rows.insert(0, runs) - # if len(runs) > 1: - # print("HIERRRR!") + line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( @@ -192,7 +191,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0 and not isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): if (res.is_empty or len(res.coords) == 1): row_spacing = -row_spacing - # print("Set to right") + line_offsetted = line.parallel_offset(row_spacing, 'left', 5) if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( @@ -203,8 +202,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - # if res.geom_type != 'LineString': - # print("HIER!!") + return rows diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py new file mode 100644 index 00000000..a01e69cd --- /dev/null +++ b/lib/stitches/point_transfer.py @@ -0,0 +1,495 @@ +from shapely.geometry import Point, MultiPoint +from shapely.geometry.polygon import LineString, LinearRing +from collections import namedtuple +from shapely.ops import nearest_points +import math +from ..stitches import constants +from ..stitches import sample_linestring + +"""This file contains routines which shall project already selected points for stitching to remaining +unstitched lines in the neighborhood to create a regular pattern of points.""" + +projected_point_tuple = namedtuple( + 'projected_point_tuple', ['point', 'point_source']) + + +def calc_transferred_point(bisectorline, child): + """ + Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). + It returns the intersection point and its distance along the coordinates of the child or "None, None" if no + intersection was found. + """ + result = bisectorline.intersection(child.val) + if result.is_empty: + return None, None + desired_point = Point() + if result.geom_type == 'Point': + desired_point = result + elif result.geom_type == 'LineString': + desired_point = Point(result.coords[0]) + else: + resultlist = list(result) + desired_point = resultlist[0] + if len(resultlist) > 1: + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] + + priority = child.val.project(desired_point) + point = desired_point + return point, priority + + +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 + overnext_neighbor=False, transfer_forbidden_points=False, + transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): + """ + Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -to_transfer_points_origin: The origin tag of each point in to_transfer_points + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_parent: If True, points will be transferred to the parent + -transfer_to_sibling: If True, points will be transferred to the siblings + -transfer_to_child: If True, points will be transferred to the childs + Output: + -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert(len(to_transfer_points) == len(to_transfer_points_origin) + or len(to_transfer_points_origin) == 0) + assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) + + if len(to_transfer_points) == 0: + return + + # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: + childs_tuple = treenode.children + siblings_tuple = treenode.siblings + # Take only neighbors which have not rastered before + # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) + child_list = [] + child_list_forbidden = [] + neighbor_list = [] + neighbor_list_forbidden = [] + + if transfer_to_child: + for child in childs_tuple: + if not child.already_rastered: + if not overnext_neighbor: + child_list.append(child) + if transfer_forbidden_points: + child_list_forbidden.append(child) + if overnext_neighbor: + for subchild in child.children: + if not subchild.already_rastered: + child_list.append(subchild) + + if transfer_to_sibling: + for sibling in siblings_tuple: + if not sibling.already_rastered: + if not overnext_neighbor: + neighbor_list.append(sibling) + if transfer_forbidden_points: + neighbor_list_forbidden.append(sibling) + if overnext_neighbor: + for subchild in sibling.children: + if not subchild.already_rastered: + neighbor_list.append(subchild) + + if transfer_to_parent and treenode.parent is not None: + if not treenode.parent.already_rastered: + if not overnext_neighbor: + neighbor_list.append(treenode.parent) + if transfer_forbidden_points: + neighbor_list_forbidden.append(treenode.parent) + if overnext_neighbor: + if treenode.parent.parent is not None: + if not treenode.parent.parent.already_rastered: + neighbor_list.append(treenode.parent.parent) + + if not neighbor_list and not child_list: + return + + # Go through all rastered points of treenode and check where they should be transferred to its neighbar + point_list = list(MultiPoint(to_transfer_points)) + point_source_list = to_transfer_points_origin.copy() + + # For a linear ring the last point is the same as the starting point which we delete + # since we do not want to transfer the starting and end point twice + closed_line = LineString(to_transfer_points) + if point_list[0].distance(point_list[-1]) < constants.point_spacing_to_be_considered_equal: + point_list.pop() + if(point_source_list): + point_source_list.pop() + if len(point_list) == 0: + return + else: + # closed line is needed if we offset by half since we need to determine the line + # length including the closing segment + closed_line = LinearRing(to_transfer_points) + + bisectorline_length = abs(used_offset) * constants.transfer_point_distance_factor * (2.0 if overnext_neighbor else 1.0) + + bisectorline_length_forbidden_points = abs(used_offset) * constants.transfer_point_distance_factor + + linesign_child = math.copysign(1, used_offset) + + i = 0 + currentDistance = 0 + while i < len(point_list): + assert(point_source_list[i] != + sample_linestring.PointSource.ENTER_LEAVING_POINT) + + # We create a bisecting line through the current point + normalized_vector_prev_x = ( + point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape + normalized_vector_prev_y = ( + point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) + prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + + normalized_vector_prev_y*normalized_vector_prev_y) + + normalized_vector_prev_x /= prev_spacing + normalized_vector_prev_y /= prev_spacing + + normalized_vector_next_x = normalized_vector_next_y = 0 + next_spacing = 0 + while True: + normalized_vector_next_x = ( + point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) + normalized_vector_next_y = ( + point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) + next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + + normalized_vector_next_y*normalized_vector_next_y) + if next_spacing < constants.line_lengh_seen_as_one_point: + point_list.pop(i) + if(point_source_list): + point_source_list.pop(i) + currentDistance += next_spacing + continue + + normalized_vector_next_x /= next_spacing + normalized_vector_next_y /= next_spacing + break + + vecx = (normalized_vector_next_x+normalized_vector_prev_x) + vecy = (normalized_vector_next_y+normalized_vector_prev_y) + vec_length = math.sqrt(vecx*vecx+vecy*vecy) + + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + # The two sides are (anti)parallel - construct normal vector (bisector) manually: + # If we offset by half we are offseting normal to the next segment + if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): + vecx = linesign_child*bisectorline_length*normalized_vector_next_y + vecy = -linesign_child*bisectorline_length*normalized_vector_next_x + + if transfer_forbidden_points: + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x + + else: + vecx *= bisectorline_length/vec_length + vecy *= bisectorline_length/vec_length + + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: + vecx = -vecx + vecy = -vecy + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + assert((vecx*normalized_vector_next_y-vecy * + normalized_vector_next_x)*linesign_child >= 0) + + originPoint = point_list[i] + originPoint_forbidden_point = point_list[i] + if(offset_by_half): + off = currentDistance+next_spacing/2 + if off > closed_line.length: + off -= closed_line.length + originPoint = closed_line.interpolate(off) + + bisectorline_child = LineString([(originPoint.coords[0][0], + originPoint.coords[0][1]), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) + + bisectorline_neighbor = LineString([(originPoint.coords[0][0], + originPoint.coords[0][1]), + (originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy)]) + + bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + + bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) + + for child in child_list: + point, priority = calc_transferred_point(bisectorline_child, child) + if point is None: + continue + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for child in child_list_forbidden: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_child, child) + if point is None: + continue + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + for neighbor in neighbor_list: + point, priority = calc_transferred_point( + bisectorline_neighbor, neighbor) + if point is None: + continue + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for neighbor in neighbor_list_forbidden: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_neighbor, neighbor) + if point is None: + continue + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + i += 1 + currentDistance += next_spacing + + assert(len(point_list) == len(point_source_list)) + + +# Calculates the nearest interserction point of "bisectorline" with the coordinates of child. +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. +def calc_transferred_point_graph(bisectorline, edge_geometry): + result = bisectorline.intersection(edge_geometry) + if result.is_empty: + return None, None + desired_point = Point() + if result.geom_type == 'Point': + desired_point = result + elif result.geom_type == 'LineString': + desired_point = Point(result.coords[0]) + else: + resultlist = list(result) + desired_point = resultlist[0] + if len(resultlist) > 1: + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] + + priority = edge_geometry.project(desired_point) + point = desired_point + return point, priority + + +def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 + overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): + """ + Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -fill_stitch_graph: Graph data structure of the stitching lines + -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_previous: If True, points will be transferred to the previous edge in the graph + -transfer_to_next: If True, points will be transferred to the next edge in the graph + Output: + -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) + + if len(to_transfer_points) == 0: + return + + # Take only neighbors which have not rastered before + # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) + previous_edge_list = [] + previous_edge_list_forbidden = [] + next_edge_list = [] + next_edge_list_forbidden = [] + + if transfer_to_previous: + previous_neighbors_tuples = current_edge['previous_neighbors'] + for neighbor in previous_neighbors_tuples: + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] + if not neighbor_edge['already_rastered']: + if not overnext_neighbor: + previous_edge_list.append(neighbor_edge) + if transfer_forbidden_points: + previous_edge_list_forbidden.append(neighbor_edge) + if overnext_neighbor: + overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] + for overnext_neighbor in overnext_previous_neighbors_tuples: + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] + if not overnext_neighbor_edge['already_rastered']: + previous_edge_list.append(overnext_neighbor_edge) + + if transfer_to_next: + next_neighbors_tuples = current_edge['next_neighbors'] + for neighbor in next_neighbors_tuples: + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] + if not neighbor_edge['already_rastered']: + if not overnext_neighbor: + next_edge_list.append(neighbor_edge) + if transfer_forbidden_points: + next_edge_list_forbidden.append(neighbor_edge) + if overnext_neighbor: + overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] + for overnext_neighbor in overnext_next_neighbors_tuples: + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] + if not overnext_neighbor_edge['already_rastered']: + next_edge_list.append(overnext_neighbor_edge) + + if not previous_edge_list and not next_edge_list: + return + + # Go through all rastered points of treenode and check where they should be transferred to its neighbar + point_list = list(MultiPoint(to_transfer_points)) + line = LineString(to_transfer_points) + + bisectorline_length = abs(used_offset) * \ + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) + + bisectorline_length_forbidden_points = abs(used_offset) * \ + constants.transfer_point_distance_factor + + linesign_child = math.copysign(1, used_offset) + + i = 0 + currentDistance = 0 + while i < len(point_list): + + # We create a bisecting line through the current point + normalized_vector_prev_x = ( + point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape + normalized_vector_prev_y = ( + point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) + prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + + normalized_vector_prev_y*normalized_vector_prev_y) + + normalized_vector_prev_x /= prev_spacing + normalized_vector_prev_y /= prev_spacing + + normalized_vector_next_x = normalized_vector_next_y = 0 + next_spacing = 0 + while True: + normalized_vector_next_x = ( + point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) + normalized_vector_next_y = ( + point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) + next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + + normalized_vector_next_y*normalized_vector_next_y) + if next_spacing < constants.line_lengh_seen_as_one_point: + point_list.pop(i) + currentDistance += next_spacing + continue + + normalized_vector_next_x /= next_spacing + normalized_vector_next_y /= next_spacing + break + + vecx = (normalized_vector_next_x+normalized_vector_prev_x) + vecy = (normalized_vector_next_y+normalized_vector_prev_y) + vec_length = math.sqrt(vecx*vecx+vecy*vecy) + + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + # The two sides are (anti)parallel - construct normal vector (bisector) manually: + # If we offset by half we are offseting normal to the next segment + if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): + vecx = linesign_child*bisectorline_length*normalized_vector_next_y + vecy = -linesign_child*bisectorline_length*normalized_vector_next_x + + if transfer_forbidden_points: + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x + + else: + vecx *= bisectorline_length/vec_length + vecy *= bisectorline_length/vec_length + + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: + vecx = -vecx + vecy = -vecy + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + assert((vecx*normalized_vector_next_y-vecy * + normalized_vector_next_x)*linesign_child >= 0) + + originPoint = point_list[i] + originPoint_forbidden_point = point_list[i] + if(offset_by_half): + off = currentDistance+next_spacing/2 + if off > line.length: + break + originPoint = line.interpolate(off) + + bisectorline = LineString([(originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) + + bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + + for edge in previous_edge_list+next_edge_list: + point, priority = calc_transferred_point_graph( + bisectorline, edge['geometry']) + if point is None: + continue + edge['projected_points'].insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: + point, priority = calc_transferred_point_graph( + bisectorline_forbidden_point, edge_forbidden['geometry']) + if point is None: + continue + edge_forbidden['projected_points'].insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + i += 1 + currentDistance += next_spacing diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py new file mode 100644 index 00000000..fb4bbc52 --- /dev/null +++ b/lib/stitches/sample_linestring.py @@ -0,0 +1,325 @@ +from shapely.geometry.polygon import LineString +from shapely.geometry import Point +from shapely.ops import substring +import math +import numpy as np +from enum import IntEnum +from ..stitches import constants +from ..stitches import point_transfer + + +class PointSource(IntEnum): + """ + Used to tag the origin of a rastered point + """ + # MUST_USE = 0 # Legacy + REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance + # INITIAL_RASTERING = 2 #Legacy + # point which must be stitched to avoid to large deviations to the desired path + EDGE_NEEDED = 3 + # NOT_NEEDED = 4 #Legacy + # ALREADY_TRANSFERRED = 5 #Legacy + # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy + # EDGE_RASTERING_ALLOWED = 7 #Legacy + # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy + ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child + # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + SOFT_EDGE_INTERNAL = 10 + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE_INTERNAL = 11 + # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT + PROJECTED_POINT = 12 + REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance + # FORBIDDEN_POINT_INTERNAL=14 #Legacy + SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE = 16 + FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden + # If one decides to avoid forbidden points new points to the left and to the right as replacement are created + REPLACED_FORBIDDEN_POINT = 18 + DIRECT = 19 # Calculated by next neighbor projection + OVERNEXT = 20 # Calculated by overnext neighbor projection + + +def calculate_line_angles(line): + """ + Calculates the angles between adjacent edges at each interior point + Note that the first and last values in the return array are zero since for the boundary points no + angle calculations were possible + """ + Angles = np.zeros(len(line.coords)) + for i in range(1, len(line.coords)-1): + vec1 = np.array(line.coords[i])-np.array(line.coords[i-1]) + vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) + vec1length = np.linalg.norm(vec1) + vec2length = np.linalg.norm(vec2) + + assert(vec1length > 0) + assert(vec2length > 0) + scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) + scalar_prod = min(max(scalar_prod, -1), 1) + + Angles[i] = math.acos(scalar_prod) + return Angles + + +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 + must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): + """ + Rasters a line between start_distance and end_distance. + Input: + -line: The line to be rastered + -start_distance: The distance along the line from which the rastering should start + -end_distance: The distance along the line until which the rastering should be done + -maxstitch_distance: The maximum allowed stitch distance + -Note that start_distance > end_distance for stitching_direction = -1 + -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + -abs_offset: used offset between to offsetted curves + -offset_by_half: Whether the points of neighboring lines shall be interlaced or not + -replace_forbidden_points: Whether points marked as forbidden in must_use_points_deque shall be replaced by adjacend points + Output: + -List of tuples with the rastered point coordinates + -List which defines the point origin for each point according to the PointSource enum. + """ + + if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): + return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] + + deque_points = list(must_use_points_deque) + + linecoords = line.coords + + if start_distance > end_distance: + start_distance, end_distance = line.length - \ + start_distance, line.length-end_distance + linecoords = linecoords[::-1] + for i in range(len(deque_points)): + deque_points[i] = (deque_points[i][0], + line.length-deque_points[i][1]) + else: + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] + + # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] + while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): + deque_points.pop(0) + while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): + deque_points.pop() + + +# Ordering in priority queue: +# (point, LineStringSampling.PointSource), priority) + # might be different from line for stitching_direction=-1 + aligned_line = LineString(linecoords) + path_coords = substring(aligned_line, + start_distance, end_distance) + + # aligned line is a line without doubled points. + # I had the strange situation in which the offset "start_distance" from the line beginning + # resulted in a starting point which was already present in aligned_line causing a doubled point. + # A double point is not allowed in the following calculations so we need to remove it: + if (abs(path_coords.coords[0][0]-path_coords.coords[1][0]) < constants.eps and + abs(path_coords.coords[0][1]-path_coords.coords[1][1]) < constants.eps): + path_coords.coords = path_coords.coords[1:] + if (abs(path_coords.coords[-1][0]-path_coords.coords[-2][0]) < constants.eps and + abs(path_coords.coords[-1][1]-path_coords.coords[-2][1]) < constants.eps): + path_coords.coords = path_coords.coords[:-1] + + angles = calculate_line_angles(path_coords) + # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge + angles[0] = 1.1*constants.limiting_angle + angles[-1] = 1.1*constants.limiting_angle + + current_distance = 0 + last_point = Point(path_coords.coords[0]) + # Next we merge the line points and the projected (deque) points into one list + merged_point_list = [] + dq_iter = 0 + for point, angle in zip(path_coords.coords, angles): + current_distance += last_point.distance(Point(point)) + last_point = Point(point) + while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance+start_distance: + # We want to avoid setting points at soft edges close to forbidden points + if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: + # Check whether a previous added point is a soft edge close to the forbidden point + if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and + abs(merged_point_list[-1][1]-deque_points[dq_iter][1]+start_distance < abs_offset*constants.factor_offset_forbidden_point)): + item = merged_point_list.pop() + merged_point_list.append((point_transfer.projected_point_tuple( + point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1]-start_distance)) + else: + merged_point_list.append( + (deque_points[dq_iter][0], deque_points[dq_iter][1]-start_distance)) + # merged_point_list.append(deque_points[dq_iter]) + dq_iter += 1 + # Check whether the current point is close to a forbidden point + if (dq_iter < len(deque_points) and + deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and + angle < constants.limiting_angle and + abs(deque_points[dq_iter-1][1]-current_distance-start_distance) < abs_offset*constants.factor_offset_forbidden_point): + point_source = PointSource.FORBIDDEN_POINT + else: + if angle < constants.limiting_angle: + point_source = PointSource.SOFT_EDGE_INTERNAL + else: + point_source = PointSource.HARD_EDGE_INTERNAL + merged_point_list.append((point_transfer.projected_point_tuple( + point=Point(point), point_source=point_source), current_distance)) + + result_list = [merged_point_list[0]] + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment + # and start point for the next segment (so we do not always take the maximum possible length for a segment) + segment_start_index = 0 + segment_end_index = 1 + forbidden_point_list = [] + while segment_end_index < len(merged_point_list): + # Collection of points for the current segment + current_point_list = [merged_point_list[segment_start_index][0].point] + + while segment_end_index < len(merged_point_list): + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] + if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (point_transfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + segment_end_index += 1 + break + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it + break + + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 + break + segment_end_index += 1 + + segment_end_index -= 1 + + # Now we choose the best fitting point within this segment + index_overnext = -1 + index_direct = -1 + index_hard_edge = -1 + + iter = segment_start_index+1 + while (iter <= segment_end_index): + if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: + index_overnext = iter + elif merged_point_list[iter][0].point_source == PointSource.DIRECT: + index_direct = iter + elif merged_point_list[iter][0].point_source == PointSource.HARD_EDGE_INTERNAL: + index_hard_edge = iter + iter += 1 + if index_hard_edge != -1: + segment_end_index = index_hard_edge + else: + if offset_by_half: + index_preferred = index_overnext + index_less_preferred = index_direct + else: + index_preferred = index_direct + index_less_preferred = index_overnext + + if index_preferred != -1: + if (index_less_preferred != -1 and index_less_preferred > index_preferred and + (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * + (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length + segment_end_index = index_less_preferred + else: + segment_end_index = index_preferred + elif index_less_preferred != -1: + segment_end_index = index_less_preferred + + # Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges + # If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: + new_point_left_proj = result_list[index][1]-distance_left + if new_point_left_proj < 0: + new_point_left_proj += line.length + new_point_right_proj = result_list[index][1]+distance_right + if new_point_right_proj > line.length: + new_point_right_proj -= line.length + point_left = line.interpolate(new_point_left_proj) + point_right = line.interpolate(new_point_right_proj) + forbidden_point_distance = result_list[index][0].point.distance( + LineString([point_left, point_right])) + if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: + del result_list[index] + result_list.insert(index, (point_transfer.projected_point_tuple( + point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) + result_list.insert(index, (point_transfer.projected_point_tuple( + point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) + current_index_shift += 1 + break + else: + distance_left /= 2.0 + distance_right /= 2.0 + return result_list diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py new file mode 100644 index 00000000..af14ea0f --- /dev/null +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -0,0 +1,330 @@ +from shapely.geometry.polygon import LinearRing, LineString +from shapely.geometry import Polygon, MultiLineString +from shapely.ops import polygonize +from shapely.geometry import MultiPolygon +from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter +from shapely.geometry.polygon import orient +from depq import DEPQ +from enum import IntEnum +from ..stitches import tangential_fill_stitch_pattern_creator +from ..stitches import constants + + +def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): + """ + Solves following problem: When shapely offsets a LinearRing the + start/end point might be handled wrongly since they + are only treated as LineString. + (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) + This method checks first whether the start/end point form a problematic + edge with respect to the offset side. If it is not a problematic + edge we can use the normal offset_routine. Otherwise we need to + perform two offsets: + -offset the ring + -offset the start/end point + its two neighbors left and right + Finally both offsets are merged together to get the correct + offset of a LinearRing + """ + + coords = ring.coords[:] + # check whether edge at index 0 is concave or convex. Only for + # concave edges we need to spend additional effort + dx_seg1 = dy_seg1 = 0 + if coords[0] != coords[-1]: + dx_seg1 = coords[0][0] - coords[-1][0] + dy_seg1 = coords[0][1] - coords[-1][1] + else: + dx_seg1 = coords[0][0] - coords[-2][0] + dy_seg1 = coords[0][1] - coords[-2][1] + dx_seg2 = coords[1][0] - coords[0][0] + dy_seg2 = coords[1][1] - coords[0][1] + # use cross product: + crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 + sidesign = 1 + if side == "left": + sidesign = -1 + + # We do not need to take care of the joint n-0 since we + # offset along a concave edge: + if sidesign * offset * crossvalue <= 0: + return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) + + # We offset along a convex edge so we offset the joint n-0 separately: + if coords[0] != coords[-1]: + coords.append(coords[0]) + offset_ring1 = ring.parallel_offset( + offset, side, resolution, join_style, mitre_limit + ) + offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( + offset, side, resolution, join_style, mitre_limit + ) + + # Next we need to merge the results: + if offset_ring1.geom_type == "LineString": + return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) + else: + # We have more than one resulting LineString for offset of + # the geometry (ring) = offset_ring1. + # Hence we need to find the LineString which belongs to the + # offset of element 0 in coords =offset_ring2 + # in order to add offset_ring2 geometry to it: + result_list = [] + thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) + for offsets in offset_ring1: + if ( + abs(offsets.coords[0][0] - coords[0][0]) < thresh + and abs(offsets.coords[0][1] - coords[0][1]) < thresh + ): + result_list.append( + LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) + ) + else: + result_list.append(LinearRing(offsets)) + return MultiLineString(result_list) + + +def take_only_valid_linear_rings(rings): + """ + Removes all geometries which do not form a "valid" LinearRing + (meaning a ring which does not form a straight line) + """ + if rings.geom_type == "MultiLineString": + new_list = [] + for ring in rings: + if len(ring.coords) > 3 or ( + len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] + ): + new_list.append(ring) + if len(new_list) == 1: + return LinearRing(new_list[0]) + else: + return MultiLineString(new_list) + else: + if len(rings.coords) <= 2: + return LinearRing() + elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]: + return LinearRing() + else: + return rings + + +def make_tree_uniform_ccw(root): + """ + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree "root" uniform (having all the same + ordering direction) + """ + for node in PreOrderIter(root): + if node.id == "hole": + node.val.coords = list(node.val.coords)[::-1] + + +# Used to define which stitching strategy shall be used +class StitchingStrategy(IntEnum): + CLOSEST_POINT = 0 + INNER_TO_OUTER = 1 + SPIRAL = 2 + + +def check_and_prepare_tree_for_valid_spiral(root): + """ + Takes a tree consisting of offsetted curves. If a parent has more than one child we + cannot create a spiral. However, to make the routine more robust, we allow more than + one child if only one of the childs has own childs. The other childs are removed in this + routine then. If the routine returns true, the tree will have been cleaned up from unwanted + childs. If the routine returns false even under the mentioned weaker conditions the + tree cannot be connected by one spiral. + """ + for children in LevelOrderGroupIter(root): + if len(children) > 1: + count = 0 + child_with_children = None + for child in children: + if not child.is_leaf: + count += 1 + child_with_children = child + if count > 1: + return False + elif count == 1: + child_with_children.parent.children = [child_with_children] + else: # count == 0 means all childs have no children so we take only the longest child + max_length = 0 + longest_child = None + for child in children: + if child.val.length > max_length: + max_length = child.val.length + longest_child = child + longest_child.parent.children = [longest_child] + return True + + +def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 + """ + Takes a polygon (which can have holes) as input and creates offsetted + versions until the polygon is filled with these smaller offsets. + These created geometries are afterwards connected to each other and + resampled with a maximum stitch_distance. + The return value is a LineString which should cover the full polygon. + Input: + -poly: The shapely polygon which can have holes + -offset: The used offset for the curves + -join_style: Join style for the offset - can be round, mitered or bevel + (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) + For examples look at + https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png + -stitch_distance maximum allowed stitch distance between two points + -offset_by_half: True if the points shall be interlaced + -strategy: According to StitchingStrategy enum class you can select between + different strategies for the connection between parent and childs. In + addition it offers the option "SPIRAL" which creates a real spiral towards inner. + In contrast to the other two options, "SPIRAL" does not end at the starting point + but at the innermost point + -starting_point: Defines the starting point for the stitching + Output: + -List of point coordinate tuples + -Tag (origin) of each point to analyze why a point was placed + at this position + """ + + if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: + raise ValueError( + "Single spiral geometry must not have more than one hole!") + + ordered_poly = orient(poly, -1) + ordered_poly = ordered_poly.simplify( + constants.simplification_threshold, False) + root = AnyNode( + id="node", + val=ordered_poly.exterior, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) + active_polys = [root] + active_holes = [[]] + + for holes in ordered_poly.interiors: + active_holes[0].append( + AnyNode( + id="hole", + val=holes, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None), + ) + ) + + while len(active_polys) > 0: + current_poly = active_polys.pop() + current_holes = active_holes.pop() + poly_inners = [] + + outer = offset_linear_ring( + current_poly.val, + offset, + "left", + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + outer = outer.simplify(constants.simplification_threshold, False) + outer = take_only_valid_linear_rings(outer) + + for j in range(len(current_holes)): + inner = offset_linear_ring( + current_holes[j].val, + offset, + "left", + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + inner = inner.simplify(constants.simplification_threshold, False) + inner = take_only_valid_linear_rings(inner) + if not inner.is_empty: + poly_inners.append(Polygon(inner)) + if not outer.is_empty: + if len(poly_inners) == 0: + if outer.geom_type == "LineString": + result = Polygon(outer) + else: + result = MultiPolygon(polygonize(outer)) + else: + if outer.geom_type == "LineString": + result = Polygon(outer).difference( + MultiPolygon(poly_inners)) + else: + result = MultiPolygon(outer).difference( + MultiPolygon(poly_inners)) + + if not result.is_empty and result.area > offset * offset / 10: + result_list = [] + if result.geom_type == "Polygon": + result_list = [result] + else: + result_list = list(result) + + for polygon in result_list: + polygon = orient(polygon, -1) + + if polygon.area < offset * offset / 10: + continue + + polygon = polygon.simplify( + constants.simplification_threshold, False + ) + poly_coords = polygon.exterior + poly_coords = take_only_valid_linear_rings(poly_coords) + if poly_coords.is_empty: + continue + + node = AnyNode( + id="node", + parent=current_poly, + val=poly_coords, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) + active_polys.append(node) + hole_node_list = [] + for hole in polygon.interiors: + hole_node = AnyNode( + id="hole", + val=hole, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) + for previous_hole in current_holes: + if Polygon(hole).contains(Polygon(previous_hole.val)): + previous_hole.parent = hole_node + hole_node_list.append(hole_node) + active_holes.append(hole_node_list) + for previous_hole in current_holes: + # If the previous holes are not + # contained in the new holes they + # have been merged with the + # outer polygon + if previous_hole.parent is None: + previous_hole.parent = current_poly + + + make_tree_uniform_ccw(root) + + if strategy == StitchingStrategy.CLOSEST_POINT: + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_nearest_neighbor( + root, offset, stitch_distance, starting_point, offset_by_half) + elif strategy == StitchingStrategy.INNER_TO_OUTER: + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( + root, offset, stitch_distance, starting_point, offset_by_half) + elif strategy == StitchingStrategy.SPIRAL: + if not check_and_prepare_tree_for_valid_spiral(root): + raise ValueError("Geometry cannot be filled with one spiral!") + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_spiral( + root, offset, stitch_distance, starting_point, offset_by_half) + else: + raise ValueError("Invalid stitching stratety!") + + return connected_line, connected_line_origin diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py new file mode 100644 index 00000000..d7afad0c --- /dev/null +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -0,0 +1,906 @@ +from shapely.geometry.polygon import LineString, LinearRing +from shapely.geometry import Point, MultiPoint +from shapely.ops import nearest_points +from collections import namedtuple +from depq import DEPQ +import trimesh +import numpy as np +from scipy import spatial +import math +from anytree import PreOrderIter +from ..stitches import sample_linestring +from ..stitches import point_transfer +from ..stitches import constants + +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) + + +def cut(line, distance): + """ + Cuts a closed line so that the new closed line starts at the + point with "distance" to the beginning of the old line. + """ + if distance <= 0.0 or distance >= line.length: + return [LineString(line)] + coords = list(line.coords) + for i, p in enumerate(coords): + if i > 0 and p == coords[0]: + pd = line.length + else: + pd = line.project(Point(p)) + if pd == distance: + if coords[0] == coords[-1]: + return LineString(coords[i:] + coords[1: i + 1]) + else: + return LineString(coords[i:] + coords[:i]) + if pd > distance: + cp = line.interpolate(distance) + if coords[0] == coords[-1]: + return LineString( + [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] + ) + else: + return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) + + +def connect_raster_tree_nearest_neighbor( # noqa: C901 + tree, used_offset, stitch_distance, close_point, offset_by_half): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made where both curves + come closest together. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ + + current_coords = tree.val + abs_offset = abs(used_offset) + result_coords = [] + result_coords_origin = [] + + # We cut the current item so that its index 0 is closest to close_point + start_distance = tree.val.project(close_point) + if start_distance > 0: + current_coords = cut(current_coords, start_distance) + tree.val = current_coords + + if not tree.transferred_point_priority_deque.is_empty(): + new_DEPQ = DEPQ(iterable=None, maxlen=None) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) + tree.transferred_point_priority_deque = new_DEPQ + + stitching_direction = 1 + # This list should contain a tuple of nearest points between + # the current geometry and the subgeometry, the projected + # distance along the current geometry, and the belonging subtree node + nearest_points_list = [] + + for subnode in tree.children: + point_parent, point_child = nearest_points(current_coords, subnode.val) + proj_distance = current_coords.project(point_parent) + nearest_points_list.append( + nearest_neighbor_tuple( + nearest_point_parent=point_parent, + nearest_point_child=point_child, + proj_distance_parent=proj_distance, + child_node=subnode) + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + if nearest_points_list: + start_distance = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_distance = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) + else: + start_distance = abs_offset * constants.factor_offset_starting_points + end_distance = (current_coords.length - abs_offset * constants.factor_offset_starting_points) + + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + start_distance, # We add/subtract an offset to not sample + # the same point again (avoid double + # points for start and end) + end_distance, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + assert len(own_coords) == len(own_coords_origin) + own_coords_origin[0] = sample_linestring.PointSource.ENTER_LEAVING_POINT + own_coords_origin[-1] = sample_linestring.PointSource.ENTER_LEAVING_POINT + tree.stitching_direction = stitching_direction + tree.already_rastered = True + + # Next we need to transfer our rastered points to siblings and childs + to_transfer_point_list = [] + to_transfer_point_list_origin = [] + for k in range(1, len(own_coords) - 1): + # Do not take the first and the last since they are ENTER_LEAVING_POINT + # points for sure + + if (not offset_by_half and own_coords_origin[k] == sample_linestring.PointSource.EDGE_NEEDED): + continue + if (own_coords_origin[k] == sample_linestring.PointSource.ENTER_LEAVING_POINT or + own_coords_origin[k] == sample_linestring.PointSource.FORBIDDEN_POINT): + continue + to_transfer_point_list.append(Point(own_coords[k])) + point_origin = own_coords_origin[k] + to_transfer_point_list_origin.append(point_origin) + + # Since the projection is only in ccw direction towards inner we need + # to use "-used_offset" for stitching_direction==-1 + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + if not nearest_points_list: + # If there is no child (inner geometry) we can simply take + # our own rastered coords as result + result_coords = own_coords + result_coords_origin = own_coords_origin + else: + # There are childs so we need to merge their coordinates + + # with our own rastered coords + + # To create a closed ring + own_coords.append(own_coords[0]) + own_coords_origin.append(own_coords_origin[0]) + + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) + total_distance = start_distance + cur_item = 0 + result_coords = [own_coords[0]] + result_coords_origin = [ + sample_linestring.PointSource.ENTER_LEAVING_POINT] + for i in range(1, len(own_coords)): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + + item = nearest_points_list[cur_item] + (child_coords, child_coords_origin) = connect_raster_tree_nearest_neighbor( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + d = item.nearest_point_parent.distance( + Point(own_coords[i - 1])) + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append(item.nearest_point_parent.coords[0]) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + # reversing avoids crossing when entering and + # leaving the child segment + result_coords.extend(child_coords[::-1]) + result_coords_origin.extend(child_coords_origin[::-1]) + + # And here we calculate the point for the leaving + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs(nearest_points_list[cur_item+1].proj_distance_parent-item.proj_distance_parent) + ) + + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + abs_offset * constants.factor_offset_starting_points + ).coords[0] + ) + result_coords_origin.append(sample_linestring.PointSource.ENTER_LEAVING_POINT) + + cur_item += 1 + if i < len(own_coords) - 1: + if (Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset * constants.factor_offset_remove_points): + result_coords.append(own_coords[i]) + result_coords_origin.append(own_coords_origin[i]) + + # Since current_coords and temp are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in temp would be very time + # consuming we project only every n-th point which resets + # the accumulated error every n-th point. + if i % 20 == 0: + total_distance = current_coords.project(Point(own_coords[i])) + else: + total_distance += next_distance + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin + + +def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): + """ + Takes a line and calculates the nearest distance along this + line to enter the next_line + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter next_line + -next_line: contains the next_line which need to be entered + -thresh: The distance between travel_line and next_line needs + to below thresh to be a valid point for entering + Output: + -tuple - the tuple structure is: + (nearest point in travel_line, nearest point in next_line) + """ + point_list = list(MultiPoint(travel_line.coords)) + + if point_list[0].distance(next_line) < thresh: + return nearest_points(point_list[0], next_line) + + for i in range(len(point_list) - 1): + line_segment = LineString([point_list[i], point_list[i + 1]]) + result = nearest_points(line_segment, next_line) + + if result[0].distance(result[1]) < thresh: + return result + line_segment = LineString([point_list[-1], point_list[0]]) + result = nearest_points(line_segment, next_line) + + if result[0].distance(result[1]) < thresh: + return result + else: + return None + + +def create_nearest_points_list( + travel_line, children_list, threshold, threshold_hard, preferred_direction=0): + """ + Takes a line and calculates the nearest distance along this line to + enter the childs in children_list + The method calculates the distances along the line and along the + reversed line to find the best direction which minimizes the overall + distance for all childs. + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter the childs + -children_list: contains the childs of travel_line which need to be entered + -threshold: The distance between travel_line and a child needs to be + below threshold to be a valid point for entering + -preferred_direction: Put a bias on the desired travel direction along + travel_line. If equals zero no bias is applied. + preferred_direction=1 means we prefer the direction of travel_line; + preferred_direction=-1 means we prefer the opposite direction. + Output: + -stitching direction for travel_line + -list of tuples (one tuple per child). The tuple structure is: + ((nearest point in travel_line, nearest point in child), + distance along travel_line, belonging child) + """ + + result_list_in_order = [] + result_list_reversed_order = [] + + travel_line_reversed = LinearRing(travel_line.coords[::-1]) + + weight_in_order = 0 + weight_reversed_order = 0 + for child in children_list: + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold_hard + ) + assert result is not None + proj = travel_line.project(result[0]) + weight_in_order += proj + result_list_in_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold_hard + ) + assert result is not None + proj = travel_line_reversed.project(result[0]) + weight_reversed_order += proj + result_list_reversed_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + if preferred_direction == 1: + # Reduce weight_in_order to make in order stitching more preferred + weight_in_order = min( + weight_in_order / 2, max(0, weight_in_order - 10 * threshold) + ) + if weight_in_order == weight_reversed_order: + return (1, result_list_in_order) + elif preferred_direction == -1: + # Reduce weight_reversed_order to make reversed + # stitching more preferred + weight_reversed_order = min( + weight_reversed_order / + 2, max(0, weight_reversed_order - 10 * threshold) + ) + if weight_in_order == weight_reversed_order: + return (-1, result_list_reversed_order) + + if weight_in_order < weight_reversed_order: + return (1, result_list_in_order) + else: + return (-1, result_list_reversed_order) + + +def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): + """ + Takes a line segment (consisting of 3 points!) + and calculates a new middle point if the line_segment is + straight enough to be resampled by points max_stitch_distance apart FROM THE END OF line_segment. + Returns None if the middle point is not needed. + """ + angles = sample_linestring.calculate_line_angles(line_segment) + if angles[1] < abs_offset * constants.limiting_angle_straight: + if line_segment.length < max_stitch_distance: + return None + else: + return line_segment.interpolate(line_segment.length - max_stitch_distance).coords[0] + else: + return line_segment.coords[1] + + +def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made as fast as possible to + reach the innermost child as fast as possible in order to stitch afterwards + from inner to outer. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with points obeying + stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was placed + at this position + """ + + current_coords = tree.val + abs_offset = abs(used_offset) + result_coords = [] + result_coords_origin = [] + + start_distance = tree.val.project(close_point) + # We cut the current path so that its index 0 is closest to close_point + if start_distance > 0: + current_coords = cut(current_coords, start_distance) + tree.val = current_coords + + if not tree.transferred_point_priority_deque.is_empty(): + new_DEPQ = DEPQ(iterable=None, maxlen=None) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) + tree.transferred_point_priority_deque = new_DEPQ + + # We try to use always the opposite stitching direction with respect to the + # parent to avoid crossings when entering and leaving the child + parent_stitching_direction = -1 + if tree.parent is not None: + parent_stitching_direction = tree.parent.stitching_direction + + # Find the nearest point in current_coords and its children and + # sort it along the stitching direction + stitching_direction, nearest_points_list = create_nearest_points_list( + current_coords, + tree.children, + constants.offset_factor_for_adjacent_geometry * abs_offset, + 2.05 * abs_offset, + parent_stitching_direction, + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + # Have a small offset for the starting and ending to avoid double points + # at start and end point (since the paths are closed rings) + if nearest_points_list: + start_offset = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_offset = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) + else: + start_offset = abs_offset * constants.factor_offset_starting_points + end_offset = (current_coords.length - abs_offset * constants.factor_offset_starting_points) + + if stitching_direction == 1: + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + start_offset, # We add start_offset to not sample the same + # point again (avoid double points for start + # and end) + end_offset, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False + ) + else: + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + current_coords.length - start_offset, # We subtract + # start_offset to not + # sample the same point + # again (avoid double + # points for start + # and end) + current_coords.length - end_offset, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False + ) + current_coords.coords = current_coords.coords[::-1] + + assert len(own_coords) == len(own_coords_origin) + + tree.stitching_direction = stitching_direction + tree.already_rastered = True + + to_transfer_point_list = [] + to_transfer_point_list_origin = [] + for k in range(0, len(own_coords)): + # TODO: maybe do not take the first and the last + # since they are ENTER_LEAVING_POINT points for sure + if ( + not offset_by_half + and own_coords_origin[k] == sample_linestring.PointSource.EDGE_NEEDED + or own_coords_origin[k] == sample_linestring.PointSource.FORBIDDEN_POINT): + continue + if own_coords_origin[k] == sample_linestring.PointSource.ENTER_LEAVING_POINT: + continue + to_transfer_point_list.append(Point(own_coords[k])) + to_transfer_point_list_origin.append(own_coords_origin[k]) + + assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) + + # Next we need to transfer our rastered points to siblings and childs + # Since the projection is only in ccw direction towards inner we + # need to use "-used_offset" for stitching_direction==-1 + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + if not nearest_points_list: + # If there is no child (inner geometry) we can simply + # take our own rastered coords as result + result_coords = own_coords + result_coords_origin = own_coords_origin + else: + # There are childs so we need to merge their coordinates + # with our own rastered coords + + # Create a closed ring for the following code + own_coords.append(own_coords[0]) + own_coords_origin.append(own_coords_origin[0]) + + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) + total_distance = start_offset + + cur_item = 0 + result_coords = [own_coords[0]] + result_coords_origin = [own_coords_origin[0]] + + for i in range(1, len(own_coords)): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + # The current and the next point in own_coords enclose the + # nearest point tuple between this geometry and child + # geometry. Hence we need to insert the child geometry points + # here before the next point of own_coords. + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_from_inner_to_outer( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + # Imagine the nearest point of the child is within a long + # segment of the parent. Without additonal points + # on the parent side this would cause noticeable deviations. + # Hence we add here points shortly before and after + # the entering of the child to have only minor deviations to + # the desired shape. + # Here is the point for the entering: + if (Point(result_coords[-1]).distance(item.nearest_point_parent) > constants.factor_offset_starting_points * abs_offset): + result_coords.append(item.nearest_point_parent.coords[0]) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + + # Check whether the number of points of the connecting lines + # from child to child can be reduced + if len(child_coords) > 1: + point = calculate_replacing_middle_point( + LineString( + [result_coords[-1], child_coords[0], child_coords[1]] + ), + abs_offset, + stitch_distance, + ) + + if point is not None: + result_coords.append(point) + result_coords_origin.append(child_coords_origin[0]) + + result_coords.extend(child_coords[1:]) + result_coords_origin.extend(child_coords_origin[1:]) + else: + result_coords.extend(child_coords) + result_coords_origin.extend(child_coords_origin) + + # And here is the point for the leaving of the child + # (distance to the own following point should not be too large) + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > constants.factor_offset_starting_points * abs_offset: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + 2 * constants.factor_offset_starting_points * abs_offset + ).coords[0] + ) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + # Check whether this additional point makes the last point + # of the child unnecessary + point = calculate_replacing_middle_point( + LineString( + [result_coords[-3], result_coords[-2], result_coords[-1]] + ), + abs_offset, + stitch_distance, + ) + if point is None: + result_coords.pop(-2) + result_coords_origin.pop(-2) + + cur_item += 1 + if i < len(own_coords) - 1: + if (Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset * constants.factor_offset_remove_points): + result_coords.append(own_coords[i]) + result_coords_origin.append(own_coords_origin[i]) + + # Since current_coords and own_coords are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in own_coords would be very + # time consuming we project only every n-th point which resets + # the accumulated error every n-th point. + if i % 20 == 0: + total_distance = current_coords.project(Point(own_coords[i])) + else: + total_distance += next_distance + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin + + +# Partly taken from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py +def interpolate_LinearRings(a, b, start=None, step=.005): + """ + Interpolate between two LinearRings + Parameters + ------------- + a : shapely.geometry.Polygon.LinearRing + LinearRing start point will lie on + b : shapely.geometry.Polygon.LinearRing + LinearRing end point will lie on + start : (2,) float, or None + Point to start at + step : float + How far apart should points on + the path be. + Returns + ------------- + path : (n, 2) float + Path interpolated between two LinearRings + """ + + # resample the first LinearRing so every sample is spaced evenly + ra = trimesh.path.traversal.resample_path( + a, step=step) + if not a.is_ccw: + ra = ra[::-1] + + assert trimesh.path.util.is_ccw(ra) + if start is not None: + # find the closest index on LinerRing 'a' + # by creating a KDTree + tree_a = spatial.cKDTree(ra) + index = tree_a.query(start)[1] + ra = np.roll(ra, -index, axis=0) + + # resample the second LinearRing for even spacing + rb = trimesh.path.traversal.resample_path(b, + step=step) + if not b.is_ccw: + rb = rb[::-1] + + # we want points on 'b' that correspond index- wise + # the resampled points on 'a' + tree_b = spatial.cKDTree(rb) + # points on b with corresponding indexes to ra + pb = rb[tree_b.query(ra)[1]] + + # linearly interpolate between 'a' and 'b' + weights = np.linspace(0.0, 1.0, len(ra)).reshape((-1, 1)) + + # start on 'a' and end on 'b' + points = (ra * (1.0 - weights)) + (pb * weights) + + result = LineString(points) + + return result.simplify(constants.simplification_threshold, False) + + +def connect_raster_tree_spiral( + tree, used_offset, stitch_distance, close_point, offset_by_half): + """ + Takes the offsetted curves organized as tree, connects and samples them as a spiral. + It expects that each node in the tree has max. one child + Input: + -tree: contains the offsetted curves in a hierarchical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one spiral and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ + + abs_offset = abs(used_offset) + if tree.is_leaf: + return sample_linestring.raster_line_string_with_priority_points( + tree.val, + 0, + tree.val.length, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + result_coords = [] + result_coords_origin = [] + starting_point = close_point.coords[0] + # iterate to the second last level + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): + ring1 = node.val + ring2 = node.children[0].val + + part_spiral = interpolate_LinearRings( + ring1, ring2, starting_point) + node.val = part_spiral + + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + node.val, + 0, + node.val.length, + stitch_distance, + node.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + point_transfer.transfer_points_to_surrounding( + node, + -used_offset, + offset_by_half, + own_coords, + own_coords_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + node, + -used_offset, + False, + own_coords, + own_coords_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + # Check whether starting of own_coords or end of result_coords can be removed + if not result_coords: + result_coords.extend(own_coords) + result_coords_origin.extend(own_coords_origin) + elif len(own_coords) > 0: + if Point(result_coords[-1]).distance(Point(own_coords[0])) > constants.line_lengh_seen_as_one_point: + lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) + else: + lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[1]]) + (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, + DEPQ(), abs_offset, offset_by_half, False) + if len(temp_coords) == 2: # only start and end point of lineseg was needed + result_coords.pop() + result_coords_origin.pop() + result_coords.extend(own_coords[1:]) + result_coords_origin.extend(own_coords_origin[1:]) + elif len(temp_coords) == 3: # one middle point within lineseg was needed + result_coords.pop() + result_coords.append(temp_coords[1]) + result_coords.extend(own_coords[1:]) + result_coords_origin.extend(own_coords_origin[1:]) + else: # all points were needed + result_coords.extend(own_coords) + result_coords_origin.extend(own_coords_origin) + # make sure the next section starts where this + # section of the curve ends + starting_point = result_coords[-1] + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin -- cgit v1.3.1 From e65aaebbcab1ca6fbcf99d9f3665af423e02c2f5 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Wed, 4 May 2022 20:04:39 +0200 Subject: rebase corrections --- lib/elements/clone.py | 6 ------ lib/extensions/base.py | 15 ++++++++++++--- lib/stitches/fill.py | 1 - lib/svg/tags.py | 10 ++++++++-- 4 files changed, 20 insertions(+), 12 deletions(-) (limited to 'lib/extensions') diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 303c1c2f..d9185012 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,13 +5,7 @@ from math import atan, degrees -<<<<<<< HEAD -from ..commands import is_command, is_command_symbol -======= -import inkex - from ..commands import is_command_symbol ->>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 949f947e..cf94714c 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -8,10 +8,12 @@ import os import re from collections.abc import MutableMapping -import inkex from lxml import etree +from lxml.etree import Comment from stringcase import snakecase +import inkex + from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone @@ -19,7 +21,8 @@ from ..i18n import _ from ..marker import has_marker from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, + SVG_GROUP_TAG, SVG_MASK_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -129,6 +132,10 @@ class InkstitchExtension(inkex.Effect): def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901 nodes = [] + + if node.tag == Comment: + return [] + element = EmbroideryElement(node) if element.has_command('ignore_object'): @@ -141,7 +148,9 @@ class InkstitchExtension(inkex.Effect): if (node.tag in EMBROIDERABLE_TAGS or node.tag == SVG_GROUP_TAG) and element.get_style('display', 'inline') is None: return [] - if node.tag == SVG_DEFS_TAG: + # defs, masks and clippaths can contain embroiderable elements + # but should never be rendered directly. + if node.tag in [SVG_DEFS_TAG, SVG_MASK_TAG, SVG_CLIPPATH_TAG]: return [] # command connectors with a fill color set, will glitch into the elements list diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index a09b93b1..94df3f77 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -11,7 +11,6 @@ from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache -from ..stitch_plan import Stitch def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 02340aa5..0c5ffd3d 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -3,14 +3,16 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import inkex from lxml import etree +import inkex + etree.register_namespace("inkstitch", "http://inkstitch.org/namespace") inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') +SVG_POLYGON_TAG = inkex.addNS('polygon', 'svg') SVG_RECT_TAG = inkex.addNS('rect', 'svg') SVG_ELLIPSE_TAG = inkex.addNS('ellipse', 'svg') SVG_CIRCLE_TAG = inkex.addNS('circle', 'svg') @@ -22,12 +24,15 @@ SVG_LINK_TAG = inkex.addNS('a', 'svg') SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg') SVG_USE_TAG = inkex.addNS('use', 'svg') SVG_IMAGE_TAG = inkex.addNS('image', 'svg') +SVG_CLIPPATH_TAG = inkex.addNS('clipPath', 'svg') +SVG_MASK_TAG = inkex.addNS('mask', 'svg') INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') CONNECTION_START = inkex.addNS('connection-start', 'inkscape') CONNECTION_END = inkex.addNS('connection-end', 'inkscape') CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') +INKSCAPE_DOCUMENT_UNITS = inkex.addNS('document-units', 'inkscape') XLINK_HREF = inkex.addNS('href', 'xlink') @@ -37,7 +42,8 @@ SODIPODI_ROLE = inkex.addNS('role', 'sodipodi') INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch') -EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG) +EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_POLYGON_TAG, + SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG) NOT_EMBROIDERABLE_TAGS = (SVG_IMAGE_TAG, SVG_TEXT_TAG) SVG_OBJECT_TAGS = (SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG, SVG_RECT_TAG) -- cgit v1.3.1 From 672bded1259589d609d1a6656df5537c5da20569 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 6 May 2022 21:03:56 -0400 Subject: shapely geoms fixes --- lib/extensions/break_apart.py | 2 +- lib/stitches/auto_fill.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) (limited to 'lib/extensions') diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py index 5bfd88a4..581e49bc 100644 --- a/lib/extensions/break_apart.py +++ b/lib/extensions/break_apart.py @@ -83,7 +83,7 @@ class BreakApart(InkstitchExtension): if diff.geom_type == 'MultiPolygon': polygons.remove(other) polygons.remove(polygon) - for p in diff: + for p in diff.geoms: polygons.append(p) # it is possible, that a polygons overlap with multiple # polygons, this means, we need to start all over again diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index b3b9434f..1d72e710 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -90,7 +90,7 @@ def which_outline(shape, coords): # fail sometimes. point = shgeo.Point(*coords) - outlines = list(shape.boundary.geoms) + outlines = ensure_multi_line_string(shape.boundary).geoms outline_indices = list(range(len(outlines))) closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) @@ -104,7 +104,7 @@ def project(shape, coords, outline_index): This returns the distance along the outline at which the point resides. """ - outline = list(shape.boundary.geoms)[outline_index] + outline = ensure_multi_line_string(shape.boundary).geoms[outline_index] return outline.project(shgeo.Point(*coords)) @@ -173,7 +173,7 @@ def insert_node(graph, shape, point): point = tuple(point) outline = which_outline(shape, point) projection = project(shape, point, outline) - projected_point = list(shape.boundary.geoms)[outline].interpolate(projection) + projected_point = ensure_multi_line_string(shape.boundary).geoms[outline].interpolate(projection) node = (projected_point.x, projected_point.y) edges = [] @@ -200,7 +200,8 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): def add_boundary_travel_nodes(graph, shape): - for outline_index, outline in enumerate(shape.boundary.geoms): + outlines = ensure_multi_line_string(shape.boundary).geoms + for outline_index, outline in enumerate(outlines): prev = None for point in outline.coords: point = shgeo.Point(point) @@ -265,7 +266,10 @@ def fallback(shape, running_stitch_length): matter. """ - return running_stitch(line_string_to_point_list(shape.boundary[0]), running_stitch_length) + boundary = ensure_multi_line_string(shape.boundary) + outline = boundary.geoms[0] + + return running_stitch(line_string_to_point_list(outline), running_stitch_length) @debug.time -- cgit v1.3.1