diff options
| -rw-r--r-- | lib/elements/auto_fill.py | 118 | ||||
| -rw-r--r-- | lib/elements/clone.py | 5 | ||||
| -rw-r--r-- | lib/elements/element.py | 22 | ||||
| -rw-r--r-- | lib/elements/utils.py | 5 | ||||
| -rw-r--r-- | lib/extensions/params.py | 125 | ||||
| -rw-r--r-- | lib/extensions/selection_to_guide_line.py | 8 | ||||
| -rw-r--r-- | lib/patterns.py | 5 | ||||
| -rw-r--r-- | lib/stitches/ConnectAndSamplePattern.py | 834 | ||||
| -rw-r--r-- | lib/stitches/DebuggingMethods.py | 56 | ||||
| -rw-r--r-- | lib/stitches/LineStringSampling.py | 446 | ||||
| -rw-r--r-- | lib/stitches/PointTransfer.py | 277 | ||||
| -rw-r--r-- | lib/stitches/StitchPattern.py | 266 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 140 | ||||
| -rw-r--r-- | lib/stitches/constants.py | 53 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 53 |
15 files changed, 1489 insertions, 924 deletions
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): @@ -177,18 +182,15 @@ class AutoFill(EmbroideryElement): 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])<constants.eps and abs(path_coords.coords[0][1]-path_coords.coords[1][1])<constants.eps: + # 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: + 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) current_distance = start_distance - #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(path_coords.coords,angles): - #if abs(point[0]-40.4) < 0.2 and abs(point[1]-2.3)< 0.2: + for point, angle in zip(path_coords.coords, angles): + # if abs(point[0]-40.4) < 0.2 and abs(point[1]-2.3)< 0.2: # print("GEFUNDEN") current_distance = start_distance+path_coords.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: - 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 (<abs_offset) we remove one of it + # 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 (<abs_offset) we remove one of it if (((merged_point_list[segment_start_index][0].point_source == PointSource.OVERNEXT and merged_point_list[segment_end_index][0].point_source == PointSource.DIRECT) or (merged_point_list[segment_start_index][0].point_source == PointSource.DIRECT and merged_point_list[segment_end_index][0].point_source == PointSource.OVERNEXT)) and - abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset): + abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset): result_list.pop() result_list.append(merged_point_list[segment_end_index]) - #To have a chance to replace all forbidden points afterwards + # To have a chance to replace all forbidden points afterwards if merged_point_list[segment_end_index][0].point_source == PointSource.FORBIDDEN_POINT: forbidden_point_list.append(len(result_list)-1) segment_start_index = segment_end_index - segment_end_index+=1 + segment_end_index += 1 - return_point_list = [] #[result_list[0][0].point.coords[0]] - return_point_source_list = []#[result_list[0][0].point_source] + return_point_list = [] # [result_list[0][0].point.coords[0]] + return_point_source_list = [] # [result_list[0][0].point_source] - #Currently replacement of forbidden points not satisfying - #result_list = replace_forbidden_points(aligned_line, result_list, forbidden_point_list,abs_offset) + # Currently replacement of forbidden points not satisfying + # result_list = replace_forbidden_points(aligned_line, result_list, forbidden_point_list,abs_offset) - #Finally we create the final return_point_list and return_point_source_list + # Finally we create the final return_point_list and return_point_source_list for i in range(len(result_list)): return_point_list.append(result_list[i][0].point.coords[0]) - #if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2: + # if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2: # print("GEFUNDEN") if result_list[i][0].point_source == PointSource.HARD_EDGE_INTERNAL: - point_source = PointSource.HARD_EDGE + point_source = PointSource.HARD_EDGE elif result_list[i][0].point_source == PointSource.SOFT_EDGE_INTERNAL: point_source = PointSource.SOFT_EDGE elif result_list[i][0].point_source == PointSource.REGULAR_SPACING_INTERNAL: @@ -261,132 +286,145 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, return_point_source_list.append(point_source) - assert(len(return_point_list) == len(return_point_source_list)) - #return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset) + # return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset) return return_point_list, return_point_source_list -#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. +# -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 (<abs_offset) we remove one of it + # 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 (<abs_offset) we remove one of it if (((merged_point_list[segment_start_index][0].point_source == PointSource.OVERNEXT and merged_point_list[segment_end_index][0].point_source == PointSource.DIRECT) or (merged_point_list[segment_start_index][0].point_source == PointSource.DIRECT and merged_point_list[segment_end_index][0].point_source == PointSource.OVERNEXT)) and - abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset): + abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset): result_list.pop() result_list.append(merged_point_list[segment_end_index]) - #To have a chance to replace all forbidden points afterwards + # To have a chance to replace all forbidden points afterwards if merged_point_list[segment_end_index][0].point_source == PointSource.FORBIDDEN_POINT: forbidden_point_list.append(len(result_list)-1) segment_start_index = segment_end_index - segment_end_index+=1 + segment_end_index += 1 - return_point_list = [] #[result_list[0][0].point.coords[0]] - return_point_source_list = []#[result_list[0][0].point_source] + return_point_list = [] # [result_list[0][0].point.coords[0]] + return_point_source_list = [] # [result_list[0][0].point_source] - #Currently replacement of forbidden points not satisfying - result_list = replace_forbidden_points(aligned_line, result_list, forbidden_point_list,abs_offset) + # Currently replacement of forbidden points not satisfying + result_list = replace_forbidden_points( + aligned_line, result_list, forbidden_point_list, abs_offset) - #Finally we create the final return_point_list and return_point_source_list + # Finally we create the final return_point_list and return_point_source_list for i in range(len(result_list)): return_point_list.append(result_list[i][0].point.coords[0]) - #if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2: + # if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2: # print("GEFUNDEN") if result_list[i][0].point_source == PointSource.HARD_EDGE_INTERNAL: - point_source = PointSource.HARD_EDGE + point_source = PointSource.HARD_EDGE elif result_list[i][0].point_source == PointSource.SOFT_EDGE_INTERNAL: point_source = PointSource.SOFT_EDGE elif result_list[i][0].point_source == PointSource.REGULAR_SPACING_INTERNAL: @@ -459,44 +498,49 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit return_point_source_list.append(point_source) - assert(len(return_point_list) == len(return_point_source_list)) - #return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset) + # return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset) return return_point_list, return_point_source_list + def replace_forbidden_points(line, result_list, forbidden_point_list_indices, abs_offset): - current_index_shift = 0 #since we add and remove points in the result_list, we need to adjust the indices stored in forbidden_point_list_indices + # since we add and remove points in the result_list, we need to adjust the indices stored in forbidden_point_list_indices + current_index_shift = 0 for index in forbidden_point_list_indices: - #if abs(result_list[index][0].point.coords[0][0]-40.7) < 0.2 and abs(result_list[index][0].point.coords[0][1]-1.3)< 0.2: + # if abs(result_list[index][0].point.coords[0][0]-40.7) < 0.2 and abs(result_list[index][0].point.coords[0][1]-1.3)< 0.2: # print("GEFUNDEN") - index+=current_index_shift - distance_left = result_list[index][0].point.distance(result_list[index-1][0].point)/2.0 - distance_right = result_list[index][0].point.distance(result_list[(index+1)%len(result_list)][0].point)/2.0 + index += current_index_shift + distance_left = result_list[index][0].point.distance( + result_list[index-1][0].point)/2.0 + distance_right = result_list[index][0].point.distance( + result_list[(index+1) % len(result_list)][0].point)/2.0 while distance_left > 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 |
