From 0fcf8bb97ced8df552cd0283b4ea009b6ca42623 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 21 Oct 2021 16:24:40 +0200 Subject: added tangential and guided fill --- lib/elements/__init__.py | 2 +- lib/elements/auto_fill.py | 281 ++++++++++++++++++++++++++++++++++++++++------ lib/elements/clone.py | 10 +- lib/elements/element.py | 4 +- lib/elements/fill.py | 205 --------------------------------- lib/elements/utils.py | 10 +- 6 files changed, 263 insertions(+), 249 deletions(-) delete mode 100644 lib/elements/fill.py (limited to 'lib/elements') diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index 2e4c31a7..bb5c95ba 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.py @@ -7,7 +7,7 @@ from .auto_fill import AutoFill from .clone import Clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -from .fill import Fill +#from .fill import Fill from .image import ImageObject from .polyline import Polyline from .satin_column import SatinColumn diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index fbbd86d3..87bdb010 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -6,18 +6,26 @@ import math import sys import traceback +import re +import logging +import inkex from shapely import geometry as shgeo - -from .element import param -from .fill import Fill -from .validation import ValidationWarning +from shapely.validation import explain_validity +from ..stitches import legacy_fill from ..i18n import _ from ..stitch_plan import StitchGroup from ..stitches import auto_fill -from ..svg.tags import INKSCAPE_LABEL +from ..stitches import StitchPattern 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") @@ -38,13 +46,125 @@ class UnderlayInsetWarning(ValidationWarning): "Ink/Stitch will ignore it and will use the original size instead.") -class AutoFill(Fill): +class AutoFill(EmbroideryElement): element_name = _("AutoFill") @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index = 1) + def auto_fill2(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('fill_method', _('Fill method'), type='dropdown', default=0, options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], 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) + def tangential_strategy(self): + return self.get_int_param('tangential_strategy', 1) + + @property + @param('join_style', _('Join Style'), type='dropdown', default=0, options=[_("Round"), _("Mitered"), _("Beveled")],select_items=[('fill_method',1)], sort_index = 2) + def join_style(self): + return self.get_int_param('join_style', 0) + + @property + @param('interlaced', _('Interlaced'), type='boolean', default=True,select_items=[('fill_method',1),('fill_method',2)], sort_index = 2) + def interlaced(self): + return self.get_boolean_param('interlaced', True) + + @property + @param('angle', + _('Angle of lines of stitches'), + tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + unit='deg', + type='float', + sort_index = 4, + select_items=[('fill_method',0)], + default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") + + @property + @param( + 'skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + type='boolean', + sort_index = 4, + select_items=[('fill_method',0), ('fill_method',2)], + default=False) + def skip_last(self): + return self.get_boolean_param("skip_last", False) + + @property + @param( + 'flip', + _('Flip fill (start right-to-left)'), + tooltip=_('The flip option can help you with routing your stitch path. ' + 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), + type='boolean', + sort_index = 4, + select_items=[('fill_method',0), ('fill_method',2)], + default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', + _('Spacing between rows'), + tooltip=_('Distance between rows of stitches.'), + unit='mm', + sort_index = 4, + type='float', + default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', + _('Maximum fill stitch length'), + tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index = 4, + type='float', + default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', + _('Stagger rows this many times before repeating'), + tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + type='int', + sort_index = 4, + select_items=[('fill_method',0)], + default=4) + def staggers(self): + return max(self.get_int_param("staggers", 4), 1) + + @property + @cache + def paths(self): + paths = self.flatten(self.parse_path()) + # ensure path length + for i, path in enumerate(paths): + if len(path) < 3: + paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + return paths + @property @cache @@ -66,7 +186,9 @@ class AutoFill(Fill): 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) + default=1.5, + select_items=[('fill_method',0),('fill_method',2)], + sort_index = 4) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -147,7 +269,9 @@ class AutoFill(Fill): tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', - default=0) + default=0, + sort_index = 5, + select_items=[('fill_method',0),('fill_method',2)]) def expand(self): return self.get_float_param('expand_mm', 0) @@ -158,7 +282,9 @@ class AutoFill(Fill): 'stitches avoid traveling in the direction of the row angle so that they ' 'are not visible. This gives them a jagged appearance.'), type='boolean', - default=True) + default=True, + select_items=[('fill_method',0),('fill_method',2)], + sort_index = 6) def underpath(self): return self.get_boolean_param('underpath', True) @@ -175,6 +301,51 @@ class AutoFill(Fill): def underlay_underpath(self): return self.get_boolean_param('underlay_underpath', True) + @property + @cache + def shape(self): + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) + + # There is a great number of "crossing border" errors on fill shapes + # If the polygon fails, we can try to run buffer(0) on the polygon in the + # hope it will fix at least some of them + if not self.shape_is_valid(polygon): + why = explain_validity(polygon) + message = re.match(r".+?(?=\[)", why) + if message.group(0) == "Self-intersection": + buffered = polygon.buffer(0) + # we do not want to break apart into multiple objects (possibly in the future?!) + # best way to distinguish the resulting polygon is to compare the area size of the two + # and make sure users will not experience significantly altered shapes without a warning + if math.isclose(polygon.area, buffered.area): + polygon = shgeo.MultiPolygon([buffered]) + + return polygon + + def shape_is_valid(self, shape): + # Shapely will log to stdout to complain about the shape unless we make + # it shut up. + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid = shape.is_valid + + logger.setLevel(level) + + return valid + def shrink_or_grow_shape(self, amount, validate=False): if amount: shape = self.shape.buffer(amount) @@ -226,7 +397,8 @@ class AutoFill(Fill): 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, self.fill_underlay_row_spacing, @@ -237,25 +409,70 @@ class AutoFill(Fill): starting_point, underpath=self.underlay_underpath)) stitch_groups.append(underlay) + starting_point = underlay.stitches[-1] + + if self.fill_method == 0: #Auto Fill + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + None, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + self.skip_last, + starting_point, + ending_point, + self.underpath)) + stitch_groups.append(stitch_group) + elif self.fill_method == 1: #Tangential Fill + polygons = list(self.fill_shape) + if not starting_point: + starting_point = (0,0) + for poly in polygons: + connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, + self.interlaced, + self.tangential_strategy, + shgeo.Point(starting_point)) + path = [InkstitchPoint(*p) for p in connectedLine] + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) + stitch_groups.append(stitch_group) + elif self.fill_method == 2: #Guided Auto Fill + lines = get_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")) + else: + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + lines[0].geoms[0], + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + 0, + self.skip_last, + starting_point, + ending_point, + self.underpath, + self.interlaced)) + stitch_groups.append(stitch_group) - starting_point = underlay.stitches[-1] - - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - self.skip_last, - starting_point, - ending_point, - self.underpath)) - stitch_groups.append(stitch_group) except Exception: if hasattr(sys, 'gettrace') and sys.gettrace(): # if we're debugging, let the exception bubble up diff --git a/lib/elements/clone.py b/lib/elements/clone.py index f408917d..bcecf3f0 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -14,7 +14,7 @@ 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 .fill import Fill from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -79,10 +79,10 @@ 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): - elements.append(AutoFill(node)) - else: - elements.append(Fill(node)) + #if element.get_boolean_param("auto_fill", True): + elements.append(AutoFill(node)) + #else: + # elements.append(Fill(node)) if element.get_style("stroke", self.node) is not None: if not is_command(element.node): elements.append(Stroke(node)) diff --git a/lib/elements/element.py b/lib/elements/element.py index 05bfd353..b8728f60 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -20,7 +20,7 @@ from ..utils import Point, cache class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, - options=[], default=None, tooltip=None, sort_index=0): + options=[], default=None, tooltip=None, sort_index=0, select_items=None): self.name = name self.description = description self.unit = unit @@ -32,6 +32,8 @@ class Param(object): self.default = default 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) diff --git a/lib/elements/fill.py b/lib/elements/fill.py deleted file mode 100644 index 51a6d703..00000000 --- a/lib/elements/fill.py +++ /dev/null @@ -1,205 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import logging -import math -import re - -from shapely import geometry as shgeo -from shapely.validation import explain_validity - -from .element import EmbroideryElement, param -from .validation import ValidationError -from ..i18n import _ -from ..stitch_plan import StitchGroup -from ..stitches import legacy_fill -from ..svg import PIXELS_PER_MM -from ..utils import cache - - -class UnconnectedError(ValidationError): - name = _("Unconnected") - description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " - "Ink/Stitch doesn't know what order to stitch them in. Please break this " - "object up into separate shapes.") - steps_to_solve = [ - _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'), - ] - - -class InvalidShapeError(ValidationError): - name = _("Border crosses itself") - description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") - steps_to_solve = [ - _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') - ] - - -class Fill(EmbroideryElement): - element_name = _("Fill") - - def __init__(self, *args, **kwargs): - super(Fill, self).__init__(*args, **kwargs) - - @property - @param('auto_fill', - _('Manually routed fill stitching'), - tooltip=_('AutoFill is the default method for generating fill stitching.'), - type='toggle', - inverse=True, - default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('angle', - _('Angle of lines of stitches'), - tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), - unit='deg', - type='float', - default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - # SVG spec says the default fill is black - return self.get_style("fill", "#000000") - - @property - @param( - 'skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - type='boolean', - default=False) - def skip_last(self): - return self.get_boolean_param("skip_last", False) - - @property - @param( - 'flip', - _('Flip fill (start right-to-left)'), - tooltip=_('The flip option can help you with routing your stitch path. ' - 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), - type='boolean', - default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', - _('Spacing between rows'), - tooltip=_('Distance between rows of stitches.'), - unit='mm', - type='float', - default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', - _('Maximum fill stitch length'), - tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), - unit='mm', - type='float', - default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - - @property - @param('staggers', - _('Stagger rows this many times before repeating'), - tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), - type='int', - default=4) - def staggers(self): - return max(self.get_int_param("staggers", 4), 1) - - @property - @cache - def paths(self): - paths = self.flatten(self.parse_path()) - # ensure path length - for i, path in enumerate(paths): - if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] - return paths - - @property - @cache - def shape(self): - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - # Very small holes will cause a shape to be rendered as an outline only - # they are too small to be rendered and only confuse the auto_fill algorithm. - # So let's ignore them - if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: - paths = [path for path in paths if shgeo.Polygon(path).area > 3] - - polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) - - # There is a great number of "crossing border" errors on fill shapes - # If the polygon fails, we can try to run buffer(0) on the polygon in the - # hope it will fix at least some of them - if not self.shape_is_valid(polygon): - why = explain_validity(polygon) - message = re.match(r".+?(?=\[)", why) - if message.group(0) == "Self-intersection": - buffered = polygon.buffer(0) - # if we receive a multipolygon, only use the first one of it - if type(buffered) == shgeo.MultiPolygon: - buffered = buffered[0] - # we do not want to break apart into multiple objects (possibly in the future?!) - # best way to distinguish the resulting polygon is to compare the area size of the two - # and make sure users will not experience significantly altered shapes without a warning - if type(buffered) == shgeo.Polygon and math.isclose(polygon.area, buffered.area, abs_tol=0.5): - polygon = shgeo.MultiPolygon([buffered]) - - return polygon - - def shape_is_valid(self, shape): - # Shapely will log to stdout to complain about the shape unless we make - # it shut up. - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid = shape.is_valid - - logger.setLevel(level) - - return valid - - def validation_errors(self): - if not self.shape_is_valid(self.shape): - why = explain_validity(self.shape) - message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) - - # I Wish this weren't so brittle... - if "Hole lies outside shell" in message: - yield UnconnectedError((x, y)) - else: - yield InvalidShapeError((x, y)) - - def to_stitch_groups(self, last_patch): - stitch_lists = legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers, - self.skip_last) - return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 99df7002..f858cc81 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -11,7 +11,7 @@ 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 .fill import Fill from .image import ImageObject from .pattern import PatternObject from .polyline import Polyline @@ -41,10 +41,10 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - else: - elements.append(Fill(node)) + #if element.get_boolean_param("auto_fill", True): + elements.append(AutoFill(node)) + #else: + # elements.append(Fill(node)) if element.get_style("stroke"): if not is_command(element.node): elements.append(Stroke(node)) -- cgit v1.3.1 From 125db3f83b3b330df757f7cc0faf6489b3cb348d Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 29 Oct 2021 16:18:22 +0200 Subject: Applied style guide --- lib/elements/auto_fill.py | 118 +++-- lib/elements/clone.py | 5 +- lib/elements/element.py | 22 +- lib/elements/utils.py | 5 +- lib/extensions/params.py | 125 +++-- lib/extensions/selection_to_guide_line.py | 8 +- lib/patterns.py | 5 +- lib/stitches/ConnectAndSamplePattern.py | 834 ++++++++++++++++++++---------- lib/stitches/DebuggingMethods.py | 56 +- lib/stitches/LineStringSampling.py | 446 +++++++++------- lib/stitches/PointTransfer.py | 277 +++++----- lib/stitches/StitchPattern.py | 266 ++++++---- lib/stitches/auto_fill.py | 140 +++-- lib/stitches/constants.py | 53 +- lib/stitches/fill.py | 53 +- 15 files changed, 1489 insertions(+), 924 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 87bdb010..81abf7ad 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -12,7 +12,6 @@ import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity -from ..stitches import legacy_fill from ..i18n import _ from ..stitch_plan import StitchGroup from ..stitches import auto_fill @@ -21,12 +20,12 @@ from ..utils import cache, version from .element import param from .element import EmbroideryElement from ..patterns import get_patterns -#from .fill import Fill from .validation import ValidationWarning from ..utils import Point as InkstitchPoint from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL + class SmallShapeWarning(ValidationWarning): name = _("Small Fill") description = _("This fill object is so small that it would probably look better as running stitch or satin column. " @@ -50,38 +49,42 @@ class AutoFill(EmbroideryElement): element_name = _("AutoFill") @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index = 1) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - + return self.get_boolean_param('auto_fill', True) + @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index = 2) + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, options=[_("Closest point"), _("Inner to Outer")],select_items=[('fill_method',1)], sort_index = 2) + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) @property - @param('join_style', _('Join Style'), type='dropdown', default=0, options=[_("Round"), _("Mitered"), _("Beveled")],select_items=[('fill_method',1)], sort_index = 2) + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) def join_style(self): return self.get_int_param('join_style', 0) @property - @param('interlaced', _('Interlaced'), type='boolean', default=True,select_items=[('fill_method',1),('fill_method',2)], sort_index = 2) + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) def interlaced(self): return self.get_boolean_param('interlaced', True) @property @param('angle', _('Angle of lines of stitches'), - tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=0) @cache def angle(self): @@ -99,8 +102,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def skip_last(self): return self.get_boolean_param("skip_last", False) @@ -112,8 +115,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The flip option can help you with routing your stitch path. ' 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -123,7 +126,7 @@ class AutoFill(EmbroideryElement): _('Spacing between rows'), tooltip=_('Distance between rows of stitches.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=0.25) def row_spacing(self): @@ -136,9 +139,10 @@ class AutoFill(EmbroideryElement): @property @param('max_stitch_length_mm', _('Maximum fill stitch length'), - tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=3.0) def max_stitch_length(self): @@ -147,10 +151,11 @@ class AutoFill(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=4) def staggers(self): return max(self.get_int_param("staggers", 4), 1) @@ -162,10 +167,10 @@ class AutoFill(EmbroideryElement): # ensure path length for i, path in enumerate(paths): if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + paths[i] = [(path[0][0], path[0][1]), (path[0][0] + + 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] return paths - @property @cache def outline(self): @@ -176,19 +181,16 @@ class AutoFill(EmbroideryElement): def outline_length(self): return self.outline.length - @property - def flip(self): - return False - @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 4) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -200,7 +202,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -211,7 +214,8 @@ class AutoFill(EmbroideryElement): if underlay_angles is not None: underlay_angles = underlay_angles.strip().split(',') try: - underlay_angles = [math.radians(float(angle)) for angle in underlay_angles] + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] except (TypeError, ValueError): return default_value else: @@ -243,7 +247,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -266,12 +271,13 @@ class AutoFill(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, - sort_index = 5, - select_items=[('fill_method',0),('fill_method',2)]) + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) def expand(self): return self.get_float_param('expand_mm', 0) @@ -283,8 +289,8 @@ class AutoFill(EmbroideryElement): 'are not visible. This gives them a jagged appearance.'), type='boolean', default=True, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 6) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) def underpath(self): return self.get_boolean_param('underpath', True) @@ -308,7 +314,8 @@ class AutoFill(EmbroideryElement): # from the first. So let's at least make sure the "first" thing is the # biggest path. paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + paths.sort(key=lambda point_list: shgeo.Polygon( + point_list).area, reverse=True) # Very small holes will cause a shape to be rendered as an outline only # they are too small to be rendered and only confuse the auto_fill algorithm. # So let's ignore them @@ -397,7 +404,7 @@ class AutoFill(EmbroideryElement): color=self.color, tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( - self.underlay_shape, + self.underlay_shape, None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, @@ -410,8 +417,8 @@ class AutoFill(EmbroideryElement): underpath=self.underlay_underpath)) stitch_groups.append(underlay) starting_point = underlay.stitches[-1] - - if self.fill_method == 0: #Auto Fill + + if self.fill_method == 0: # Auto Fill stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), @@ -429,30 +436,31 @@ class AutoFill(EmbroideryElement): ending_point, self.underpath)) stitch_groups.append(stitch_group) - elif self.fill_method == 1: #Tangential Fill + elif self.fill_method == 1: # Tangential Fill polygons = list(self.fill_shape) if not starting_point: - starting_point = (0,0) + starting_point = (0, 0) for poly in polygons: connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, self.interlaced, self.tangential_strategy, shgeo.Point(starting_point)) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) stitch_groups.append(stitch_group) - elif self.fill_method == 2: #Guided Auto Fill - lines = get_patterns(self.node,"#inkstitch-guide-line-marker") + elif self.fill_method == 2: # Guided Auto Fill + lines = get_patterns(self.node, "#inkstitch-guide-line-marker") lines = lines['stroke_patterns'] if not lines or lines[0].is_empty: - inkex.errormsg(_("No line marked as guide line found within the same group as patch")) + inkex.errormsg( + _("No line marked as guide line found within the same group as patch")) else: stitch_group = StitchGroup( color=self.color, diff --git a/lib/elements/clone.py b/lib/elements/clone.py index bcecf3f0..15e7591c 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -14,7 +14,6 @@ from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, from ..utils import cache from .auto_fill import AutoFill from .element import EmbroideryElement, param -#from .fill import Fill from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -79,9 +78,9 @@ class Clone(EmbroideryElement): else: elements = [] if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke", self.node) is not None: if not is_command(element.node): diff --git a/lib/elements/element.py b/lib/elements/element.py index b8728f60..ef70510d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -33,7 +33,6 @@ class Param(object): self.tooltip = tooltip self.sort_index = sort_index self.select_items = select_items - #print("IN PARAM: ", self.values) def __repr__(self): return "Param(%s)" % vars(self) @@ -164,7 +163,8 @@ class EmbroideryElement(object): # Of course, transforms may also involve rotation, skewing, and translation. # All except translation can affect how wide the stroke appears on the screen. - node_transform = inkex.transforms.Transform(get_node_transform(self.node)) + node_transform = inkex.transforms.Transform( + get_node_transform(self.node)) # First, figure out the translation component of the transform. Using a zero # vector completely cancels out the rotation, scale, and skew components. @@ -198,7 +198,8 @@ class EmbroideryElement(object): @property @param('ties', _('Allow lock stitches'), - tooltip=_('Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), + tooltip=_( + 'Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), type='dropdown', # Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither # L10N options to allow lock stitch before and after objects @@ -256,7 +257,8 @@ class EmbroideryElement(object): d = self.node.get("d", "") if not d: - self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id"))) + self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict( + id=self.node.get("id"))) return inkex.paths.Path(d).to_superpath() @@ -266,7 +268,8 @@ class EmbroideryElement(object): @property def shape(self): - raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + raise NotImplementedError( + "INTERNAL ERROR: %s must implement shape()", self.__class__) @property @cache @@ -316,7 +319,8 @@ class EmbroideryElement(object): return self.get_boolean_param('stop_after', False) def to_stitch_groups(self, last_patch): - raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__) + raise NotImplementedError( + "%s must implement to_stitch_groups()" % self.__class__.__name__) def embroider(self, last_patch): self.validate() @@ -329,8 +333,10 @@ class EmbroideryElement(object): patch.force_lock_stitches = self.force_lock_stitches if patches: - patches[-1].trim_after = self.has_command("trim") or self.trim_after - patches[-1].stop_after = self.has_command("stop") or self.stop_after + patches[-1].trim_after = self.has_command( + "trim") or self.trim_after + patches[-1].stop_after = self.has_command( + "stop") or self.stop_after return patches diff --git a/lib/elements/utils.py b/lib/elements/utils.py index f858cc81..9fec8b63 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -11,7 +11,6 @@ from .auto_fill import AutoFill from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill from .image import ImageObject from .pattern import PatternObject from .polyline import Polyline @@ -41,9 +40,9 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke"): if not is_command(element.node): diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 8021d5d7..30f6ba1d 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,9 +7,9 @@ import os import sys -from collections import defaultdict,namedtuple +from collections import defaultdict from copy import copy -from itertools import groupby,zip_longest +from itertools import groupby, zip_longest import wx from wx.lib.scrolledpanel import ScrolledPanel @@ -25,14 +25,11 @@ from ..utils import get_resource_dir from .base import InkstitchExtension -#ChoiceWidgets = namedtuple("ChoiceWidgets", "param widget last_initialized_choice") - - - def grouper(iterable_obj, count, fillvalue=None): args = [iter(iterable_obj)] * count return zip_longest(*args, fillvalue=fillvalue) + class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) @@ -56,14 +53,16 @@ class ParamsTab(ScrolledPanel): if toggles: self.toggle = toggles[0] self.params.remove(self.toggle) - self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description) + self.toggle_checkbox = wx.CheckBox( + self, label=self.toggle.description) value = any(self.toggle.values) if self.toggle.inverse: value = not value self.toggle_checkbox.SetValue(value) - self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state) + self.toggle_checkbox.Bind( + wx.EVT_CHECKBOX, self.update_toggle_state) self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed) self.param_inputs[self.toggle.name] = self.toggle_checkbox @@ -76,7 +75,8 @@ class ParamsTab(ScrolledPanel): self.settings_grid.AddGrowableCol(1, 2) self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) - self.pencil_icon = wx.Image(os.path.join(get_resource_dir("icons"), "pencil_20x20.png")).ConvertToBitmap() + self.pencil_icon = wx.Image(os.path.join(get_resource_dir( + "icons"), "pencil_20x20.png")).ConvertToBitmap() self.__set_properties() self.__do_layout() @@ -230,19 +230,25 @@ class ParamsTab(ScrolledPanel): if len(self.nodes) == 1: description = _("These settings will be applied to 1 object.") else: - description = _("These settings will be applied to %d objects.") % len(self.nodes) + description = _( + "These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + description += "\n • " + \ + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: if len(self.dependent_tabs) == 1: - description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + description += "\n • " + \ + _("Disabling this tab will disable the following %d tabs.") % len( + self.dependent_tabs) else: - description += "\n • " + _("Disabling this tab will disable the following tab.") + description += "\n • " + \ + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + description += "\n • " + \ + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -268,21 +274,21 @@ class ParamsTab(ScrolledPanel): # end wxGlade pass - #choice tuple is None or contains ("choice widget param name", "actual selection") - def update_choice_widgets(self, choice_tuple = None): - if choice_tuple == None: #update all choices + # choice tuple is None or contains ("choice widget param name", "actual selection") + def update_choice_widgets(self, choice_tuple=None): + if choice_tuple is None: # update all choices for choice in self.dict_of_choices.values(): - self.update_choice_widgets((choice["param"].name, choice["widget"].GetSelection())) + self.update_choice_widgets( + (choice["param"].name, choice["widget"].GetSelection())) else: choice = self.dict_of_choices[choice_tuple[0]] - last_selection = choice["last_initialized_choice"] + last_selection = choice["last_initialized_choice"] current_selection = choice["widget"].GetSelection() - if last_selection != -1 and last_selection != current_selection: #Hide the old widgets + if last_selection != -1 and last_selection != current_selection: # Hide the old widgets for widget in self.choice_widgets[(choice["param"].name, last_selection)]: widget.Hide() - #self.settings_grid.Detach(widget) - - #choice_index = self.settings_grid.GetChildren().index(self.settings_grid.GetItem(choice["widget"])) #TODO: is there a better way to get the index in the sizer? + # self.settings_grid.Detach(widget) + for widgets in grouper(self.choice_widgets[choice_tuple], 4): widgets[0].Show(True) widgets[1].Show(True) @@ -295,20 +301,24 @@ class ParamsTab(ScrolledPanel): # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) - summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects")) + summary_box = wx.StaticBox( + self, wx.ID_ANY, label=_("Inkscape objects")) sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL) self.description = wx.StaticText(self) self.update_description() self.description.SetLabel(self.description_text) self.description_container = box self.Bind(wx.EVT_SIZE, self.resized) - sizer.Add(self.description, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) + sizer.Add(self.description, proportion=0, + flag=wx.EXPAND | wx.ALL, border=5) box.Add(sizer, proportion=0, flag=wx.ALL, border=5) if self.toggle: toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) - toggle_sizer.Add(self.create_change_indicator(self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) - toggle_sizer.Add(self.toggle_checkbox, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + toggle_sizer.Add(self.create_change_indicator( + self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) + toggle_sizer.Add(self.toggle_checkbox, proportion=0, + flag=wx.ALIGN_CENTER_VERTICAL) box.Add(toggle_sizer, proportion=0, flag=wx.BOTTOM, border=10) for param in self.params: @@ -316,14 +326,16 @@ class ParamsTab(ScrolledPanel): description = wx.StaticText(self, label=param.description) description.SetToolTip(param.tooltip) - if param.select_items != None: + if param.select_items is not None: col1.Hide() description.Hide() for item in param.select_items: self.choice_widgets[item].extend([col1, description]) - #else: - self.settings_grid.Add(col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) + # else: + self.settings_grid.Add( + col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | + wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) if param.type == 'boolean': if len(param.values) > 1: @@ -340,9 +352,11 @@ class ParamsTab(ScrolledPanel): input.SetSelection(int(param.values[0])) input.Bind(wx.EVT_CHOICE, self.changed) input.Bind(wx.EVT_CHOICE, self.update_choice_state) - self.dict_of_choices[param.name] = {"param": param, "widget": input, "last_initialized_choice": 1} + self.dict_of_choices[param.name] = { + "param": param, "widget": input, "last_initialized_choice": 1} elif len(param.values) > 1: - input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(str(value) for value in param.values), style=wx.CB_DROPDOWN) + input = wx.ComboBox(self, wx.ID_ANY, choices=sorted( + str(value) for value in param.values), style=wx.CB_DROPDOWN) input.Bind(wx.EVT_COMBOBOX, self.changed) input.Bind(wx.EVT_TEXT, self.changed) else: @@ -354,14 +368,16 @@ class ParamsTab(ScrolledPanel): col4 = wx.StaticText(self, label=param.unit or "") - if param.select_items != None: + if param.select_items is not None: input.Hide() col4.Hide() for item in param.select_items: self.choice_widgets[item].extend([input, col4]) - #else: - self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) - self.settings_grid.Add(col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + # else: + self.settings_grid.Add( + input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) + self.settings_grid.Add( + col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} @@ -372,16 +388,20 @@ class ParamsTab(ScrolledPanel): self.Layout() def create_change_indicator(self, param): - indicator = wx.Button(self, style=wx.BORDER_NONE | wx.BU_NOTEXT, size=(28, 28)) - indicator.SetToolTip(_('Click to force this parameter to be saved when you click "Apply and Quit"')) - indicator.Bind(wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) + indicator = wx.Button(self, style=wx.BORDER_NONE | + wx.BU_NOTEXT, size=(28, 28)) + indicator.SetToolTip( + _('Click to force this parameter to be saved when you click "Apply and Quit"')) + indicator.Bind( + wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) self.param_change_indicators[param] = indicator return indicator def enable_change_indicator(self, param): self.param_change_indicators[param].SetBitmapLabel(self.pencil_icon) - self.param_change_indicators[param].SetToolTip(_('This parameter will be saved when you click "Apply and Quit"')) + self.param_change_indicators[param].SetToolTip( + _('This parameter will be saved when you click "Apply and Quit"')) self.changed_inputs.add(self.param_inputs[param]) @@ -407,7 +427,8 @@ class SettingsFrame(wx.Frame): _("Embroidery Params") ) - icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png")) + icon = wx.Icon(os.path.join( + get_resource_dir("icons"), "inkstitch256x256.png")) self.SetIcon(icon) self.notebook = wx.Notebook(self, wx.ID_ANY) @@ -425,7 +446,8 @@ class SettingsFrame(wx.Frame): self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) self.Bind(wx.EVT_CLOSE, self.cancel) - self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings")) + self.use_last_button = wx.Button( + self, wx.ID_ANY, _("Use Last Settings")) self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last) self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) @@ -544,7 +566,8 @@ class SettingsFrame(wx.Frame): for tab in self.tabs: self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.warning_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) - sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + sizer_1.Add(self.notebook, 1, wx.EXPAND | + wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) sizer_3.Add(self.cancel_button, 0, wx.RIGHT, 5) sizer_3.Add(self.use_last_button, 0, wx.RIGHT | wx.BOTTOM, 5) @@ -584,7 +607,7 @@ class Params(InkstitchExtension): else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": classes.append(AutoFill) - #classes.append(Fill) + # classes.append(Fill) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: @@ -611,7 +634,8 @@ class Params(InkstitchExtension): else: getter = 'get_param' - values = [item for item in (getattr(node, getter)(param.name, param.default) for node in nodes) if item is not None] + values = [item for item in (getattr(node, getter)( + param.name, param.default) for node in nodes) if item is not None] return values @@ -677,7 +701,8 @@ class Params(InkstitchExtension): for group, params in self.group_params(params): tab_name = group or cls.element_name - tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, params=list(params), nodes=nodes) + tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, + params=list(params), nodes=nodes) new_tabs.append(tab) if group == "": @@ -697,14 +722,16 @@ class Params(InkstitchExtension): def effect(self): try: app = wx.App() - frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) + frame = SettingsFrame( + tabs_factory=self.create_tabs, on_cancel=self.cancel) # position left, center current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((int(display_size[0]), int(display_size[3]/2 - frame_size[1]/2))) + frame.SetPosition((int(display_size[0]), int( + display_size[3]/2 - frame_size[1]/2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py index 85a44bb1..e11cdb4e 100644 --- a/lib/extensions/selection_to_guide_line.py +++ b/lib/extensions/selection_to_guide_line.py @@ -18,11 +18,13 @@ class SelectionToGuideLine(InkstitchExtension): return if not self.svg.selected: - inkex.errormsg(_("Please select one object to be marked as a guide line.")) + inkex.errormsg( + _("Please select one object to be marked as a guide line.")) return - if len(self.get_nodes())!=1: - inkex.errormsg(_("Please select only one object to be marked as a guide line.")) + if len(self.get_nodes()) != 1: + inkex.errormsg( + _("Please select only one object to be marked as a guide line.")) return for guide_line in self.get_nodes(): diff --git a/lib/patterns.py b/lib/patterns.py index b4b60522..789d5f89 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -19,7 +19,7 @@ def is_pattern(node): def apply_patterns(patches, node): - patterns = get_patterns(node,"#inkstitch-pattern-marker") + patterns = get_patterns(node, "#inkstitch-pattern-marker") _apply_fill_patterns(patterns['fill_patterns'], patches) _apply_stroke_patterns(patterns['stroke_patterns'], patches) @@ -32,7 +32,8 @@ def _apply_stroke_patterns(patterns, patches): patch_points.append(stitch) if i == len(patch.stitches) - 1: continue - intersection_points = _get_pattern_points(stitch, patch.stitches[i+1], pattern) + intersection_points = _get_pattern_points( + stitch, patch.stitches[i+1], pattern) for point in intersection_points: patch_points.append(Stitch(point, tags=('pattern_point',))) patch.stitches = patch_points diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 21a56cd6..9b3572d9 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -1,6 +1,6 @@ from shapely.geometry.polygon import LineString, LinearRing -from shapely.geometry import Point, MultiPoint, linestring -from shapely.ops import nearest_points, polygonize +from shapely.geometry import Point, MultiPoint +from shapely.ops import nearest_points from collections import namedtuple from depq import DEPQ import math @@ -8,11 +8,22 @@ from ..stitches import LineStringSampling from ..stitches import PointTransfer from ..stitches import constants -nearest_neighbor_tuple = namedtuple('nearest_neighbor_tuple', ['nearest_point_parent', 'nearest_point_child', 'projected_distance_parent', 'child_node']) +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) -# Cuts a closed line so that the new closed line starts at the point with "distance" to the beginning of the old line. def cut(line, distance): + """ + Cuts a closed line so that the new closed line starts at the + point with "distance" to the beginning of the old line. + """ if distance <= 0.0 or distance >= line.length: return [LineString(line)] coords = list(line.coords) @@ -23,29 +34,41 @@ def cut(line, distance): pd = line.project(Point(p)) if pd == distance: if coords[0] == coords[-1]: - return LineString(coords[i:]+coords[1:i+1]) + return LineString(coords[i:] + coords[1: i + 1]) else: - return LineString(coords[i:]+coords[:i]) + return LineString(coords[i:] + coords[:i]) if pd > distance: cp = line.interpolate(distance) if coords[0] == coords[-1]: - return LineString([(cp.x, cp.y)] + coords[i:]+coords[1:i]+[(cp.x, cp.y)]) + return LineString( + [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] + ) else: - return LineString([(cp.x, cp.y)] + coords[i:]+coords[:i]) - - -#Takes the offsetted curves organized as tree, connects and samples them. -#Strategy: A connection from parent to child is made where both curves come closest together. -#Input: -#-tree: contains the offsetted curves in a hierachical organized data structure. -#-used_offset: used offset when the offsetted curves were generated -#-stitch_distance: maximum allowed distance between two points after sampling -#-close_point: defines the beginning point for stitching (stitching starts always from the undisplaced curve) -#-offset_by_half: If true the resulting points are interlaced otherwise not. -#Returnvalues: -#-All offsetted curves connected to one line and sampled with points obeying stitch_distance and offset_by_half -#-Tag (origin) of each point to analyze why a point was placed at this position -def connect_raster_tree_nearest_neighbor(tree, used_offset, stitch_distance, close_point, offset_by_half): + return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) + + +def connect_raster_tree_nearest_neighbor( + tree, used_offset, stitch_distance, close_point, offset_by_half +): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made where both curves + come closest together. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ current_coords = tree.val abs_offset = abs(used_offset) @@ -60,176 +83,285 @@ def connect_raster_tree_nearest_neighbor(tree, used_offset, stitch_distance, clo if not tree.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item,priority in tree.transferred_point_priority_deque: - new_DEPQ.insert(item, math.fmod( - priority-start_distance+current_coords.length, current_coords.length)) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) tree.transferred_point_priority_deque = new_DEPQ - #print("Gecutted") stitching_direction = 1 - # This list should contain a tuple of nearest points between the current geometry - # and the subgeometry, the projected distance along the current geometry, - # and the belonging subtree node + # This list should contain a tuple of nearest points between + # the current geometry and the subgeometry, the projected + # distance along the current geometry, and the belonging subtree node nearest_points_list = [] - + for subnode in tree.children: point_parent, point_child = nearest_points(current_coords, subnode.val) proj_distance = current_coords.project(point_parent) - nearest_points_list.append(nearest_neighbor_tuple(nearest_point_parent = point_parent, - nearest_point_child = point_child, - projected_distance_parent = proj_distance, - child_node=subnode)) - nearest_points_list.sort(reverse=False, key=lambda tup: tup.projected_distance_parent) + nearest_points_list.append( + nearest_neighbor_tuple( + nearest_point_parent=point_parent, + nearest_point_child=point_child, + proj_distance_parent=proj_distance, + child_node=subnode, + ) + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) if nearest_points_list: - start_distance = min(abs_offset*constants.factor_offset_starting_points, nearest_points_list[0].projected_distance_parent) - end_distance = max(current_coords.length-abs_offset*constants.factor_offset_starting_points, nearest_points_list[-1].projected_distance_parent) + start_distance = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_distance = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) else: - start_distance = abs_offset*constants.factor_offset_starting_points - end_distance = current_coords.length-abs_offset*constants.factor_offset_starting_points - - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, start_distance, # We add/subtract an offset to not sample the same point again (avoid double points for start and end) - end_distance, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) - assert(len(own_coords) == len(own_coords_origin)) + start_distance = abs_offset * constants.factor_offset_starting_points + end_distance = ( + current_coords.length - abs_offset * constants.factor_offset_starting_points + ) + + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + start_distance, # We add/subtract an offset to not sample + # the same point again (avoid double + # points for start and end) + end_distance, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) + assert len(own_coords) == len(own_coords_origin) own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - - #tree.val = LineString(own_coords) - #tree.pointsourcelist = own_coords_origin tree.stitching_direction = stitching_direction tree.already_rastered = True - #Next we need to transfer our rastered points to siblings and childs + # Next we need to transfer our rastered points to siblings and childs to_transfer_point_list = [] to_transfer_point_list_origin = [] - for k in range(1, len(own_coords)-1): #Do not take the first and the last since they are ENTER_LEAVING_POINT points for sure - # if abs(temp[k][0]-5.25) < 0.5 and abs(temp[k][1]-42.9) < 0.5: - # print("HIER gefunden!") - if (not offset_by_half and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED): + for k in range(1, len(own_coords) - 1): + # Do not take the first and the last since they are ENTER_LEAVING_POINT + # points for sure + + if ( + not offset_by_half + and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED + ): continue - if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT: + if ( + own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT + or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT + ): continue to_transfer_point_list.append(Point(own_coords[k])) - point_origin = own_coords_origin[k] + point_origin = own_coords_origin[k] to_transfer_point_list_origin.append(point_origin) - - #since the projection is only in ccw direction towards inner we need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,offset_by_half,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=False, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - - - #We transfer also to the overnext child to get a more straight arrangement of points perpendicular to the stitching lines + # Since the projection is only in ccw direction towards inner we need + # to use "-used_offset" for stitching_direction==-1 + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines if offset_by_half: - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,False,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=True, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) if not nearest_points_list: - #If there is no child (inner geometry) we can simply take our own rastered coords as result + # If there is no child (inner geometry) we can simply take + # our own rastered coords as result result_coords = own_coords result_coords_origin = own_coords_origin else: - #There are childs so we need to merge their coordinates with our own rastered coords + # There are childs so we need to merge their coordinates + + # with our own rastered coords - #To create a closed ring + # To create a closed ring own_coords.append(own_coords[0]) own_coords_origin.append(own_coords_origin[0]) - - #own_coords does not start with current_coords but has an offset (see call of raster_line_string_with_priority_points) + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) total_distance = start_distance - current_item_index = 0 + cur_item = 0 result_coords = [own_coords[0]] - result_coords_origin = [LineStringSampling.PointSource.ENTER_LEAVING_POINT] + result_coords_origin = [ + LineStringSampling.PointSource.ENTER_LEAVING_POINT] for i in range(1, len(own_coords)): - next_distance = math.sqrt((own_coords[i][0]-own_coords[i-1][0])**2 + - (own_coords[i][1]-own_coords[i-1][1])**2) - while (current_item_index < len(nearest_points_list) and - total_distance+next_distance+constants.eps > nearest_points_list[current_item_index].projected_distance_parent): - - item = nearest_points_list[current_item_index] - child_coords, child_coords_origin = connect_raster_tree_nearest_neighbor( - item.child_node, used_offset, stitch_distance, item.nearest_point_child, offset_by_half) - - delta = item.nearest_point_parent.distance(Point(own_coords[i-1])) - if delta > abs_offset*constants.factor_offset_starting_points: + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_nearest_neighbor( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + d = item.nearest_point_parent.distance( + Point(own_coords[i - 1])) + if d > abs_offset * constants.factor_offset_starting_points: result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - # reversing avoids crossing when entering and leaving the child segment + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + # reversing avoids crossing when entering and + # leaving the child segment result_coords.extend(child_coords[::-1]) result_coords_origin.extend(child_coords_origin[::-1]) - - #And here we calculate the point for the leaving - delta = item.nearest_point_parent.distance(Point(own_coords[i])) - if current_item_index < len(nearest_points_list)-1: - delta = min(delta, abs( - nearest_points_list[current_item_index+1].projected_distance_parent-item.projected_distance_parent)) - - if delta > abs_offset*constants.factor_offset_starting_points: - result_coords.append(current_coords.interpolate( - item.projected_distance_parent+abs_offset*constants.factor_offset_starting_points).coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - - current_item_index += 1 - if i < len(own_coords)-1: - if(Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset*constants.factor_offset_remove_points): + # And here we calculate the point for the leaving + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + abs_offset * constants.factor_offset_starting_points + ).coords[0] + ) + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + + cur_item += 1 + if i < len(own_coords) - 1: + if ( + Point(result_coords[-1]).distance(Point(own_coords[i])) + > abs_offset * constants.factor_offset_remove_points + ): result_coords.append(own_coords[i]) result_coords_origin.append(own_coords_origin[i]) - # Since current_coords and temp are rastered differently there accumulate errors regarding the current distance. - # Since a projection of each point in temp would be very time consuming we project only every n-th point which resets the accumulated error every n-th point. + # Since current_coords and temp are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in temp would be very time + # consuming we project only every n-th point which resets + # the accumulated error every n-th point. if i % 20 == 0: total_distance = current_coords.project(Point(own_coords[i])) else: total_distance += next_distance - assert(len(result_coords) == len(result_coords_origin)) + assert len(result_coords) == len(result_coords_origin) return result_coords, result_coords_origin -#Takes a line and calculates the nearest distance along this line to enter the next_line -#Input: -#-travel_line: The "parent" line for which the distance should be minimized to enter next_line -#-next_line: contains the next_line which need to be entered -#-thresh: The distance between travel_line and next_line needs to below thresh to be a valid point for entering -#Output: -#-tuple - the tuple structure is: (nearest point in travel_line, nearest point in next_line) -def get_nearest_points_closer_than_thresh(travel_line, next_line,thresh): - point_list = list(MultiPoint(travel_line.coords)) + +def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): + """ + Takes a line and calculates the nearest distance along this + line to enter the next_line + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter next_line + -next_line: contains the next_line which need to be entered + -thresh: The distance between travel_line and next_line needs + to below thresh to be a valid point for entering + Output: + -tuple - the tuple structure is: + (nearest point in travel_line, nearest point in next_line) + """ + point_list = list(MultiPoint(travel_line.coords)) if point_list[0].distance(next_line) < thresh: return nearest_points(point_list[0], next_line) - for i in range(len(point_list)-1): - line_segment = LineString([point_list[i], point_list[i+1]]) - result = nearest_points(line_segment,next_line) + for i in range(len(point_list) - 1): + line_segment = LineString([point_list[i], point_list[i + 1]]) + result = nearest_points(line_segment, next_line) - if result[0].distance(result[1])< thresh: + if result[0].distance(result[1]) < thresh: return result line_segment = LineString([point_list[-1], point_list[0]]) - result = nearest_points(line_segment,next_line) + result = nearest_points(line_segment, next_line) - if result[0].distance(result[1])< thresh: + if result[0].distance(result[1]) < thresh: return result else: return None -#Takes a line and calculates the nearest distance along this line to enter the childs in children_list -#The method calculates the distances along the line and along the reversed line to find the best direction -#which minimizes the overall distance for all childs. -#Input: -#-travel_line: The "parent" line for which the distance should be minimized to enter the childs -#-children_list: contains the childs of travel_line which need to be entered -#-threshold: The distance between travel_line and a child needs to below threshold to be a valid point for entering -#-preferred_direction: Put a bias on the desired travel direction along travel_line. If equals zero no bias is applied. -# preferred_direction=1 means we prefer the direction of travel_line; preferred_direction=-1 means we prefer the opposite direction. -#Output: -#-stitching direction for travel_line -#-list of tuples (one tuple per child). The tuple structure is: ((nearest point in travel_line, nearest point in child), distance along travel_line, belonging child) -def create_nearest_points_list(travel_line, children_list, threshold, threshold_hard,preferred_direction=0): +def create_nearest_points_list( + travel_line, children_list, threshold, threshold_hard, preferred_direction=0 +): + """ + Takes a line and calculates the nearest distance along this line to + enter the childs in children_list + The method calculates the distances along the line and along the + reversed line to find the best direction which minimizes the overall + distance for all childs. + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter the childs + -children_list: contains the childs of travel_line which need to be entered + -threshold: The distance between travel_line and a child needs to be + below threshold to be a valid point for entering + -preferred_direction: Put a bias on the desired travel direction along + travel_line. If equals zero no bias is applied. + preferred_direction=1 means we prefer the direction of travel_line; + preferred_direction=-1 means we prefer the opposite direction. + Output: + -stitching direction for travel_line + -list of tuples (one tuple per child). The tuple structure is: + ((nearest point in travel_line, nearest point in child), + distance along travel_line, belonging child) + """ + result_list_in_order = [] result_list_reversed_order = [] @@ -238,67 +370,113 @@ def create_nearest_points_list(travel_line, children_list, threshold, threshold_ weight_in_order = 0 weight_reversed_order = 0 for child in children_list: - result = get_nearest_points_closer_than_thresh(travel_line, child.val, threshold) - if result == None: #where holes meet outer borders a distance up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh(travel_line, child.val, threshold_hard) - assert(result != None) + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold_hard + ) + assert result is not None proj = travel_line.project(result[0]) weight_in_order += proj - result_list_in_order.append(nearest_neighbor_tuple(nearest_point_parent = result[0], - nearest_point_child = result[1], - projected_distance_parent = proj, - child_node = child)) - - result = get_nearest_points_closer_than_thresh(travel_line_reversed, child.val, threshold) - if result == None: #where holes meet outer borders a distance up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh(travel_line_reversed, child.val, threshold_hard) - assert(result != None) + result_list_in_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold_hard + ) + assert result is not None proj = travel_line_reversed.project(result[0]) weight_reversed_order += proj - result_list_reversed_order.append(nearest_neighbor_tuple(nearest_point_parent = result[0], - nearest_point_child = result[1], - projected_distance_parent = proj, - child_node = child)) + result_list_reversed_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) if preferred_direction == 1: - weight_in_order=min(weight_in_order/2, max(0, weight_in_order-10*threshold)) + # Reduce weight_in_order to make in order stitching more preferred + weight_in_order = min( + weight_in_order / 2, max(0, weight_in_order - 10 * threshold) + ) if weight_in_order == weight_reversed_order: return (1, result_list_in_order) elif preferred_direction == -1: - weight_reversed_order=min(weight_reversed_order/2, max(0, weight_reversed_order-10*threshold)) + # Reduce weight_reversed_order to make reversed + # stitching more preferred + weight_reversed_order = min( + weight_reversed_order / + 2, max(0, weight_reversed_order - 10 * threshold) + ) if weight_in_order == weight_reversed_order: return (-1, result_list_reversed_order) - if weight_in_order < weight_reversed_order: return (1, result_list_in_order) else: return (-1, result_list_reversed_order) -def calculate_replacing_middle_point(line_segment, abs_offset,max_stich_distance): +def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): + """ + Takes a line segment (consisting of 3 points!) + and calculates a new middle point if the line_segment is + straight enough to be resampled by points max_stitch_distance apart. + Returns None if the middle point is not needed. + """ angles = LineStringSampling.calculate_line_angles(line_segment) - if angles[1] < abs_offset*constants.limiting_angle_straight: - if line_segment.length < max_stich_distance: + if angles[1] < abs_offset * constants.limiting_angle_straight: + if line_segment.length < max_stitch_distance: return None else: - return line_segment.interpolate(line_segment.length-max_stich_distance).coords[0] + return line_segment.interpolate( + line_segment.length - max_stitch_distance + ).coords[0] else: return line_segment.coords[1] -#Takes the offsetted curves organized as tree, connects and samples them. -#Strategy: A connection from parent to child is made as fast as possible to reach the innermost child as fast as possible in order -# to stich afterwards from inner to outer. -#Input: -#-tree: contains the offsetted curves in a hierachical organized data structure. -#-used_offset: used offset when the offsetted curves were generated -#-stitch_distance: maximum allowed distance between two points after sampling -#-close_point: defines the beginning point for stitching (stitching starts always from the undisplaced curve) -#-offset_by_half: If true the resulting points are interlaced otherwise not. -#Returnvalues: -#-All offsetted curves connected to one line and sampled with points obeying stitch_distance and offset_by_half -#-Tag (origin) of each point to analyze why a point was placed at this position -def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): + +def connect_raster_tree_from_inner_to_outer( + tree, used_offset, stitch_distance, close_point, offset_by_half +): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made as fast as possible to + reach the innermost child as fast as possible in order to stitch afterwards + from inner to outer. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with points obeying + stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was placed + at this position + """ current_coords = tree.val abs_offset = abs(used_offset) @@ -314,164 +492,280 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, if not tree.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert(item, math.fmod( - priority-start_distance+current_coords.length, current_coords.length)) + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) tree.transferred_point_priority_deque = new_DEPQ - #We try to use always the opposite stitching direction with respect to the parent to avoid crossings when entering and leaving the child + # We try to use always the opposite stitching direction with respect to the + # parent to avoid crossings when entering and leaving the child parent_stitching_direction = -1 - if tree.parent != None: + if tree.parent is not None: parent_stitching_direction = tree.parent.stitching_direction - #find the nearest point in current_coords and its children and sort it along the stitching direction - stitching_direction, nearest_points_list = create_nearest_points_list(current_coords, tree.children, 1.5*abs_offset,2.05*abs_offset,parent_stitching_direction) - nearest_points_list.sort(reverse=False, key=lambda tup: tup.projected_distance_parent) - - #Have a small offset for the starting and ending to avoid double points at start and end point (since the paths are closed rings) + # Find the nearest point in current_coords and its children and + # sort it along the stitching direction + stitching_direction, nearest_points_list = create_nearest_points_list( + current_coords, + tree.children, + 1.5 * abs_offset, + 2.05 * abs_offset, + parent_stitching_direction, + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + # Have a small offset for the starting and ending to avoid double points + # at start and end point (since the paths are closed rings) if nearest_points_list: - start_offset = min(abs_offset*constants.factor_offset_starting_points, nearest_points_list[0].projected_distance_parent) - end_offset = max(current_coords.length-abs_offset*constants.factor_offset_starting_points, nearest_points_list[-1].projected_distance_parent) + start_offset = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_offset = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) else: - start_offset = abs_offset*constants.factor_offset_starting_points - end_offset = current_coords.length-abs_offset*constants.factor_offset_starting_points - + start_offset = abs_offset * constants.factor_offset_starting_points + end_offset = ( + current_coords.length - abs_offset * constants.factor_offset_starting_points + ) if stitching_direction == 1: - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, start_offset, # We add start_offset to not sample the same point again (avoid double points for start and end) - end_offset, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + start_offset, # We add start_offset to not sample the same + # point again (avoid double points for start + # and end) + end_offset, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) else: - own_coords, own_coords_origin = LineStringSampling.raster_line_string_with_priority_points(current_coords, current_coords.length-start_offset, # We subtract start_offset to not sample the same point again (avoid double points for start and end) - current_coords.length-end_offset, stitch_distance, stitching_direction, tree.transferred_point_priority_deque, abs_offset) - current_coords.coords = current_coords.coords[::-1] - - #Adjust the points origin for start and end (so that they might not be transferred to childs) - #if own_coords_origin[-1] != LineStringSampling.PointSource.HARD_EDGE: - # own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - #if own_coords_origin[0] != LineStringSampling.PointSource.HARD_EDGE: - # own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - assert(len(own_coords) == len(own_coords_origin)) - - #tree.val = LineString(own_coords) - #tree.pointsourcelist = own_coords_origin + ( + own_coords, + own_coords_origin, + ) = LineStringSampling.raster_line_string_with_priority_points( + current_coords, + current_coords.length - start_offset, # We subtract + # start_offset to not + # sample the same point + # again (avoid double + # points for start + # and end) + current_coords.length - end_offset, + stitch_distance, + stitching_direction, + tree.transferred_point_priority_deque, + abs_offset, + ) + current_coords.coords = current_coords.coords[::-1] + + assert len(own_coords) == len(own_coords_origin) + tree.stitching_direction = stitching_direction tree.already_rastered = True - to_transfer_point_list = [] to_transfer_point_list_origin = [] - for k in range(0, len(own_coords)): #TODO: maybe do not take the first and the last since they are ENTER_LEAVING_POINT points for sure - if (not offset_by_half and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT): + for k in range(0, len(own_coords)): + # TODO: maybe do not take the first and the last + # since they are ENTER_LEAVING_POINT points for sure + if ( + not offset_by_half + and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED + or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT + ): continue if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT: continue to_transfer_point_list.append(Point(own_coords[k])) to_transfer_point_list_origin.append(own_coords_origin[k]) - assert(len(to_transfer_point_list) == len(to_transfer_point_list_origin)) - - - #Next we need to transfer our rastered points to siblings and childs - - - #since the projection is only in ccw direction towards inner we need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,offset_by_half,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=False, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - - - #We transfer also to the overnext child to get a more straight arrangement of points perpendicular to the stitching lines + assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) + + # Next we need to transfer our rastered points to siblings and childs + # Since the projection is only in ccw direction towards inner we + # need to use "-used_offset" for stitching_direction==-1 + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines if offset_by_half: - PointTransfer.transfer_points_to_surrounding(tree,stitching_direction*used_offset,False,stitch_distance, - to_transfer_point_list,to_transfer_point_list_origin,overnext_neighbor=True, - transfer_forbidden_points=False,transfer_to_parent=False,transfer_to_sibling=True,transfer_to_child=True) - + PointTransfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + if not nearest_points_list: - #If there is no child (inner geometry) we can simply take our own rastered coords as result + # If there is no child (inner geometry) we can simply + # take our own rastered coords as result result_coords = own_coords result_coords_origin = own_coords_origin else: - #There are childs so we need to merge their coordinates with our own rastered coords + # There are childs so we need to merge their coordinates + # with our own rastered coords - #Create a closed ring for the following code + # Create a closed ring for the following code own_coords.append(own_coords[0]) own_coords_origin.append(own_coords_origin[0]) - # own_coords does not start with current_coords but has an offset (see call of raster_line_string_with_priority_points) + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) total_distance = start_offset - current_item_index = 0 + cur_item = 0 result_coords = [own_coords[0]] result_coords_origin = [own_coords_origin[0]] for i in range(1, len(own_coords)): - next_distance = math.sqrt((own_coords[i][0]-own_coords[i-1][0])**2 + - (own_coords[i][1]-own_coords[i-1][1])**2) - while (current_item_index < len(nearest_points_list) and - total_distance+next_distance+constants.eps > nearest_points_list[current_item_index].projected_distance_parent): - #The current and the next point in own_coords enclose the nearest point tuple between this geometry and the child geometry. - #Hence we need to insert the child geometry points here before the next point of own_coords. - item = nearest_points_list[current_item_index] - child_coords, child_coords_origin = connect_raster_tree_from_inner_to_outer( - item.child_node, used_offset, stitch_distance, item.nearest_point_child, offset_by_half) - - #Imagine the nearest point of the child is within a long segment of the parent. Without additonal points - #on the parent side this would cause noticeable deviations. Hence we add here points shortly before and after - #the entering of the child to have only minor deviations to the desired shape. - #Here is the point for the entering: - if(Point(result_coords[-1]).distance(item.nearest_point_parent) > constants.factor_offset_starting_points*abs_offset): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + # The current and the next point in own_coords enclose the + # nearest point tuple between this geometry and child + # geometry. Hence we need to insert the child geometry points + # here before the next point of own_coords. + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_from_inner_to_outer( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + # Imagine the nearest point of the child is within a long + # segment of the parent. Without additonal points + # on the parent side this would cause noticeable deviations. + # Hence we add here points shortly before and after + # the entering of the child to have only minor deviations to + # the desired shape. + # Here is the point for the entering: + if ( + Point(result_coords[-1] + ).distance(item.nearest_point_parent) + > constants.factor_offset_starting_points * abs_offset + ): result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #if (abs(result_coords[-1][0]-61.7) < 0.2 and abs(result_coords[-1][1]-105.1) < 0.2): - # print("HIIER FOUNDED3") - - #Check whether the number of points of the connecting lines from child to child can be reduced + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + + # Check whether the number of points of the connecting lines + # from child to child can be reduced if len(child_coords) > 1: - point = calculate_replacing_middle_point(LineString([result_coords[-1],child_coords[0],child_coords[1]]),abs_offset,stitch_distance) - #if (abs(result_coords[-1][0]-8.9) < 0.2 and abs(result_coords[-1][1]-8.9) < 0.2): - # print("HIIER FOUNDED3") - if point != None: - #if (abs(point[0]-17.8) < 0.2 and abs(point[1]-17.8) < 0.2): - # print("HIIER FOUNDED3") + point = calculate_replacing_middle_point( + LineString( + [result_coords[-1], child_coords[0], child_coords[1]] + ), + abs_offset, + stitch_distance, + ) + + if point is not None: result_coords.append(point) result_coords_origin.append(child_coords_origin[0]) - + result_coords.extend(child_coords[1:]) result_coords_origin.extend(child_coords_origin[1:]) else: result_coords.extend(child_coords) result_coords_origin.extend(child_coords_origin) - #And here is the point for the leaving of the child (distance to the own following point should not be too large) - delta = item.nearest_point_parent.distance(Point(own_coords[i])) - if current_item_index < len(nearest_points_list)-1: - delta = min(delta, abs( - nearest_points_list[current_item_index+1].projected_distance_parent-item.projected_distance_parent)) - - if delta > constants.factor_offset_starting_points*abs_offset: - result_coords.append(current_coords.interpolate( - item.projected_distance_parent+2*constants.factor_offset_starting_points*abs_offset).coords[0]) - result_coords_origin.append(LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #check whether this additional point makes the last point of the child unnecessary - point = calculate_replacing_middle_point(LineString([result_coords[-3],result_coords[-2],result_coords[-1]]),abs_offset,stitch_distance) - if point == None: + # And here is the point for the leaving of the child + # (distance to the own following point should not be too large) + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > constants.factor_offset_starting_points * abs_offset: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + 2 * constants.factor_offset_starting_points * abs_offset + ).coords[0] + ) + result_coords_origin.append( + LineStringSampling.PointSource.ENTER_LEAVING_POINT + ) + # Check whether this additional point makes the last point + # of the child unnecessary + point = calculate_replacing_middle_point( + LineString( + [result_coords[-3], result_coords[-2], result_coords[-1]] + ), + abs_offset, + stitch_distance, + ) + if point is None: result_coords.pop(-2) result_coords_origin.pop(-2) - #if (abs(result_coords[-1][0]-61.7) < 0.2 and abs(result_coords[-1][1]-105.1) < 0.2): - # print("HIIER FOUNDED3") - - current_item_index += 1 - if i < len(own_coords)-1: - if(Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset*constants.factor_offset_remove_points): + cur_item += 1 + if i < len(own_coords) - 1: + if ( + Point(result_coords[-1]).distance(Point(own_coords[i])) + > abs_offset * constants.factor_offset_remove_points + ): result_coords.append(own_coords[i]) result_coords_origin.append(own_coords_origin[i]) - # Since current_coords and own_coords are rastered differently there accumulate errors regarding the current distance. - # Since a projection of each point in own_coords would be very time consuming we project only every n-th point which resets the accumulated error every n-th point. + # Since current_coords and own_coords are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in own_coords would be very + # time consuming we project only every n-th point which resets + # the accumulated error every n-th point. if i % 20 == 0: total_distance = current_coords.project(Point(own_coords[i])) else: total_distance += next_distance - assert(len(result_coords) == len(result_coords_origin)) + assert len(result_coords) == len(result_coords_origin) return result_coords, result_coords_origin diff --git a/lib/stitches/DebuggingMethods.py b/lib/stitches/DebuggingMethods.py index d0f65576..e239edba 100644 --- a/lib/stitches/DebuggingMethods.py +++ b/lib/stitches/DebuggingMethods.py @@ -1,14 +1,11 @@ - import matplotlib.pyplot as plt from shapely.geometry import Polygon -from shapely.ops import nearest_points, substring, polygonize from anytree import PreOrderIter -from shapely.geometry.polygon import orient -#import LineStringSampling as Sampler + +# import LineStringSampling as Sampler import numpy as np import matplotlib.collections as mcoll -import matplotlib.path as mpath # def offset_polygons(polys, offset,joinstyle): # if polys.geom_type == 'Polygon': @@ -40,7 +37,7 @@ import matplotlib.path as mpath def plot_MultiPolygon(MultiPoly, plt, colorString): if MultiPoly.is_empty: return - if MultiPoly.geom_type == 'Polygon': + if MultiPoly.geom_type == "Polygon": x2, y2 = MultiPoly.exterior.xy plt.plot(x2, y2, colorString) @@ -56,6 +53,7 @@ def plot_MultiPolygon(MultiPoly, plt, colorString): x2, y2 = inners.coords.xy plt.plot(x2, y2, colorString) + # Test whether there are areas which would currently not be stitched but should be stitched @@ -65,12 +63,13 @@ def subtractResult(poly, rootPoly, offsetThresh): poly2 = poly2.difference(node.val.buffer(offsetThresh, 5, 3, 3)) return poly2 + # Used for debugging - plots all polygon exteriors within an AnyTree which is provided by the root node rootPoly. def drawPoly(rootPoly, colorString): fig, axs = plt.subplots(1, 1) - axs.axis('equal') + axs.axis("equal") plt.gca().invert_yaxis() for node in PreOrderIter(rootPoly): # if(node.id == "hole"): @@ -84,15 +83,26 @@ def drawPoly(rootPoly, colorString): def drawresult(resultcoords, resultcoords_Origin, colorString): fig, axs = plt.subplots(1, 1) - axs.axis('equal') + axs.axis("equal") plt.gca().invert_yaxis() plt.plot(*zip(*resultcoords), colorString) - colormap = np.array(['r', 'g', 'b', 'c', 'm', 'y', 'k', 'gray', 'm']) - labelmap = np.array(['MUST_USE', 'REGULAR_SPACING', 'INITIAL_RASTERING', 'EDGE_NEEDED', 'NOT_NEEDED', - 'ALREADY_TRANSFERRED', 'ADDITIONAL_TRACKING_POINT_NOT_NEEDED', 'EDGE_RASTERING_ALLOWED', 'EDGE_PREVIOUSLY_SHIFTED']) - - for i in range(0, 8+1): + colormap = np.array(["r", "g", "b", "c", "m", "y", "k", "gray", "m"]) + labelmap = np.array( + [ + "MUST_USE", + "REGULAR_SPACING", + "INITIAL_RASTERING", + "EDGE_NEEDED", + "NOT_NEEDED", + "ALREADY_TRANSFERRED", + "ADDITIONAL_TRACKING_POINT_NOT_NEEDED", + "EDGE_RASTERING_ALLOWED", + "EDGE_PREVIOUSLY_SHIFTED", + ] + ) + + for i in range(0, 8 + 1): # if i != Sampler.PointSource.EDGE_NEEDED and i != Sampler.PointSource.INITIAL_RASTERING: # continue selection = [] @@ -102,8 +112,8 @@ def drawresult(resultcoords, resultcoords_Origin, colorString): if len(selection) > 0: plt.scatter(*zip(*selection), c=colormap[i], label=labelmap[i]) - # plt.scatter(*zip(*resultcoords), - # c=colormap[resultcoords_Origin]) + # plt.scatter(*zip(*resultcoords), + # c=colormap[resultcoords_Origin]) axs.legend() plt.show(block=True) @@ -112,8 +122,14 @@ def drawresult(resultcoords, resultcoords_Origin, colorString): def colorline( - x, y, z=None, cmap=plt.get_cmap('copper'), norm=plt.Normalize(0.0, 1.0), - linewidth=3, alpha=1.0): + x, + y, + z=None, + cmap=plt.get_cmap("copper"), + norm=plt.Normalize(0.0, 1.0), + linewidth=3, + alpha=1.0, +): """ http://nbviewer.ipython.org/github/dpsanders/matplotlib-examples/blob/master/colorline.ipynb http://matplotlib.org/examples/pylab_examples/multicolored_line.html @@ -133,14 +149,16 @@ def colorline( z = np.asarray(z) segments = make_segments(x, y) - lc = mcoll.LineCollection(segments, array=z, cmap=cmap, norm=norm, - linewidth=linewidth, alpha=alpha) + lc = mcoll.LineCollection( + segments, array=z, cmap=cmap, norm=norm, linewidth=linewidth, alpha=alpha + ) ax = plt.gca() ax.add_collection(lc) return lc + # Used by colorline diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index 434c6bbf..07106515 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -1,4 +1,3 @@ -from sys import path from shapely.geometry.polygon import LineString from shapely.geometry import Point from shapely.ops import substring @@ -8,33 +7,41 @@ from enum import IntEnum from ..stitches import constants from ..stitches import PointTransfer -#Used to tag the origin of a rastered point +# Used to tag the origin of a rastered point + + class PointSource(IntEnum): - #MUST_USE = 0 # Legacy + # MUST_USE = 0 # Legacy REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance - #INITIAL_RASTERING = 2 #Legacy - EDGE_NEEDED = 3 # point which must be stitched to avoid to large deviations to the desired path - #NOT_NEEDED = 4 #Legacy - #ALREADY_TRANSFERRED = 5 #Legacy - #ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy - #EDGE_RASTERING_ALLOWED = 7 #Legacy - #EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy - ENTER_LEAVING_POINT = 9 #Whether this point is used to enter or leave a child - SOFT_EDGE_INTERNAL = 10 #If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - HARD_EDGE_INTERNAL = 11 #If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - PROJECTED_POINT = 12 #If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT - REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance - #FORBIDDEN_POINT_INTERNAL=14 #Legacy - SOFT_EDGE = 15 #If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - HARD_EDGE = 16 #If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - FORBIDDEN_POINT=17 #Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden - REPLACED_FORBIDDEN_POINT=18 #If one decides to avoid forbidden points new points to the left and to the right as replacement are created - DIRECT = 19 #Calculated by next neighbor projection - OVERNEXT = 20 #Calculated by overnext neighbor projection + # INITIAL_RASTERING = 2 #Legacy + # point which must be stitched to avoid to large deviations to the desired path + EDGE_NEEDED = 3 + # NOT_NEEDED = 4 #Legacy + # ALREADY_TRANSFERRED = 5 #Legacy + # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy + # EDGE_RASTERING_ALLOWED = 7 #Legacy + # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy + ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child + # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + SOFT_EDGE_INTERNAL = 10 + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE_INTERNAL = 11 + # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT + PROJECTED_POINT = 12 + REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance + # FORBIDDEN_POINT_INTERNAL=14 #Legacy + SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE = 16 + FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden + # If one decides to avoid forbidden points new points to the left and to the right as replacement are created + REPLACED_FORBIDDEN_POINT = 18 + DIRECT = 19 # Calculated by next neighbor projection + OVERNEXT = 20 # Calculated by overnext neighbor projection # Calculates the angles between adjacent edges at each interior point -#Note that the first and last values in the return array are zero since for the boundary points no angle calculations were possible +# Note that the first and last values in the return array are zero since for the boundary points no angle calculations were possible def calculate_line_angles(line): Angles = np.zeros(len(line.coords)) for i in range(1, len(line.coords)-1): @@ -42,44 +49,47 @@ def calculate_line_angles(line): vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) vec1length = np.linalg.norm(vec1) vec2length = np.linalg.norm(vec2) - #if vec1length <= 0: + # if vec1length <= 0: # print("HIER FEHLER") - - #if vec2length <=0: + + # if vec2length <=0: # print("HIER FEHLEr") - assert(vec1length >0) - assert(vec2length >0) - scalar_prod=np.dot(vec1, vec2)/(vec1length*vec2length) - scalar_prod = min(max(scalar_prod,-1),1) - #if scalar_prod > 1.0: + assert(vec1length > 0) + assert(vec2length > 0) + scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) + scalar_prod = min(max(scalar_prod, -1), 1) + # if scalar_prod > 1.0: # scalar_prod = 1.0 - #elif scalar_prod < -1.0: + # elif scalar_prod < -1.0: # scalar_prod = -1.0 Angles[i] = math.acos(scalar_prod) return Angles -#Rasters a line between start_distance and end_distance. -#Input: -#-line: The line to be rastered -#-start_distance: The distance along the line from which the rastering should start -#-end_distance: The distance along the line until which the rastering should be done -#-maxstitch_distance: The maximum allowed stitch distance -#-stitching_direction: =1 is stitched along line direction, =-1 if stitched in reversed order. Note that +# Rasters a line between start_distance and end_distance. +# Input: +# -line: The line to be rastered +# -start_distance: The distance along the line from which the rastering should start +# -end_distance: The distance along the line until which the rastering should be done +# -maxstitch_distance: The maximum allowed stitch distance +# -stitching_direction: =1 is stitched along line direction, =-1 if stitched in reversed order. Note that # start_distance > end_distance for stitching_direction = -1 -#-must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -#-abs_offset: used offset between to offsetted curves -#Output: -#-List of tuples with the rastered point coordinates -#-List which defines the point origin for each point according to the PointSource enum. -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, stitching_direction, must_use_points_deque, abs_offset): +# -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque +# is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) +# index of point_origin is the index of the point in the neighboring line +# -abs_offset: used offset between to offsetted curves +# Output: +# -List of tuples with the rastered point coordinates +# -List which defines the point origin for each point according to the PointSource enum. + + +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, + stitching_direction, must_use_points_deque, abs_offset): if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] assert (stitching_direction == -1 and start_distance >= end_distance) or ( stitching_direction == 1 and start_distance <= end_distance) - + deque_points = list(must_use_points_deque) linecoords = line.coords @@ -92,7 +102,8 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, deque_points[i] = (deque_points[i][0], line.length-deque_points[i][1]) else: - deque_points = deque_points[::-1] #Since points with highest priority (=distance along line) are first (descending sorted) + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): @@ -107,95 +118,109 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, path_coords = substring(aligned_line, start_distance, end_distance) - #aligned line is a line without doubled points. I had the strange situation in which the offset "start_distance" from the line beginning resulted in a starting point which was - # already present in aligned_line causing a doubled point. A double point is not allowed in the following calculations so we need to remove it: - if abs(path_coords.coords[0][0]-path_coords.coords[1][0]) OVERNEXT projected point > DIRECT projected point) as termination of this segment + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment # and start point for the next segment (so we do not always take the maximum possible length for a segment) segment_start_index = 0 segment_end_index = 1 forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - #if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: + while segment_end_index < len(merged_point_list): + # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and + # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: # print("GEFUNDEN") - #Collection of points for the current segment + # Collection of points for the current segment current_point_list = [merged_point_list[segment_start_index][0].point] - + while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1]-merged_point_list[segment_start_index][1] + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1]+maxstitch_distance - merged_point_list.insert(segment_end_index,(PointTransfer.projected_point_tuple(point=aligned_line.interpolate(new_distance), point_source=\ - PointSource.REGULAR_SPACING_INTERNAL),new_distance)) - if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: - print("GEFUNDEN") - segment_end_index+=1 + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + # if (abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9) < 0.2): + # print("GEFUNDEN") + segment_end_index += 1 break - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: # print("GEFUNDEN") - - current_point_list.append(merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify(constants.factor_offset_remove_dense_points*abs_offset,preserve_topology=False).coords) - if simplified_len > 2: #not all points have been simplified - so we need to add it + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it break - if merged_point_list[segment_end_index][0].point_source ==PointSource.HARD_EDGE_INTERNAL: - segment_end_index+=1 + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 break - segment_end_index+=1 + segment_end_index += 1 - segment_end_index-=1 + segment_end_index -= 1 - #Now we choose the best fitting point within this segment + # Now we choose the best fitting point within this segment index_overnext = -1 index_direct = -1 index_hard_edge = -1 - iter = segment_start_index+1 + iter = segment_start_index+1 while (iter <= segment_end_index): if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: index_overnext = iter @@ -208,48 +233,48 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, segment_end_index = index_hard_edge else: if index_overnext != -1: - if (index_direct != -1 and index_direct > index_overnext and - (merged_point_list[index_direct][1]-merged_point_list[index_overnext][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext* + if (index_direct != -1 and index_direct > index_overnext and + (merged_point_list[index_direct][1]-merged_point_list[index_overnext][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * (merged_point_list[index_overnext][1]-merged_point_list[segment_start_index][1])): - #We allow to take the direct projected point instead of the overnext projected point if it would result in a - #significant longer segment length + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length segment_end_index = index_direct else: segment_end_index = index_overnext elif index_direct != -1: segment_end_index = index_direct - #Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - #If they are too close ( end_distance for stitching_direction = -1 -#-must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -#-abs_offset: used offset between to offsetted curves -#Output: -#-List of tuples with the rastered point coordinates -#-List which defines the point origin for each point according to the PointSource enum. +# -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque +# is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) +# index of point_origin is the index of the point in the neighboring line +# -abs_offset: used offset between to offsetted curves +# Output: +# -List of tuples with the rastered point coordinates +# -List which defines the point origin for each point according to the PointSource enum. + + def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stitching_direction, must_use_points_deque, abs_offset, offset_by_half): if (line.length < constants.line_lengh_seen_as_one_point): return [line.coords[0]], [PointSource.HARD_EDGE] - + deque_points = list(must_use_points_deque) linecoords = line.coords - if stitching_direction==-1: + if stitching_direction == -1: linecoords = linecoords[::-1] for i in range(len(deque_points)): deque_points[i] = (deque_points[i][0], line.length-deque_points[i][1]) else: - deque_points = deque_points[::-1] #Since points with highest priority (=distance along line) are first (descending sorted) + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] # Ordering in priority queue: # (point, LineStringSampling.PointSource), priority) - aligned_line = LineString(linecoords) #might be different from line for stitching_direction=-1 + # might be different from line for stitching_direction=-1 + aligned_line = LineString(linecoords) angles = calculate_line_angles(aligned_line) - #For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge + # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge angles[0] = 1.1*constants.limiting_angle - angles[-1] = 1.1*constants.limiting_angle + angles[-1] = 1.1*constants.limiting_angle current_distance = 0.0 - #Next we merge the line points and the projected (deque) points into one list + # Next we merge the line points and the projected (deque) points into one list merged_point_list = [] dq_iter = 0 - for point,angle in zip(aligned_line.coords,angles): - #if abs(point[0]-52.9) < 0.2 and abs(point[1]-183.4)< 0.2: + for point, angle in zip(aligned_line.coords, angles): + # if abs(point[0]-52.9) < 0.2 and abs(point[1]-183.4)< 0.2: # print("GEFUNDEN") current_distance = aligned_line.project(Point(point)) while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance: - #We want to avoid setting points at soft edges close to forbidden points + # We want to avoid setting points at soft edges close to forbidden points if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: - #Check whether a previous added point is a soft edge close to the forbidden point - if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and + # Check whether a previous added point is a soft edge close to the forbidden point + if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and abs(merged_point_list[-1][1]-deque_points[dq_iter][1] < abs_offset*constants.factor_offset_forbidden_point)): item = merged_point_list.pop() - merged_point_list.append((PointTransfer.projected_point_tuple(point=item[0].point, point_source=\ - PointSource.FORBIDDEN_POINT),item[1])) + merged_point_list.append((PointTransfer.projected_point_tuple( + point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1])) else: merged_point_list.append(deque_points[dq_iter]) - dq_iter+=1 - #Check whether the current point is close to a forbidden point - if (dq_iter < len(deque_points) and + dq_iter += 1 + # Check whether the current point is close to a forbidden point + if (dq_iter < len(deque_points) and deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and angle < constants.limiting_angle and - abs(deque_points[dq_iter-1][1]-current_distance) < abs_offset*constants.factor_offset_forbidden_point): + abs(deque_points[dq_iter-1][1]-current_distance) < abs_offset*constants.factor_offset_forbidden_point): point_source = PointSource.FORBIDDEN_POINT else: if angle < constants.limiting_angle: point_source = PointSource.SOFT_EDGE_INTERNAL else: point_source = PointSource.HARD_EDGE_INTERNAL - merged_point_list.append((PointTransfer.projected_point_tuple(point=Point(point), point_source=point_source),current_distance)) + merged_point_list.append((PointTransfer.projected_point_tuple( + point=Point(point), point_source=point_source), current_distance)) result_list = [merged_point_list[0]] - - #General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified to a straight line by shapelys simplify method. - #Then, look at the points within this segment and choose the best fitting one (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment # and start point for the next segment (so we do not always take the maximum possible length for a segment) segment_start_index = 0 segment_end_index = 1 forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - #if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: + while segment_end_index < len(merged_point_list): + # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and + # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: # print("GEFUNDEN") - #Collection of points for the current segment + # Collection of points for the current segment current_point_list = [merged_point_list[segment_start_index][0].point] - + while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1]-merged_point_list[segment_start_index][1] + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1]+maxstitch_distance - merged_point_list.insert(segment_end_index,(PointTransfer.projected_point_tuple(point=aligned_line.interpolate(new_distance), point_source=\ - PointSource.REGULAR_SPACING_INTERNAL),new_distance)) - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and 7 + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9)< 0.2: # print("GEFUNDEN") - segment_end_index+=1 + segment_end_index += 1 break - #if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-34.4) < 0.2 and abs(merged_point_list[segment_end_index][0].point.coords[0][1]-6.2)< 0.2: + # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-34.4) < 0.2 and + # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-6.2)< 0.2: # print("GEFUNDEN") - - current_point_list.append(merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify(constants.factor_offset_remove_dense_points*abs_offset,preserve_topology=False).coords) - if simplified_len > 2: #not all points have been simplified - so we need to add it + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it break - if merged_point_list[segment_end_index][0].point_source ==PointSource.HARD_EDGE_INTERNAL: - segment_end_index+=1 + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 break - segment_end_index+=1 + segment_end_index += 1 - segment_end_index-=1 + segment_end_index -= 1 - #Now we choose the best fitting point within this segment + # Now we choose the best fitting point within this segment index_overnext = -1 index_direct = -1 index_hard_edge = -1 - iter = segment_start_index+1 + iter = segment_start_index+1 while (iter <= segment_end_index): if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: index_overnext = iter @@ -406,48 +444,49 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit index_less_preferred = index_overnext if index_preferred != -1: - if (index_less_preferred != -1 and index_less_preferred > index_preferred and - (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext* + if (index_less_preferred != -1 and index_less_preferred > index_preferred and + (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): - #We allow to take the direct projected point instead of the overnext projected point if it would result in a - #significant longer segment length + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length segment_end_index = index_less_preferred else: segment_end_index = index_preferred elif index_less_preferred != -1: segment_end_index = index_less_preferred - #Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - #If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: new_point_left_proj = result_list[index][1]-distance_left if new_point_left_proj < 0: new_point_left_proj += line.length new_point_right_proj = result_list[index][1]+distance_right if new_point_right_proj > line.length: - new_point_right_proj-=line.length + new_point_right_proj -= line.length point_left = line.interpolate(new_point_left_proj) point_right = line.interpolate(new_point_right_proj) - forbidden_point_distance = result_list[index][0].point.distance(LineString([point_left, point_right])) + forbidden_point_distance = result_list[index][0].point.distance( + LineString([point_left, point_right])) if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: del result_list[index] - result_list.insert(index, (PointTransfer.projected_point_tuple(point=point_right, point_source=\ - PointSource.REPLACED_FORBIDDEN_POINT),new_point_right_proj)) - result_list.insert(index, (PointTransfer.projected_point_tuple(point=point_left, point_source=\ - PointSource.REPLACED_FORBIDDEN_POINT),new_point_left_proj)) - current_index_shift+=1 + result_list.insert(index, (PointTransfer.projected_point_tuple( + point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) + result_list.insert(index, (PointTransfer.projected_point_tuple( + point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) + current_index_shift += 1 break else: - distance_left/=2.0 - distance_right/=2.0 + distance_left /= 2.0 + distance_right /= 2.0 return result_list + if __name__ == "__main__": - line = LineString([(0,0), (1,0), (2,1),(3,0),(4,0)]) + line = LineString([(0, 0), (1, 0), (2, 1), (3, 0), (4, 0)]) print(calculate_line_angles(line)*180.0/math.pi) diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index 998282a3..b4c6c004 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -1,4 +1,4 @@ -from shapely.geometry import Point, MultiPoint +from shapely.geometry import Point, MultiPoint from shapely.geometry.polygon import LineString, LinearRing from collections import namedtuple from shapely.ops import nearest_points @@ -6,11 +6,14 @@ import math from ..stitches import constants from ..stitches import LineStringSampling -projected_point_tuple = namedtuple('projected_point_tuple', ['point', 'point_source']) +projected_point_tuple = namedtuple( + 'projected_point_tuple', ['point', 'point_source']) + +# Calculated the nearest interserction point of "bisectorline" with the coordinates of child (child.val). +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. + -#Calculated the nearest interserction point of "bisectorline" with the coordinates of child (child.val). -#It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -#intersection was found. def calc_transferred_point(bisectorline, child): result = bisectorline.intersection(child.val) if result.is_empty: @@ -24,37 +27,44 @@ def calc_transferred_point(bisectorline, child): resultlist = list(result) desired_point = resultlist[0] if len(resultlist) > 1: - desired_point = nearest_points(result, Point(bisectorline.coords[0]))[0] + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] priority = child.val.project(desired_point) point = desired_point return point, priority -#Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs -# To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. -#Input: -#-treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. -#-used_offset: The used offset when the curves where offsetted -#-offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" -#-max_stitching_distance: The maximum allowed stitch distance between two points -#-to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points can be handled as closed ring -#-to_transfer_points_origin: The origin tag of each point in to_transfer_points -#-overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) -#-transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as forbidden points to the neighbor to avoid a point placing there -#-transfer_to_parent: If True, points will be transferred to the parent -#-transfer_to_sibling: If True, points will be transferred to the siblings -#-transfer_to_child: If True, points will be transferred to the childs -#Output: -#-Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_stitching_distance, to_transfer_points, to_transfer_points_origin=[], - overnext_neighbor = False, transfer_forbidden_points = False, transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): - - assert(len(to_transfer_points)==len(to_transfer_points_origin) or len(to_transfer_points_origin) == 0) +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], + overnext_neighbor=False, transfer_forbidden_points=False, + transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): + """ + Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -to_transfer_points_origin: The origin tag of each point in to_transfer_points + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_parent: If True, points will be transferred to the parent + -transfer_to_sibling: If True, points will be transferred to the siblings + -transfer_to_child: If True, points will be transferred to the childs + Output: + -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert(len(to_transfer_points) == len(to_transfer_points_origin) + or len(to_transfer_points_origin) == 0) assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and (offset_by_half or not offset_by_half and overnext_neighbor)) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) if len(to_transfer_points) == 0: return @@ -71,37 +81,37 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st if transfer_to_child: for child in childs_tuple: - if child.already_rastered == False: + if not child.already_rastered: if not overnext_neighbor: child_list.append(child) if transfer_forbidden_points: child_list_forbidden.append(child) if overnext_neighbor: for subchild in child.children: - if subchild.already_rastered == False: + if not subchild.already_rastered: child_list.append(subchild) if transfer_to_sibling: for sibling in siblings_tuple: - if sibling.already_rastered == False: + if not sibling.already_rastered: if not overnext_neighbor: neighbor_list.append(sibling) if transfer_forbidden_points: neighbor_list_forbidden.append(sibling) if overnext_neighbor: for subchild in sibling.children: - if subchild.already_rastered == False: + if not subchild.already_rastered: neighbor_list.append(subchild) - if transfer_to_parent and treenode.parent != None: - if treenode.parent.already_rastered == False: + if transfer_to_parent and treenode.parent is not None: + if not treenode.parent.already_rastered: if not overnext_neighbor: - neighbor_list.append(treenode.parent) + neighbor_list.append(treenode.parent) if transfer_forbidden_points: - neighbor_list_forbidden.append(treenode.parent) + neighbor_list_forbidden.append(treenode.parent) if overnext_neighbor: - if treenode.parent.parent != None: - if treenode.parent.parent.already_rastered == False: + if treenode.parent.parent is not None: + if not treenode.parent.parent.already_rastered: neighbor_list.append(treenode.parent.parent) if not neighbor_list and not child_list: @@ -126,19 +136,20 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st closed_line = LinearRing(to_transfer_points) bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor*(2.0 if overnext_neighbor else 1.0) + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) bisectorline_length_forbidden_points = abs(used_offset) * \ constants.transfer_point_distance_factor linesign_child = math.copysign(1, used_offset) - i = 0 currentDistance = 0 while i < len(point_list): - assert(point_source_list[i] != LineStringSampling.PointSource.ENTER_LEAVING_POINT) - #if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: + assert(point_source_list[i] != + LineStringSampling.PointSource.ENTER_LEAVING_POINT) + # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: # print("HIIIIIIIIIIIERRR") # We create a bisecting line through the current point @@ -152,7 +163,6 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st normalized_vector_prev_x /= prev_spacing normalized_vector_prev_y /= prev_spacing - normalized_vector_next_x = normalized_vector_next_y = 0 next_spacing = 0 while True: @@ -187,13 +197,15 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st vecy = -linesign_child*bisectorline_length*normalized_vector_next_x if transfer_forbidden_points: - vecx_forbidden_point = linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_x + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x else: vecx *= bisectorline_length/vec_length vecy *= bisectorline_length/vec_length - + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: vecx = -vecx vecy = -vecy @@ -212,55 +224,66 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, max_st originPoint = closed_line.interpolate(off) bisectorline_child = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) + originPoint.coords[0][1]), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) bisectorline_neighbor = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy)]) + originPoint.coords[0][1]), + (originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy)]) bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) for child in child_list: - point, priority = calc_transferred_point(bisectorline_child,child) - if point==None: + point, priority = calc_transferred_point(bisectorline_child, child) + if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for child in child_list_forbidden: - point, priority = calc_transferred_point(bisectorline_forbidden_point_child,child) - if point == None: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_child, child) + if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + for neighbor in neighbor_list: - point, priority = calc_transferred_point(bisectorline_neighbor,neighbor) - if point==None: + point, priority = calc_transferred_point( + bisectorline_neighbor, neighbor) + if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for neighbor in neighbor_list_forbidden: - point, priority = calc_transferred_point(bisectorline_forbidden_point_neighbor,neighbor) - if point == None: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_neighbor, neighbor) + if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) i += 1 currentDistance += next_spacing assert(len(point_list) == len(point_source_list)) -#Calculated the nearest interserction point of "bisectorline" with the coordinates of child. -#It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -#intersection was found. +# Calculated the nearest interserction point of "bisectorline" with the coordinates of child. +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. + + def calc_transferred_point_graph(bisectorline, edge_geometry): result = bisectorline.intersection(edge_geometry) if result.is_empty: @@ -274,41 +297,44 @@ def calc_transferred_point_graph(bisectorline, edge_geometry): resultlist = list(result) desired_point = resultlist[0] if len(resultlist) > 1: - desired_point = nearest_points(result, Point(bisectorline.coords[0]))[0] + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] priority = edge_geometry.project(desired_point) point = desired_point return point, priority -#Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs -# To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. -#Input: -#-treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. -#-used_offset: The used offset when the curves where offsetted -#-offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" -#-max_stitching_distance: The maximum allowed stitch distance between two points -#-to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points can be handled as closed ring -#-to_transfer_points_origin: The origin tag of each point in to_transfer_points -#-overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) -#-transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as forbidden points to the neighbor to avoid a point placing there -#-transfer_to_parent: If True, points will be transferred to the parent -#-transfer_to_sibling: If True, points will be transferred to the siblings -#-transfer_to_child: If True, points will be transferred to the childs -#Output: -#-Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque -#is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) -#index of point_origin is the index of the point in the neighboring line def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, - overnext_neighbor = False, transfer_forbidden_points = False, transfer_to_previous=True, transfer_to_next=True): + overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): + """ + Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -fill_stitch_graph: Graph data structure of the stitching lines + -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_previous: If True, points will be transferred to the previous edge in the graph + -transfer_to_next: If True, points will be transferred to the next edge in the graph + Output: + -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and (offset_by_half or not offset_by_half and overnext_neighbor)) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) if len(to_transfer_points) == 0: return - # Take only neighbors which have not rastered before # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) previous_edge_list = [] @@ -319,7 +345,8 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if transfer_to_previous: previous_neighbors_tuples = current_edge['previous_neighbors'] for neighbor in previous_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0]][neighbor[-1]]['segment'] + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] if not neighbor_edge['already_rastered']: if not overnext_neighbor: previous_edge_list.append(neighbor_edge) @@ -328,14 +355,16 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if overnext_neighbor: overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] for overnext_neighbor in overnext_previous_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0]][overnext_neighbor[-1]]['segment'] + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] if not overnext_neighbor_edge['already_rastered']: previous_edge_list.append(overnext_neighbor_edge) if transfer_to_next: next_neighbors_tuples = current_edge['next_neighbors'] for neighbor in next_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0]][neighbor[-1]]['segment'] + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] if not neighbor_edge['already_rastered']: if not overnext_neighbor: next_edge_list.append(neighbor_edge) @@ -344,11 +373,11 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o if overnext_neighbor: overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] for overnext_neighbor in overnext_next_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0]][overnext_neighbor[-1]]['segment'] + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] if not overnext_neighbor_edge['already_rastered']: next_edge_list.append(overnext_neighbor_edge) - if not previous_edge_list and not next_edge_list: return @@ -357,19 +386,19 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o line = LineString(to_transfer_points) bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor*(2.0 if overnext_neighbor else 1.0) + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) bisectorline_length_forbidden_points = abs(used_offset) * \ constants.transfer_point_distance_factor linesign_child = math.copysign(1, used_offset) - i = 0 currentDistance = 0 while i < len(point_list): - - #if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: + + # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: # print("HIIIIIIIIIIIERRR") # We create a bisecting line through the current point @@ -383,7 +412,6 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o normalized_vector_prev_x /= prev_spacing normalized_vector_prev_y /= prev_spacing - normalized_vector_next_x = normalized_vector_next_y = 0 next_spacing = 0 while True: @@ -416,13 +444,15 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o vecy = -linesign_child*bisectorline_length*normalized_vector_next_x if transfer_forbidden_points: - vecx_forbidden_point = linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child*bisectorline_length_forbidden_points*normalized_vector_next_x + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x else: vecx *= bisectorline_length/vec_length vecy *= bisectorline_length/vec_length - + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: vecx = -vecx vecy = -vecy @@ -446,22 +476,25 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o originPoint.coords[0][1]+vecy)]) bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) for edge in previous_edge_list+next_edge_list: - point, priority = calc_transferred_point_graph(bisectorline,edge['geometry']) - if point==None: + point, priority = calc_transferred_point_graph( + bisectorline, edge['geometry']) + if point is None: continue - edge['projected_points'].insert(projected_point_tuple(point = point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor else LineStringSampling.PointSource.DIRECT), priority) + edge['projected_points'].insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor + else LineStringSampling.PointSource.DIRECT), priority) for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: - point, priority = calc_transferred_point_graph(bisectorline_forbidden_point,edge_forbidden['geometry']) - if point == None: + point, priority = calc_transferred_point_graph( + bisectorline_forbidden_point, edge_forbidden['geometry']) + if point is None: continue - edge_forbidden['projected_points'].insert(projected_point_tuple(point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - + edge_forbidden['projected_points'].insert(projected_point_tuple( + point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) + i += 1 currentDistance += next_spacing diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index d0a3f7aa..ba3e3031 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -1,6 +1,6 @@ from shapely.geometry.polygon import LinearRing, LineString from shapely.geometry import Polygon, MultiLineString -from shapely.ops import polygonize +from shapely.ops import polygonize from shapely.geometry import MultiPolygon from anytree import AnyNode, PreOrderIter from shapely.geometry.polygon import orient @@ -10,68 +10,90 @@ from ..stitches import ConnectAndSamplePattern from ..stitches import constants - -# Problem: When shapely offsets a LinearRing the start/end point might be handled wrongly since they are only treated as LineString. -# (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) -# This method checks first whether the start/end point form a problematic edge with respect to the offset side. If it is not a problematic -# edge we can use the normal offset_routine. Otherwise we need to perform two offsets: -# -offset the ring -# -offset the start/end point + its two neighbors left and right -# Finally both offsets are merged together to get the correct offset of a LinearRing def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): + """ + Solves following problem: When shapely offsets a LinearRing the + start/end point might be handled wrongly since they + are only treated as LineString. + (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) + This method checks first whether the start/end point form a problematic + edge with respect to the offset side. If it is not a problematic + edge we can use the normal offset_routine. Otherwise we need to + perform two offsets: + -offset the ring + -offset the start/end point + its two neighbors left and right + Finally both offsets are merged together to get the correct + offset of a LinearRing + """ + coords = ring.coords[:] - # check whether edge at index 0 is concave or convex. Only for concave edges we need to spend additional effort + # check whether edge at index 0 is concave or convex. Only for + # concave edges we need to spend additional effort dx_seg1 = dy_seg1 = 0 if coords[0] != coords[-1]: - dx_seg1 = coords[0][0]-coords[-1][0] - dy_seg1 = coords[0][1]-coords[-1][1] + dx_seg1 = coords[0][0] - coords[-1][0] + dy_seg1 = coords[0][1] - coords[-1][1] else: - dx_seg1 = coords[0][0]-coords[-2][0] - dy_seg1 = coords[0][1]-coords[-2][1] - dx_seg2 = coords[1][0]-coords[0][0] - dy_seg2 = coords[1][1]-coords[0][1] + dx_seg1 = coords[0][0] - coords[-2][0] + dy_seg1 = coords[0][1] - coords[-2][1] + dx_seg2 = coords[1][0] - coords[0][0] + dy_seg2 = coords[1][1] - coords[0][1] # use cross product: - crossvalue = dx_seg1*dy_seg2-dy_seg1*dx_seg2 + crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 sidesign = 1 - if side == 'left': + if side == "left": sidesign = -1 - # We do not need to take care of the joint n-0 since we offset along a concave edge: - if sidesign*offset*crossvalue <= 0: + # We do not need to take care of the joint n-0 since we + # offset along a concave edge: + if sidesign * offset * crossvalue <= 0: return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) # We offset along a convex edge so we offset the joint n-0 separately: if coords[0] != coords[-1]: coords.append(coords[0]) offset_ring1 = ring.parallel_offset( - offset, side, resolution, join_style, mitre_limit) + offset, side, resolution, join_style, mitre_limit + ) offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( - offset, side, resolution, join_style, mitre_limit) + offset, side, resolution, join_style, mitre_limit + ) # Next we need to merge the results: - if offset_ring1.geom_type == 'LineString': - return LinearRing(offset_ring2.coords[:]+offset_ring1.coords[1:-1]) + if offset_ring1.geom_type == "LineString": + return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) else: - # We have more than one resulting LineString for offset of the geometry (ring) = offset_ring1. - # Hence we need to find the LineString which belongs to the offset of element 0 in coords =offset_ring2 + # We have more than one resulting LineString for offset of + # the geometry (ring) = offset_ring1. + # Hence we need to find the LineString which belongs to the + # offset of element 0 in coords =offset_ring2 # in order to add offset_ring2 geometry to it: result_list = [] - thresh = constants.offset_factor_for_adjacent_geometry*abs(offset) + thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) for offsets in offset_ring1: - if(abs(offsets.coords[0][0]-coords[0][0]) < thresh and abs(offsets.coords[0][1]-coords[0][1]) < thresh): - result_list.append(LinearRing( - offset_ring2.coords[:]+offsets.coords[1:-1])) + if ( + abs(offsets.coords[0][0] - coords[0][0]) < thresh + and abs(offsets.coords[0][1] - coords[0][1]) < thresh + ): + result_list.append( + LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) + ) else: result_list.append(LinearRing(offsets)) return MultiLineString(result_list) -# Removes all geometries which do not form a "valid" LinearRing (meaning a ring which does not form a straight line) def take_only_valid_linear_rings(rings): - if(rings.geom_type == 'MultiLineString'): + """ + Removes all geometries which do not form a "valid" LinearRing + (meaning a ring which does not form a straight line) + """ + if rings.geom_type == "MultiLineString": new_list = [] for ring in rings: - if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): + if len(ring.coords) > 3 or ( + len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] + ): new_list.append(ring) if len(new_list) == 1: return LinearRing(new_list[0]) @@ -86,138 +108,184 @@ def take_only_valid_linear_rings(rings): return rings -#Since naturally holes have the opposite point ordering than non-holes we make -#all lines within the tree "root" uniform (having all the same ordering direction) def make_tree_uniform_ccw(root): + """ + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree "root" uniform (having all the same + ordering direction) + """ for node in PreOrderIter(root): - if(node.id == 'hole'): + if node.id == "hole": node.val.coords = list(node.val.coords)[::-1] -#Used to define which stitching strategy shall be used +# Used to define which stitching strategy shall be used class StitchingStrategy(IntEnum): CLOSEST_POINT = 0 INNER_TO_OUTER = 1 -# Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. -# These created geometries are afterwards connected to each other and resampled with a maximum stitch_distance. -# The return value is a LineString which should cover the full polygon. -#Input: -#-poly: The shapely polygon which can have holes -#-offset: The used offset for the curves -#-join_style: Join style for the offset - can be round, mitered or bevel (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) -#For examples look at https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png -#-stitch_distance maximum allowed stitch distance between two points -#-offset_by_half: True if the points shall be interlaced -#-strategy: According to StitchingStrategy you can select between different strategies for the connection between parent and childs -#Output: -#-List of point coordinate tuples -#-Tag (origin) of each point to analyze why a point was placed at this position -def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): + +def offset_poly( + poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point +): + """ + Takes a polygon (which can have holes) as input and creates offsetted + versions until the polygon is filled with these smaller offsets. + These created geometries are afterwards connected to each other and + resampled with a maximum stitch_distance. + The return value is a LineString which should cover the full polygon. + Input: + -poly: The shapely polygon which can have holes + -offset: The used offset for the curves + -join_style: Join style for the offset - can be round, mitered or bevel + (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) + For examples look at + https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png + -stitch_distance maximum allowed stitch distance between two points + -offset_by_half: True if the points shall be interlaced + -strategy: According to StitchingStrategy enum class you can select between + different strategies for the connection between parent and childs + -starting_point: Defines the starting point for the stitching + Output: + -List of point coordinate tuples + -Tag (origin) of each point to analyze why a point was placed + at this position + """ ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify( - constants.simplification_threshold, False) - root = AnyNode(id="node", val=ordered_poly.exterior, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + ordered_poly = ordered_poly.simplify(constants.simplification_threshold, False) + root = AnyNode( + id="node", + val=ordered_poly.exterior, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) active_polys = [root] active_holes = [[]] for holes in ordered_poly.interiors: - #print("hole: - is ccw: ", LinearRing(holes).is_ccw) active_holes[0].append( - AnyNode(id="hole", val=holes, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None))) + AnyNode( + id="hole", + val=holes, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) + ) - # counter = 0 - while len(active_polys) > 0: # and counter < 20: - # counter += 1 - # print("New iter") + while len(active_polys) > 0: current_poly = active_polys.pop() current_holes = active_holes.pop() poly_inners = [] - # outer = current_poly.val.parallel_offset(offset,'left', 5, join_style, 10) - outer = offset_linear_ring(current_poly.val, offset, 'left', 5, join_style, 10) + outer = offset_linear_ring( + current_poly.val, + offset, + "left", + resolution=5, + joint_style=join_style, + mitre_limit=10, + ) outer = outer.simplify(constants.simplification_threshold, False) outer = take_only_valid_linear_rings(outer) for j in range(len(current_holes)): - # inner = closeLinearRing(current_holes[j].val,offset/2.0).parallel_offset(offset,'left', 5, join_style, 10) inner = offset_linear_ring( - current_holes[j].val, offset, 'left', 5, join_style, 10) + current_holes[j].val, + offset, + "left", + resolution=5, + joint_style=join_style, + mitre_limit=10, + ) inner = inner.simplify(constants.simplification_threshold, False) inner = take_only_valid_linear_rings(inner) if not inner.is_empty: poly_inners.append(Polygon(inner)) if not outer.is_empty: if len(poly_inners) == 0: - if outer.geom_type == 'LineString': + if outer.geom_type == "LineString": result = Polygon(outer) else: result = MultiPolygon(polygonize(outer)) else: - if outer.geom_type == 'LineString': - result = Polygon(outer).difference( - MultiPolygon(poly_inners)) + if outer.geom_type == "LineString": + result = Polygon(outer).difference(MultiPolygon(poly_inners)) else: - result = MultiPolygon(outer).difference( - MultiPolygon(poly_inners)) + result = MultiPolygon(outer).difference(MultiPolygon(poly_inners)) - if not result.is_empty and result.area > offset*offset/10: + if not result.is_empty and result.area > offset * offset / 10: result_list = [] - if result.geom_type == 'Polygon': + if result.geom_type == "Polygon": result_list = [result] else: result_list = list(result) - # print("New result_list: ", len(result_list)) + for polygon in result_list: polygon = orient(polygon, -1) - if polygon.area < offset*offset/10: + if polygon.area < offset * offset / 10: continue - polygon = polygon.simplify(constants.simplification_threshold, False) + polygon = polygon.simplify( + constants.simplification_threshold, False + ) poly_coords = polygon.exterior - # if polygon.exterior.is_ccw: - # hole.coords = list(hole.coords)[::-1] - #poly_coords = polygon.exterior.simplify(constants.simplification_threshold, False) poly_coords = take_only_valid_linear_rings(poly_coords) if poly_coords.is_empty: continue - #print("node: - is ccw: ", LinearRing(poly_coords).is_ccw) - # if(LinearRing(poly_coords).is_ccw): - # print("Fehler!") - node = AnyNode(id="node", parent=current_poly, - val=poly_coords, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + + node = AnyNode( + id="node", + parent=current_poly, + val=poly_coords, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) active_polys.append(node) hole_node_list = [] for hole in polygon.interiors: hole_node = AnyNode( - id="hole", val=hole, already_rastered=False, transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None)) + id="hole", + val=hole, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) for previous_hole in current_holes: if Polygon(hole).contains(Polygon(previous_hole.val)): previous_hole.parent = hole_node hole_node_list.append(hole_node) active_holes.append(hole_node_list) - for previous_hole in current_holes: # if the previous holes are not contained in the new holes they have been merged with the outer polygon - if previous_hole.parent == None: + for previous_hole in current_holes: + # If the previous holes are not + # contained in the new holes they + # have been merged with the + # outer polygon + if previous_hole.parent is None: previous_hole.parent = current_poly - - #DebuggingMethods.drawPoly(root, 'r-') + # DebuggingMethods.drawPoly(root, 'r-') make_tree_uniform_ccw(root) # print(RenderTree(root)) if strategy == StitchingStrategy.CLOSEST_POINT: - connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( - root, offset, stitch_distance, starting_point, offset_by_half) + ( + connected_line, + connected_line_origin, + ) = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( + root, offset, stitch_distance, starting_point, offset_by_half + ) elif strategy == StitchingStrategy.INNER_TO_OUTER: - connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( - root, offset, stitch_distance, starting_point, offset_by_half) + ( + connected_line, + connected_line_origin, + ) = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( + root, offset, stitch_distance, starting_point, offset_by_half + ) else: - print("Invalid strategy!") - assert(0) + raise ValueError("Invalid stitching stratety!") return connected_line, connected_line_origin diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 71cfd80f..1331ecb2 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -16,7 +16,6 @@ from depq import DEPQ from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM -from ..utils import geometry from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import line_string_to_point_list from .fill import intersect_region_with_grating, intersect_region_with_grating_line, stitch_row @@ -64,11 +63,12 @@ def auto_fill(shape, ending_point=None, underpath=True, offset_by_half=True): - #offset_by_half only relevant for line != None; staggers only relevant for line == None! + # offset_by_half only relevant for line != None; staggers only relevant for line == None! fill_stitch_graph = [] try: - fill_stitch_graph = build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, starting_point, ending_point) + fill_stitch_graph = build_fill_stitch_graph( + shape, line, angle, row_spacing, end_row_spacing, starting_point, ending_point) except ValueError: # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node return fallback(shape, running_stitch_length) @@ -76,10 +76,12 @@ def auto_fill(shape, if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): return fallback(shape, running_stitch_length) - travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) - path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) + travel_graph = build_travel_graph( + fill_stitch_graph, shape, angle, underpath) + path = find_stitch_path( + fill_stitch_graph, travel_graph, starting_point, ending_point) result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, - max_stitch_length, running_stitch_length, staggers, skip_last,line!=None,offset_by_half) + max_stitch_length, running_stitch_length, staggers, skip_last, line is not None, offset_by_half) return result @@ -97,7 +99,8 @@ def which_outline(shape, coords): point = shgeo.Point(*coords) outlines = list(shape.boundary) outline_indices = list(range(len(outlines))) - closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) + closest = min(outline_indices, + key=lambda index: outlines[index].distance(point)) return closest @@ -148,17 +151,18 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st debug.add_layer("auto-fill fill stitch") - if line == None: + if line is None: # Convert the shape into a set of parallel line segments. - rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + rows_of_segments = intersect_region_with_grating( + shape, angle, row_spacing, end_row_spacing) else: - rows_of_segments = intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing) + rows_of_segments = intersect_region_with_grating_line( + shape, line, row_spacing, end_row_spacing) - #segments = [segment for row in rows_of_segments for segment in row] + # segments = [segment for row in rows_of_segments for segment in row] graph = networkx.MultiGraph() - for i in range(len(rows_of_segments)): for segment in rows_of_segments[i]: # First, add the grating segments as edges. We'll use the coordinates @@ -166,16 +170,18 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st # networkx allows us to label nodes with arbitrary data. We'll # mark this one as a grating segment. - #graph.add_edge(*segment, key="segment", underpath_edges=[]) - previous_neighbors_ = [(seg[0],seg[-1]) for seg in rows_of_segments[i-1] if i > 0] - next_neighbors_ = [(seg[0],seg[-1]) for seg in rows_of_segments[(i+1)% len(rows_of_segments)] if i < len(rows_of_segments)-1] + # graph.add_edge(*segment, key="segment", underpath_edges=[]) + previous_neighbors_ = [(seg[0], seg[-1]) + for seg in rows_of_segments[i-1] if i > 0] + next_neighbors_ = [(seg[0], seg[-1]) for seg in rows_of_segments[(i+1) % + len(rows_of_segments)] if i < len(rows_of_segments)-1] - graph.add_edge(segment[0],segment[-1], key="segment", underpath_edges=[], - geometry=shgeo.LineString(segment), previous_neighbors = previous_neighbors_, next_neighbors = next_neighbors_, - projected_points=DEPQ(iterable=None, maxlen=None), already_rastered=False) + graph.add_edge(segment[0], segment[-1], key="segment", underpath_edges=[], + geometry=shgeo.LineString(segment), previous_neighbors=previous_neighbors_, next_neighbors=next_neighbors_, + projected_points=DEPQ(iterable=None, maxlen=None), already_rastered=False) -#fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) +# fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -205,7 +211,8 @@ def insert_node(graph, shape, point): if key == "outline": edges.append(((start, end), data)) - edge, data = min(edges, key=lambda edge_data: shgeo.LineString(edge_data[0]).distance(projected_point)) + edge, data = min(edges, key=lambda edge_data: shgeo.LineString( + edge_data[0]).distance(projected_point)) graph.remove_edge(*edge, key="outline") graph.add_edge(edge[0], node, key="outline", **data) @@ -218,7 +225,8 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): outline_index = which_outline(shape, node) outline_projection = project(shape, node, outline_index) - graph.add_node(node, outline=outline_index, projection=outline_projection) + graph.add_node(node, outline=outline_index, + projection=outline_projection) def add_boundary_travel_nodes(graph, shape): @@ -236,9 +244,11 @@ def add_boundary_travel_nodes(graph, shape): # resolution. A pixel is around a quarter of a millimeter. for i in range(1, int(length)): subpoint = segment.interpolate(i) - graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index) + graph.add_node((subpoint.x, subpoint.y), projection=outline.project( + subpoint), outline=outline_index) - graph.add_node((point.x, point.y), projection=outline.project(point), outline=outline_index) + graph.add_node((point.x, point.y), projection=outline.project( + point), outline=outline_index) prev = point @@ -253,7 +263,8 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False): outline. """ - nodes = list(graph.nodes(data=True)) # returns a list of tuples: [(node, {data}), (node, {data}) ...] + # returns a list of tuples: [(node, {data}), (node, {data}) ...] + nodes = list(graph.nodes(data=True)) nodes.sort(key=lambda node: (node[1]['outline'], node[1]['projection'])) for outline_index, nodes in groupby(nodes, key=lambda node: node[1]['outline']): @@ -318,7 +329,8 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath): graph.add_nodes_from(fill_stitch_graph.nodes(data=True)) if underpath: - boundary_points, travel_edges = build_travel_edges(shape, fill_stitch_angle) + boundary_points, travel_edges = build_travel_edges( + shape, fill_stitch_angle) # This will ensure that a path traveling inside the shape can reach its # target on the outline, which will be one of the points added above. @@ -349,7 +361,7 @@ def get_segments(graph): for start, end, key, data in graph.edges(keys=True, data=True): if key == 'segment': segments.append(data["geometry"]) - #segments.append(shgeo.LineString((start, end))) + # segments.append(shgeo.LineString((start, end))) return segments @@ -371,7 +383,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): # This makes the distance calculations below a bit faster. We're # not looking for high precision anyway. - outline = shape.boundary.simplify(0.5 * PIXELS_PER_MM, preserve_topology=False) + outline = shape.boundary.simplify( + 0.5 * PIXELS_PER_MM, preserve_topology=False) for ls in travel_edges: # In most cases, ls will be a simple line segment. If we're @@ -389,7 +402,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): if segment.crosses(ls): start = segment.coords[0] end = segment.coords[-1] - fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge) + fill_stitch_graph[start][end]['segment']['underpath_edges'].append( + edge) # The weight of a travel edge is the length of the line segment. weight = p1.distance(p2) @@ -458,9 +472,12 @@ def build_travel_edges(shape, fill_angle): else: scale = 1.0 - grating1 = travel_grating(shape, fill_angle + math.pi / 4, scale * 2 * PIXELS_PER_MM) - grating2 = travel_grating(shape, fill_angle - math.pi / 4, scale * 2 * PIXELS_PER_MM) - grating3 = travel_grating(shape, fill_angle - math.pi / 2, scale * math.sqrt(2) * PIXELS_PER_MM) + grating1 = travel_grating( + shape, fill_angle + math.pi / 4, scale * 2 * PIXELS_PER_MM) + grating2 = travel_grating( + shape, fill_angle - math.pi / 4, scale * 2 * PIXELS_PER_MM) + grating3 = travel_grating( + shape, fill_angle - math.pi / 2, scale * math.sqrt(2) * PIXELS_PER_MM) debug.add_layer("auto-fill travel") debug.log_line_strings(grating1, "grating1") @@ -471,10 +488,12 @@ def build_travel_edges(shape, fill_angle): for ls in mls for coord in ls.coords] - diagonal_edges = ensure_multi_line_string(grating1.symmetric_difference(grating2)) + diagonal_edges = ensure_multi_line_string( + grating1.symmetric_difference(grating2)) # without this, floating point inaccuracies prevent the intersection points from lining up perfectly. - vertical_edges = ensure_multi_line_string(snap(grating3.difference(grating1), diagonal_edges, 0.005)) + vertical_edges = ensure_multi_line_string( + snap(grating3.difference(grating1), diagonal_edges, 0.005)) return endpoints, chain(diagonal_edges, vertical_edges) @@ -536,7 +555,8 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None last_vertex, last_key = current_vertex, current_key vertex_stack.pop() else: - ignore, next_vertex, next_key = pick_edge(graph.edges(current_vertex, keys=True)) + ignore, next_vertex, next_key = pick_edge( + graph.edges(current_vertex, keys=True)) vertex_stack.append((next_vertex, next_key)) graph.remove_edge(current_vertex, next_vertex, next_key) @@ -565,7 +585,8 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None # relevant in the case that the user specifies an underlay with an inset # value, because the starting point (and possibly ending point) can be # inside the shape. - outline_nodes = [node for node, outline in travel_graph.nodes(data="outline") if outline is not None] + outline_nodes = [node for node, outline in travel_graph.nodes( + data="outline") if outline is not None] real_end = nearest_node(outline_nodes, ending_point) path.append(PathEdge((ending_node, real_end), key="outline")) @@ -639,28 +660,31 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): # stitch. return stitches[1:] -def stitch_line(stitches, stitching_direction, geometry,projected_points, max_stitch_length,row_spacing,skip_last,offset_by_half): - #print(start_point) - #print(geometry[0]) - #if stitching_direction == -1: - # geometry.coords = geometry.coords[::-1] - stitched_line, stitched_line_origin = raster_line_string_with_priority_points_graph(geometry,max_stitch_length,stitching_direction,projected_points,abs(row_spacing),offset_by_half) +def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): + # print(start_point) + # print(geometry[0]) + # if stitching_direction == -1: + # geometry.coords = geometry.coords[::-1] + stitched_line, stitched_line_origin = raster_line_string_with_priority_points_graph( + geometry, max_stitch_length, stitching_direction, projected_points, abs(row_spacing), offset_by_half) stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) - for i in range(1,len(stitched_line)): + for i in range(1, len(stitched_line)): stitches.append(Stitch(*stitched_line[i], tags=('fill_row'))) - + if not skip_last: - if stitching_direction==1: - stitches.append(Stitch(*geometry.coords[-1], tags=('fill_row_end',))) + if stitching_direction == 1: + stitches.append( + Stitch(*geometry.coords[-1], tags=('fill_row_end',))) else: - stitches.append(Stitch(*geometry.coords[0], tags=('fill_row_end',))) + stitches.append( + Stitch(*geometry.coords[0], tags=('fill_row_end',))) @debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, - running_stitch_length, staggers, skip_last, offsetted_line, offset_by_half): +def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, + running_stitch_length, staggers, skip_last, offsetted_line, offset_by_half): path = collapse_sequential_outline_edges(path) stitches = [] @@ -678,18 +702,24 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, projected_points = current_edge['projected_points'] stitching_direction = 1 if (abs(edge[0][0]-path_geometry.coords[0][0])+abs(edge[0][1]-path_geometry.coords[0][1]) > - abs(edge[0][0]-path_geometry.coords[-1][0])+abs(edge[0][1]-path_geometry.coords[-1][1])): + abs(edge[0][0]-path_geometry.coords[-1][0])+abs(edge[0][1]-path_geometry.coords[-1][1])): stitching_direction = -1 - stitch_line(new_stitches, stitching_direction, path_geometry,projected_points, max_stitch_length,row_spacing,skip_last,offset_by_half) + stitch_line(new_stitches, stitching_direction, path_geometry, projected_points, + max_stitch_length, row_spacing, skip_last, offset_by_half) current_edge['already_rastered'] = True - transfer_points_to_surrounding_graph(fill_stitch_graph,current_edge,row_spacing,False,new_stitches,overnext_neighbor=True) - transfer_points_to_surrounding_graph(fill_stitch_graph,current_edge,row_spacing,offset_by_half,new_stitches,overnext_neighbor=False,transfer_forbidden_points=offset_by_half) + transfer_points_to_surrounding_graph( + fill_stitch_graph, current_edge, row_spacing, False, new_stitches, overnext_neighbor=True) + transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, row_spacing, offset_by_half, + new_stitches, overnext_neighbor=False, transfer_forbidden_points=offset_by_half) stitches.extend(new_stitches) else: - stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) - travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) + stitch_row(stitches, edge[0], edge[1], angle, + row_spacing, max_stitch_length, staggers, skip_last) + travel_graph.remove_edges_from( + fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) + stitches.extend( + travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) return stitches diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py index 63746310..162c4cfb 100644 --- a/lib/stitches/constants.py +++ b/lib/stitches/constants.py @@ -3,39 +3,60 @@ import math # Used in the simplify routine of shapely simplification_threshold = 0.01 -# If a transferred point is closer than this value to one of its neighbors, it will be checked whether it can be removed +# If a transferred point is closer than this value to one of its neighbors, +# it will be checked whether it can be removed distance_thresh_remove_transferred_point = 0.15 # If a line segment is shorter than this threshold it is handled as a single point line_lengh_seen_as_one_point = 0.05 -# E.g. to check whether a point is already present in a point list, the point is allowed to be this value in distance apart +# E.g. to check whether a point is already present in a point list, +# the point is allowed to be this value in distance apart point_spacing_to_be_considered_equal = 0.05 -# Adjacent geometry should have points closer than offset*offset_factor_for_adjacent_geometry to be considered adjacent +# Adjacent geometry should have points closer than +# offset*offset_factor_for_adjacent_geometry to be considered adjacent offset_factor_for_adjacent_geometry = 1.5 -# Transfer point distance is used for projecting points from already rastered geometry to adjacent geometry -# (max spacing transfer_point_distance_factor*offset) to get a more regular pattern +# Transfer point distance is used for projecting points from already +# rastered geometry to adjacent geometry +# (max spacing transfer_point_distance_factor*offset) +# to get a more regular pattern transfer_point_distance_factor = 1.5 # Used to handle numerical inaccuracies during comparisons -eps = 1E-3 +eps = 1e-3 -factor_offset_starting_points=0.5 #When entering and leaving a child from a parent we introduce an offset of abs_offset*factor_offset_starting_points so - #that entering and leaving points are not lying above each other. +# When entering and leaving a child from a parent we introduce an offset of +# abs_offset*factor_offset_starting_points +# so that entering and leaving points are not lying above each other. +factor_offset_starting_points = 0.5 -factor_offset_remove_points=0.5 #if points are closer than abs_offset*factor_offset_remove_points one of it is removed +# if points are closer than abs_offset*factor_offset_remove_points one of it is removed +factor_offset_remove_points = 0.5 -fac_offset_edge_shift = 0.25 #if an unshifted relevant edge is closer than abs_offset*fac_offset_edge_shift to the line segment created by the shifted edge, - #the shift is allowed - otherwise the edge must not be shifted. +# if an unshifted relevant edge is closer than +# abs_offset*fac_offset_edge_shift +# to the line segment created by the shifted edge, +# the shift is allowed - otherwise the edge must not be shifted. +fac_offset_edge_shift = 0.25 -limiting_angle = math.pi*15/180.0 #decides whether the point belongs to a hard edge (must use this point during sampling) or soft edge (do not necessarily need to use this point) -limiting_angle_straight = math.pi*0.5/180.0 #angles straighter (smaller) than this are considered as more or less straight (no concrete edges required for path segments having only angles <= this value) +# decides whether the point belongs to a hard edge (must use this point during sampling) +# or soft edge (do not necessarily need to use this point) +limiting_angle = math.pi * 15 / 180.0 +# angles straighter (smaller) than this are considered as more or less straight +# (no concrete edges required for path segments having only angles <= this value) +limiting_angle_straight = math.pi * 0.5 / 180.0 -factor_offset_remove_dense_points=0.2 #if a point distance to the connected line of its two neighbors is smaller than abs_offset times this factor, this point will be removed if the stitching distance will not be exceeded +# if a point distance to the connected line of its two neighbors is smaller than +# abs_offset times this factor, this point will be removed if the stitching distance will not be exceeded +factor_offset_remove_dense_points = 0.2 -factor_offset_forbidden_point = 1.0 #if a soft edge is closer to a forbidden point than abs_offset*this factor it will be marked as forbidden. +# if a soft edge is closer to a forbidden point than abs_offset*this factor it will be marked as forbidden. +factor_offset_forbidden_point = 1.0 -factor_segment_length_direct_preferred_over_overnext = 0.5 #usually overnext projected points are preferred. If an overnext projected point would create a much smaller segment than a direct projected point we might prefer the direct projected point +# usually overnext projected points are preferred. +# If an overnext projected point would create a much smaller segment than a direct +# projected point we might prefer the direct projected point +factor_segment_length_direct_preferred_over_overnext = 0.5 diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 4e1669e9..9a7254e2 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -12,8 +12,10 @@ from ..utils import Point as InkstitchPoint from ..utils import cache from ..stitch_plan import Stitch + def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): - rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip) + rows_of_segments = intersect_region_with_grating( + shape, angle, row_spacing, end_row_spacing, flip) groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing) return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last) @@ -73,7 +75,8 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge stitches.append(beg) - first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers) + first_stitch = adjust_stagger( + beg, angle, row_spacing, max_stitch_length, staggers) # we might have chosen our first stitch just outside this row, so move back in if (first_stitch - beg) * row_direction < 0: @@ -82,13 +85,15 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge offset = (first_stitch - beg).length() while offset < segment_length: - stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row'))) + stitches.append( + Stitch(beg + offset * row_direction, tags=('fill_row'))) offset += max_stitch_length if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM and not skip_last: stitches.append(end) -def extend_line(line, minx,maxx,miny,maxy): + +def extend_line(line, minx, maxx, miny, maxy): line = line.simplify(0.01, False) upper_left = InkstitchPoint(minx, miny) @@ -103,26 +108,30 @@ def extend_line(line, minx,maxx,miny,maxy): point4 = InkstitchPoint(*line.coords[-1]) new_ending_point = point4+(point4-point3).unit()*length - line = LineString([new_starting_point.as_tuple()]+line.coords[1:-1]+[new_ending_point.as_tuple()]) + line = LineString([new_starting_point.as_tuple()] + + line.coords[1:-1]+[new_ending_point.as_tuple()]) def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): - + row_spacing = abs(row_spacing) (minx, miny, maxx, maxy) = shape.bounds upper_left = InkstitchPoint(minx, miny) rows = [] - extend_line(line, minx,maxx,miny,maxy) #extend the line towards the ends to increase probability that all offsetted curves cross the shape + # extend the line towards the ends to increase probability that all offsetted curves cross the shape + extend_line(line, minx, maxx, miny, maxy) line_offsetted = line res = line_offsetted.intersection(shape) while isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)) or (not res.is_empty and len(res.coords) > 1): if isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): - runs = [line_string.coords for line_string in res.geoms if (not line_string.is_empty and len(line_string.coords) > 1)] + runs = [line_string.coords for line_string in res.geoms if ( + not line_string.is_empty and len(line_string.coords) > 1)] else: runs = [res.coords] - runs.sort(key=lambda seg: (InkstitchPoint(*seg[0]) - upper_left).length()) + runs.sort(key=lambda seg: ( + InkstitchPoint(*seg[0]) - upper_left).length()) if flip: runs.reverse() runs = [tuple(reversed(run)) for run in runs] @@ -130,8 +139,8 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0: rows.append(runs) else: - rows.insert(0,runs) - line_offsetted = line_offsetted.parallel_offset(row_spacing,'left',5) + rows.insert(0, runs) + line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) if row_spacing < 0: line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) @@ -139,12 +148,13 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0 and not isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): if (res.is_empty or len(res.coords) == 1): row_spacing = -row_spacing - #print("Set to right") - line_offsetted = line.parallel_offset(row_spacing,'left',5) - line_offsetted.coords = line_offsetted.coords[::-1] #using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this + # print("Set to right") + line_offsetted = line.parallel_offset(row_spacing, 'left', 5) + # using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this + line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - + return rows @@ -174,7 +184,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non # angle degrees clockwise and ask for the new bounding box. The max # and min y tell me how far to go. - _, start, _, end = shapely.affinity.rotate(shape, angle, origin='center', use_radians=True).bounds + _, start, _, end = shapely.affinity.rotate( + shape, angle, origin='center', use_radians=True).bounds # convert start and end to be relative to center (simplifies things later) start -= center.y @@ -211,7 +222,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non runs = [res.coords] if runs: - runs.sort(key=lambda seg: (InkstitchPoint(*seg[0]) - upper_left).length()) + runs.sort(key=lambda seg: ( + InkstitchPoint(*seg[0]) - upper_left).length()) if flip: runs.reverse() @@ -220,7 +232,9 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non rows.append(runs) if end_row_spacing: - current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height) + current_row_y += row_spacing + \ + (end_row_spacing - row_spacing) * \ + ((current_row_y - start) / height) else: current_row_y += row_spacing @@ -237,7 +251,8 @@ def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length if (swap): (beg, end) = (end, beg) - stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last) + stitch_row(stitches, beg, end, angle, row_spacing, + max_stitch_length, staggers, skip_last) swap = not swap -- cgit v1.3.1 From 3caaae693893354ff10472044116e623e219e633 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 10 Nov 2021 17:23:24 +0100 Subject: bug fixing --- lib/elements/auto_fill.py | 3 ++- lib/patterns.py | 4 ++-- lib/stitches/fill.py | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 81abf7ad..094ad91e 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -456,7 +456,8 @@ class AutoFill(EmbroideryElement): stitches=path) stitch_groups.append(stitch_group) elif self.fill_method == 2: # Guided Auto Fill - lines = get_patterns(self.node, "#inkstitch-guide-line-marker") + lines = get_patterns( + self.node, "#inkstitch-guide-line-marker", False, True) lines = lines['stroke_patterns'] if not lines or lines[0].is_empty: inkex.errormsg( diff --git a/lib/patterns.py b/lib/patterns.py index 789d5f89..7ec4d082 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -65,7 +65,7 @@ def _apply_fill_patterns(patterns, patches): patch.stitches = patch_points -def get_patterns(node, marker_id): +def get_patterns(node, marker_id, get_fills=True, get_strokes=True): from .elements import EmbroideryElement from .elements.auto_fill import auto_fill from .elements.stroke import Stroke @@ -88,7 +88,7 @@ def get_patterns(node, marker_id): for ring in linear_rings: fills.append(shgeo.Polygon(ring)) - if stroke is not None: + if get_strokes and stroke is not None: stroke_pattern = Stroke(pattern).paths line_strings = [shgeo.LineString(path) for path in stroke_pattern] strokes.append(shgeo.MultiLineString(line_strings)) diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 9a7254e2..55ce09a4 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -7,6 +7,7 @@ import math import shapely from shapely.geometry.linestring import LineString +from shapely.ops import linemerge from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache @@ -108,7 +109,7 @@ 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()] + + return LineString([new_starting_point.as_tuple()] + line.coords[1:-1]+[new_ending_point.as_tuple()]) @@ -119,7 +120,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing upper_left = InkstitchPoint(minx, miny) rows = [] # extend the line towards the ends to increase probability that all offsetted curves cross the shape - extend_line(line, minx, maxx, miny, maxy) + line = extend_line(line, minx, maxx, miny, maxy) line_offsetted = line res = line_offsetted.intersection(shape) @@ -141,6 +142,17 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing else: rows.insert(0, runs) line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) + if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest + lines = linemerge(line_offsetted) + lines = list(line_offsetted.geoms) + max_length = -1 + max_length_idx = -1 + for idx, subline in enumerate(lines): + if subline.length > max_length: + max_length = subline.length + max_length_idx = idx + line_offsetted = lines[max_length_idx] + if row_spacing < 0: line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) -- cgit v1.3.1 From d445b38629a902f6c13565a83ed81a91b6458480 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 21 Nov 2021 12:44:06 +0100 Subject: bug fixing+first spiral implementation --- lib/elements/auto_fill.py | 2 +- lib/stitches/ConnectAndSamplePattern.py | 164 ++++++++++++++++++++++++++++++-- lib/stitches/LineStringSampling.py | 23 +++-- lib/stitches/PointTransfer.py | 14 +-- lib/stitches/StitchPattern.py | 154 +++++++++++++++++++++++++++--- lib/stitches/fill.py | 28 +++++- requirements.txt | 2 + 7 files changed, 348 insertions(+), 39 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 094ad91e..dc678087 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -61,7 +61,7 @@ class AutoFill(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Closest point"), _("Inner to Outer"), _("single Spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index e8f1def5..2410f3ca 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -3,7 +3,12 @@ from shapely.geometry import Point, MultiPoint from shapely.ops import nearest_points from collections import namedtuple from depq import DEPQ +import trimesh +import numpy as np +from scipy import spatial import math +from shapely.geometry import asLineString +from anytree import PreOrderIter from ..stitches import LineStringSampling from ..stitches import PointTransfer from ..stitches import constants @@ -48,8 +53,7 @@ def cut(line, distance): def connect_raster_tree_nearest_neighbor( - tree, used_offset, stitch_distance, close_point, offset_by_half -): + 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 @@ -338,8 +342,7 @@ def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): def create_nearest_points_list( - travel_line, children_list, threshold, threshold_hard, preferred_direction=0 -): + 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 @@ -456,8 +459,7 @@ def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distan def connect_raster_tree_from_inner_to_outer( - tree, used_offset, stitch_distance, close_point, offset_by_half -): + 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 @@ -772,3 +774,153 @@ def connect_raster_tree_from_inner_to_outer( assert len(result_coords) == len(result_coords_origin) return result_coords, result_coords_origin + + +# Partly taken from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py +def interpolate_LinearRings(a, b, start=None, step=.005): + """ + Interpolate between two LinearRings + Parameters + ------------- + a : shapely.geometry.Polygon.LinearRing + LinearRing start point will lie on + b : shapely.geometry.Polygon.LinearRing + LinearRing end point will lie on + start : (2,) float, or None + Point to start at + step : float + How far apart should points on + the path be. + Returns + ------------- + path : (n, 2) float + Path interpolated between two LinearRings + """ + + # resample the first LinearRing so every sample is spaced evenly + ra = trimesh.path.traversal.resample_path( + a, step=step) + if not a.is_ccw: + ra = ra[::-1] + + assert trimesh.path.util.is_ccw(ra) + if start is not None: + # find the closest index on LinerRing 'a' + # by creating a KDTree + tree_a = spatial.cKDTree(ra) + index = tree_a.query(start)[1] + ra = np.roll(ra, -index, axis=0) + + # resample the second LinearRing for even spacing + rb = trimesh.path.traversal.resample_path(b, + step=step) + if not b.is_ccw: + rb = rb[::-1] + + # we want points on 'b' that correspond index- wise + # the resampled points on 'a' + tree_b = spatial.cKDTree(rb) + # points on b with corresponding indexes to ra + pb = rb[tree_b.query(ra)[1]] + + # linearly interpolate between 'a' and 'b' + weights = np.linspace(0.0, 1.0, len(ra)).reshape((-1, 1)) + + # start on 'a' and end on 'b' + points = (ra * (1.0 - weights)) + (pb * weights) + + result = LineString(points) + + return result.simplify(constants.simplification_threshold, False) + + +def connect_raster_tree_spiral( + tree, used_offset, stitch_distance, close_point, offset_by_half): + """ + Takes the offsetted curves organized as tree, connects and samples them as a spiral. + It expects that each node in the tree has max. one child + Input: + -tree: contains the offsetted curves in a 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 spiral and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ + + abs_offset = abs(used_offset) + if tree.is_leaf: + return LineStringSampling.raster_line_string_with_priority_points( + tree.val, + 0, + tree.val.length, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + result_coords = [] + result_coords_origin = [] + starting_point = close_point.coords[0] + # iterate to the second last level + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): + ring1 = node.val + ring2 = node.children[0].val + + part_spiral = interpolate_LinearRings( + ring1, ring2, starting_point) + + (own_coords, own_coords_origin) = LineStringSampling.raster_line_string_with_priority_points( + part_spiral, + 0, + part_spiral.length, + stitch_distance, + node.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + PointTransfer.transfer_points_to_surrounding( + node, + used_offset, + offset_by_half, + own_coords, + own_coords_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + PointTransfer.transfer_points_to_surrounding( + node, + used_offset, + False, + own_coords, + own_coords_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + result_coords.extend(own_coords) + result_coords_origin.extend(own_coords_origin) + + # make sure the next section starts where this + # section of the curve ends + starting_point = own_coords[-1] + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index bd20f55c..43f650e6 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -139,32 +139,35 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, angles[0] = 1.1*constants.limiting_angle angles[-1] = 1.1*constants.limiting_angle - current_distance = start_distance - + current_distance = 0 + last_point = Point(path_coords.coords[0]) # Next we merge the line points and the projected (deque) points into one list merged_point_list = [] dq_iter = 0 - for point, angle in zip(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(path_coords.coords, angles): + # if abs(point[0]-7) < 0.2 and abs(point[1]-3.3) < 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: + current_distance += last_point.distance(Point(point)) + last_point = Point(point) + while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance+start_distance: # We want to avoid setting points at soft edges close to forbidden points if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: # Check whether a previous added point is a soft edge close to the forbidden point if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and - abs(merged_point_list[-1][1]-deque_points[dq_iter][1] < abs_offset*constants.factor_offset_forbidden_point)): + abs(merged_point_list[-1][1]-deque_points[dq_iter][1]+start_distance < abs_offset*constants.factor_offset_forbidden_point)): item = merged_point_list.pop() merged_point_list.append((PointTransfer.projected_point_tuple( - point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1])) + point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1]-start_distance)) else: - merged_point_list.append(deque_points[dq_iter]) + merged_point_list.append( + (deque_points[dq_iter][0], deque_points[dq_iter][1]-start_distance)) + # merged_point_list.append(deque_points[dq_iter]) dq_iter += 1 # Check whether the current point is close to a forbidden point if (dq_iter < len(deque_points) and deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and angle < constants.limiting_angle and - abs(deque_points[dq_iter-1][1]-current_distance) < abs_offset*constants.factor_offset_forbidden_point): + abs(deque_points[dq_iter-1][1]-current_distance-start_distance) < abs_offset*constants.factor_offset_forbidden_point): point_source = PointSource.FORBIDDEN_POINT else: if angle < constants.limiting_angle: diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index da73aea0..b6e4e026 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -9,12 +9,13 @@ from ..stitches import LineStringSampling 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. - def calc_transferred_point(bisectorline, child): + """ + Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). + It returns the intersection point and its distance along the coordinates of the child or "None, None" if no + intersection was found. + """ result = bisectorline.intersection(child.val) if result.is_empty: return None, None @@ -279,11 +280,10 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra assert(len(point_list) == len(point_source_list)) -# Calculated the nearest interserction point of "bisectorline" with the coordinates of child. + +# Calculates the nearest interserction point of "bisectorline" with the coordinates of child. # It returns the intersection point and its distance along the coordinates of the child or "None, None" if no # intersection was found. - - def calc_transferred_point_graph(bisectorline, edge_geometry): result = bisectorline.intersection(edge_geometry) if result.is_empty: diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index 1edfd452..62ef2b0f 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -1,8 +1,9 @@ +from anytree.render import RenderTree from shapely.geometry.polygon import LinearRing, LineString from shapely.geometry import Polygon, MultiLineString from shapely.ops import polygonize from shapely.geometry import MultiPolygon -from anytree import AnyNode, PreOrderIter +from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter from shapely.geometry.polygon import orient from depq import DEPQ from enum import IntEnum @@ -126,6 +127,38 @@ class StitchingStrategy(IntEnum): SPIRAL = 2 +def check_and_prepare_tree_for_valid_spiral(root): + """ + Takes a tree consisting of offsetted curves. If a parent has more than one child we + cannot create a spiral. However, to make the routine more robust, we allow more than + one child if only one of the childs has own childs. The other childs are removed in this + routine then. If the routine returns true, the tree will have been cleaned up from unwanted + childs. If the routine returns false even under the mentioned weaker conditions the + tree cannot be connected by one spiral. + """ + for children in LevelOrderGroupIter(root): + if len(children) > 1: + count = 0 + child_with_children = None + for child in children: + if not child.is_leaf: + count += 1 + child_with_children = child + if count > 1: + return False + elif count == 1: + child_with_children.parent.children = [child_with_children] + else: # count == 0 means all childs have no children so we take only the longest child + max_length = 0 + longest_child = None + for child in children: + if child.val.length > max_length: + max_length = child.val.length + longest_child = child + longest_child.parent.children = [longest_child] + return True + + def offset_poly( poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): """ @@ -144,13 +177,21 @@ def offset_poly( -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 + different strategies for the connection between parent and childs. In + addition it offers the option "SPIRAL" which creates a real spiral towards inner. + In contrast to the other two options, "SPIRAL" does not end at the starting point + but at the innermost point -starting_point: Defines the starting point for the stitching Output: -List of point coordinate tuples -Tag (origin) of each point to analyze why a point was placed at this position """ + + if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: + raise ValueError( + "Single spiral geometry must not have more than one hole!") + ordered_poly = orient(poly, -1) ordered_poly = ordered_poly.simplify( constants.simplification_threshold, False) @@ -276,20 +317,105 @@ def offset_poly( 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) + elif strategy == StitchingStrategy.SPIRAL: + if not check_and_prepare_tree_for_valid_spiral(root): + raise ValueError("Geometry cannot be filled with one spiral!") + (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_spiral( + root, offset, stitch_distance, starting_point, offset_by_half) else: raise ValueError("Invalid stitching stratety!") return connected_line, connected_line_origin + + +if __name__ == "__main__": + line1 = LineString([(0, 0), (1, 0)]) + line2 = LineString([(0, 0), (3, 0)]) + + root = AnyNode( + id="root", + val=line1) + child1 = AnyNode( + id="node", + val=line1, + parent=root) + child2 = AnyNode( + id="node", + val=line1, + parent=root) + child3 = AnyNode( + id="node", + val=line2, + parent=root) + + print(RenderTree(root)) + print(check_and_prepare_tree_for_valid_spiral(root)) + print(RenderTree(root)) + print("---------------------------") + root = AnyNode( + id="root", + val=line1) + child1 = AnyNode( + id="node", + val=line1, + parent=root) + child2 = AnyNode( + id="node", + val=line1, + parent=root) + child3 = AnyNode( + id="node", + val=line2, + parent=child1) + print(RenderTree(root)) + print(check_and_prepare_tree_for_valid_spiral(root)) + print(RenderTree(root)) + + print("---------------------------") + root = AnyNode( + id="root", + val=line1) + child1 = AnyNode( + id="node", + val=line1, + parent=root) + child2 = AnyNode( + id="node", + val=line1, + parent=child1) + child3 = AnyNode( + id="node", + val=line2, + parent=child2) + print(RenderTree(root)) + print(check_and_prepare_tree_for_valid_spiral(root)) + print(RenderTree(root)) + + print("---------------------------") + root = AnyNode( + id="root", + val=line1) + child1 = AnyNode( + id="node", + val=line1, + parent=root) + child2 = AnyNode( + id="node", + val=line1, + parent=root) + child3 = AnyNode( + id="node", + val=line2, + parent=child1) + child4 = AnyNode( + id="node", + val=line2, + parent=child2) + print(RenderTree(root)) + print(check_and_prepare_tree_for_valid_spiral(root)) + print(RenderTree(root)) diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 01bfdc20..5afcb228 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -7,7 +7,7 @@ import math import shapely from shapely.geometry.linestring import LineString -from shapely.ops import linemerge +from shapely.ops import linemerge, unary_union from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache @@ -126,12 +126,34 @@ def repair_multiple_parallel_offset_curves(multi_line): return lines[max_length_idx].simplify(0.01, False) +def repair_non_simple_lines(line): + repaired = unary_union(line) + counter = 0 + # Do several iterations since we might have several concatenated selfcrossings + while repaired.geom_type != 'LineString' and counter < 4: + line_segments = [] + for line_seg in repaired.geoms: + if not line_seg.is_ring: + line_segments.append(line_seg) + + repaired = unary_union(linemerge(line_segments)) + counter += 1 + if repaired.geom_type != 'LineString': + raise ValueError( + "Guide line (or offsetted instance) is self crossing!") + else: + return repaired + + 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 = [] + + if line.geom_type != 'LineString' or not line.is_simple: + line = repair_non_simple_lines(line) # extend the line towards the ends to increase probability that all offsetted curves cross the shape line = extend_line(line, minx, maxx, miny, maxy) @@ -160,6 +182,8 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( line_offsetted) + if not line_offsetted.is_simple: + line_offsetted = repair_non_simple_lines(line_offsetted) if row_spacing < 0: line_offsetted.coords = line_offsetted.coords[::-1] @@ -173,6 +197,8 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( line_offsetted) + if not line_offsetted.is_simple: + line_offsetted = repair_non_simple_lines(line_offsetted) # 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) diff --git a/requirements.txt b/requirements.txt index 309e1be6..09999307 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,8 @@ flask fonttools anytree depq +trimesh +scipy pywinutils; sys.platform == 'win32' pywin32; sys.platform == 'win32' -- cgit v1.3.1 From b5c7f637c14beaf9ba075c0c4445fbd3541a8270 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 24 Nov 2021 19:05:20 +0100 Subject: minor changes --- lib/elements/auto_fill.py | 2 +- lib/stitches/ConnectAndSamplePattern.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index dc678087..55c9e2d0 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -61,7 +61,7 @@ class AutoFill(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("single Spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 2410f3ca..2c538e5d 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -877,11 +877,13 @@ def connect_raster_tree_spiral( part_spiral = interpolate_LinearRings( ring1, ring2, starting_point) + node.val = part_spiral + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): (own_coords, own_coords_origin) = LineStringSampling.raster_line_string_with_priority_points( - part_spiral, + node.val, 0, - part_spiral.length, + node.val.length, stitch_distance, node.transferred_point_priority_deque, abs_offset, @@ -890,7 +892,7 @@ def connect_raster_tree_spiral( PointTransfer.transfer_points_to_surrounding( node, - used_offset, + -used_offset, offset_by_half, own_coords, own_coords_origin, @@ -905,7 +907,7 @@ def connect_raster_tree_spiral( if offset_by_half: PointTransfer.transfer_points_to_surrounding( node, - used_offset, + -used_offset, False, own_coords, own_coords_origin, -- cgit v1.3.1 From 95a933161616e5860862435d4b999db49b8e50a5 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 28 Jan 2022 22:31:55 +0100 Subject: added legacy fill again --- lib/elements/auto_fill.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 55c9e2d0..3f75180b 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -14,7 +14,7 @@ from shapely import geometry as shgeo from shapely.validation import explain_validity from ..i18n import _ from ..stitch_plan import StitchGroup -from ..stitches import auto_fill +from ..stitches import auto_fill, fill from ..stitches import StitchPattern from ..utils import cache, version from .element import param @@ -55,7 +55,7 @@ class AutoFill(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index=2) + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @@ -84,7 +84,7 @@ class AutoFill(EmbroideryElement): unit='deg', type='float', sort_index=4, - select_items=[('fill_method', 0)], + select_items=[('fill_method', 0), ('fill_method', 3)], default=0) @cache def angle(self): @@ -103,7 +103,8 @@ class AutoFill(EmbroideryElement): 'Skipping it decreases stitch count and density.'), type='boolean', sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2)], + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], default=False) def skip_last(self): return self.get_boolean_param("skip_last", False) @@ -116,7 +117,8 @@ class AutoFill(EmbroideryElement): '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)], + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -155,7 +157,7 @@ class AutoFill(EmbroideryElement): '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)], + select_items=[('fill_method', 0), ('fill_method', 3)], default=4) def staggers(self): return max(self.get_int_param("staggers", 4), 1) @@ -481,6 +483,21 @@ class AutoFill(EmbroideryElement): self.underpath, self.interlaced)) stitch_groups.append(stitch_group) + elif self.fill_method == 3: # Legacy Fill + stitch_lists = fill.legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers, + self.skip_last) + for stitch_list in stitch_lists: + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=stitch_list) + stitch_groups.append(stitch_group) except Exception: if hasattr(sys, 'gettrace') and sys.gettrace(): -- cgit v1.3.1 From 82216b184c669d6dea26672e5c0771146e62ca39 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sat, 29 Jan 2022 09:53:50 +0100 Subject: remove some pattern and marker mixups and some style issues --- lib/elements/auto_fill.py | 29 +++++++++---------- lib/elements/utils.py | 4 +-- lib/extensions/base.py | 6 ++-- lib/extensions/params.py | 2 +- lib/marker.py | 41 ++++++++++++++++++++++++++- lib/patterns.py | 50 ++++----------------------------- lib/stitches/ConnectAndSamplePattern.py | 6 ++-- lib/stitches/LineStringSampling.py | 2 +- lib/stitches/PointTransfer.py | 4 +-- lib/stitches/StitchPattern.py | 3 +- lib/stitches/fill.py | 2 +- 11 files changed, 72 insertions(+), 77 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 3f75180b..614e6887 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -3,27 +3,26 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +import logging import math +import re import sys import traceback -import re -import logging -import inkex +import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity + from ..i18n import _ +from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import auto_fill, fill -from ..stitches import StitchPattern -from ..utils import cache, version -from .element import param -from .element import EmbroideryElement -from ..patterns import get_patterns -from .validation import ValidationWarning -from ..utils import Point as InkstitchPoint +from ..stitches import StitchPattern, auto_fill, fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationWarning class SmallShapeWarning(ValidationWarning): @@ -393,7 +392,8 @@ class AutoFill(EmbroideryElement): else: return None - def to_stitch_groups(self, last_patch): + def to_stitch_groups(self, last_patch): # noqa: C901 + # TODO: split this up do_legacy_fill() etc. stitch_groups = [] starting_point = self.get_starting_point(last_patch) @@ -458,9 +458,8 @@ class AutoFill(EmbroideryElement): stitches=path) stitch_groups.append(stitch_group) elif self.fill_method == 2: # Guided Auto Fill - lines = get_patterns( - self.node, "#inkstitch-guide-line-marker", False, True) - lines = lines['stroke_patterns'] + lines = get_marker_elements(self.node, "guide-line", False, True) + lines = lines['stroke'] if not lines or lines[0].is_empty: inkex.errormsg( _("No line marked as guide line found within the same group as patch")) diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9fec8b63..9b9b8f14 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -4,7 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. from ..commands import is_command -from ..patterns import is_pattern +from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) from .auto_fill import AutoFill @@ -29,7 +29,7 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif is_pattern(node): + elif has_marker(node, 'pattern'): return [PatternObject(node)] elif node.tag in EMBROIDERABLE_TAGS: diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 56385458..cf846324 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -16,7 +16,7 @@ from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone from ..i18n import _ -from ..patterns import is_pattern +from ..marker import has_marker from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) @@ -161,10 +161,10 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not is_pattern(node): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node, 'pattern'): nodes.append(node) # add images, text and patterns for the troubleshoot extension - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_pattern(node)): + elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node, 'pattern')): nodes.append(node) return nodes diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 30f6ba1d..55963625 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -296,7 +296,7 @@ class ParamsTab(ScrolledPanel): widgets[3].Show(True) choice["last_initialized_choice"] = current_selection - def __do_layout(self, only_settings_grid=False): + def __do_layout(self, only_settings_grid=False): # noqa: C901 # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) diff --git a/lib/marker.py b/lib/marker.py index 1d9145e7..3c145145 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -7,10 +7,12 @@ from copy import deepcopy from os import path import inkex +from shapely import geometry as shgeo +from .svg.tags import EMBROIDERABLE_TAGS from .utils import cache, get_bundled_dir -MARKER = ['pattern'] +MARKER = ['pattern', 'guide-line'] def ensure_marker(svg, marker): @@ -35,3 +37,40 @@ def set_marker(node, position, marker): style = [i for i in style if not i.startswith('marker-%s' % position)] style.append('marker-%s:url(#inkstitch-%s-marker)' % (position, marker)) node.set('style', ";".join(style)) + + +def get_marker_elements(node, marker, get_fills=True, get_strokes=True): + from .elements import EmbroideryElement + from .elements.stroke import Stroke + + fills = [] + strokes = [] + xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-%s-marker)')]" % marker + markers = node.xpath(xpath, namespaces=inkex.NSS) + for marker in markers: + if marker.tag not in EMBROIDERABLE_TAGS: + continue + + element = EmbroideryElement(marker) + fill = element.get_style('fill') + stroke = element.get_style('stroke') + + if get_fills and fill is not None: + fill = Stroke(marker).paths + linear_rings = [shgeo.LinearRing(path) for path in fill] + for ring in linear_rings: + fills.append(shgeo.Polygon(ring)) + + if get_strokes and stroke is not None: + stroke = Stroke(marker).paths + line_strings = [shgeo.LineString(path) for path in stroke] + strokes.append(shgeo.MultiLineString(line_strings)) + + return {'fill': fills, 'stroke': strokes} + + +def has_marker(node, marker): + if node.tag not in EMBROIDERABLE_TAGS: + return False + style = node.get('style') or '' + return "marker-start:url(#inkstitch-%s-marker)" % marker in style diff --git a/lib/patterns.py b/lib/patterns.py index 7ec4d082..1650523c 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -3,25 +3,17 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import inkex from shapely import geometry as shgeo +from .marker import get_marker_elements from .stitch_plan import Stitch -from .svg.tags import EMBROIDERABLE_TAGS from .utils import Point -def is_pattern(node): - if node.tag not in EMBROIDERABLE_TAGS: - return False - style = node.get('style') or '' - return "marker-start:url(#inkstitch-pattern-marker)" in style - - def apply_patterns(patches, node): - patterns = get_patterns(node, "#inkstitch-pattern-marker") - _apply_fill_patterns(patterns['fill_patterns'], patches) - _apply_stroke_patterns(patterns['stroke_patterns'], patches) + patterns = get_marker_elements(node, "pattern") + _apply_fill_patterns(patterns['fill'], patches) + _apply_stroke_patterns(patterns['stroke'], patches) def _apply_stroke_patterns(patterns, patches): @@ -32,8 +24,7 @@ def _apply_stroke_patterns(patterns, patches): patch_points.append(stitch) if i == len(patch.stitches) - 1: continue - intersection_points = _get_pattern_points( - stitch, patch.stitches[i+1], pattern) + intersection_points = _get_pattern_points(stitch, patch.stitches[i+1], pattern) for point in intersection_points: patch_points.append(Stitch(point, tags=('pattern_point',))) patch.stitches = patch_points @@ -65,37 +56,6 @@ def _apply_fill_patterns(patterns, patches): patch.stitches = patch_points -def get_patterns(node, marker_id, get_fills=True, get_strokes=True): - from .elements import EmbroideryElement - from .elements.auto_fill import auto_fill - from .elements.stroke import Stroke - - fills = [] - strokes = [] - xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url("+marker_id+")')]" - patterns = node.xpath(xpath, namespaces=inkex.NSS) - for pattern in patterns: - if pattern.tag not in EMBROIDERABLE_TAGS: - continue - - element = EmbroideryElement(pattern) - fill = element.get_style('fill') - stroke = element.get_style('stroke') - - if fill is not None: - fill_pattern = Stroke(pattern).paths - linear_rings = [shgeo.LinearRing(path) for path in fill_pattern] - for ring in linear_rings: - fills.append(shgeo.Polygon(ring)) - - if get_strokes and stroke is not None: - stroke_pattern = Stroke(pattern).paths - line_strings = [shgeo.LineString(path) for path in stroke_pattern] - strokes.append(shgeo.MultiLineString(line_strings)) - - return {'fill_patterns': fills, 'stroke_patterns': strokes} - - def _get_pattern_points(first, second, pattern): points = [] intersection = shgeo.LineString([first, second]).intersection(pattern) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 33a1ba6d..1cf2b2a1 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -7,7 +7,6 @@ import trimesh import numpy as np from scipy import spatial import math -from shapely.geometry import asLineString from anytree import PreOrderIter from ..stitches import LineStringSampling from ..stitches import PointTransfer @@ -52,7 +51,7 @@ def cut(line, distance): return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) -def connect_raster_tree_nearest_neighbor( +def connect_raster_tree_nearest_neighbor( # noqa: C901 tree, used_offset, stitch_distance, close_point, offset_by_half): """ Takes the offsetted curves organized as tree, connects and samples them. @@ -458,8 +457,7 @@ def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distan return line_segment.coords[1] -def connect_raster_tree_from_inner_to_outer( - tree, used_offset, stitch_distance, close_point, offset_by_half): +def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them. Strategy: A connection from parent to child is made as fast as possible to diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index 43f650e6..71660e2d 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -70,7 +70,7 @@ def calculate_line_angles(line): return Angles -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): """ Rasters a line between start_distance and end_distance. diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index b6e4e026..93fe02c5 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -36,7 +36,7 @@ def calc_transferred_point(bisectorline, child): return point, priority -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): """ @@ -305,7 +305,7 @@ def calc_transferred_point_graph(bisectorline, edge_geometry): return point, priority -def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, +def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): """ Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index 62ef2b0f..4a38c0bc 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -159,8 +159,7 @@ def check_and_prepare_tree_for_valid_spiral(root): return True -def offset_poly( - poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): +def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 5afcb228..ceac56d9 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -145,7 +145,7 @@ def repair_non_simple_lines(line): return repaired -def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): +def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): # noqa: C901 row_spacing = abs(row_spacing) (minx, miny, maxx, maxy) = shape.bounds -- cgit v1.3.1 From 3d1600ed039c9078bcb4a28328ab60eb96994dfd Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sun, 30 Jan 2022 15:48:51 +0100 Subject: * autofill to fillstitch * remove too complex warning for fillstitch * some marker adjustments --- lib/elements/__init__.py | 3 +- lib/elements/auto_fill.py | 533 ------------------------------------- lib/elements/clone.py | 38 +-- lib/elements/element.py | 12 +- lib/elements/fill_stitch.py | 624 ++++++++++++++++++++++++++++++++++++++++++++ lib/elements/marker.py | 32 +++ lib/elements/pattern.py | 33 --- lib/elements/utils.py | 13 +- lib/extensions/base.py | 6 +- lib/extensions/cleanup.py | 4 +- lib/extensions/params.py | 5 +- lib/marker.py | 13 +- symbols/marker.svg | 32 +-- 13 files changed, 713 insertions(+), 635 deletions(-) delete mode 100644 lib/elements/auto_fill.py create mode 100644 lib/elements/fill_stitch.py create mode 100644 lib/elements/marker.py delete mode 100644 lib/elements/pattern.py (limited to 'lib/elements') diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index bb5c95ba..00933f36 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.py @@ -3,11 +3,10 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from .auto_fill import AutoFill from .clone import Clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill +from .fill_stitch import FillStitch from .image import ImageObject from .polyline import Polyline from .satin_column import SatinColumn diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py deleted file mode 100644 index 614e6887..00000000 --- a/lib/elements/auto_fill.py +++ /dev/null @@ -1,533 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import logging -import math -import re -import sys -import traceback - -import inkex -from shapely import geometry as shgeo -from shapely.validation import explain_validity - -from ..i18n import _ -from ..marker import get_marker_elements -from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, fill -from ..svg import PIXELS_PER_MM -from ..svg.tags import INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint -from ..utils import cache, version -from .element import EmbroideryElement, param -from .validation import ValidationWarning - - -class SmallShapeWarning(ValidationWarning): - name = _("Small Fill") - description = _("This fill object is so small that it would probably look better as running stitch or satin column. " - "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " - "the outline instead.") - - -class ExpandWarning(ValidationWarning): - name = _("Expand") - description = _("The expand parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use original size instead.") - - -class UnderlayInsetWarning(ValidationWarning): - name = _("Inset") - description = _("The underlay inset parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use the original size instead.") - - -class AutoFill(EmbroideryElement): - element_name = _("AutoFill") - - @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) - def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) - def fill_method(self): - return self.get_int_param('fill_method', 0) - - @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) - def tangential_strategy(self): - return self.get_int_param('tangential_strategy', 1) - - @property - @param('join_style', _('Join Style'), type='dropdown', default=0, - options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) - def join_style(self): - return self.get_int_param('join_style', 0) - - @property - @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) - def interlaced(self): - return self.get_boolean_param('interlaced', True) - - @property - @param('angle', - _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), - unit='deg', - type='float', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - # SVG spec says the default fill is black - return self.get_style("fill", "#000000") - - @property - @param( - 'skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def skip_last(self): - return self.get_boolean_param("skip_last", False) - - @property - @param( - 'flip', - _('Flip fill (start right-to-left)'), - tooltip=_('The flip option can help you with routing your stitch path. ' - 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', - _('Spacing between rows'), - tooltip=_('Distance between rows of stitches.'), - unit='mm', - sort_index=4, - type='float', - default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', - _('Maximum fill stitch length'), - tooltip=_( - 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), - unit='mm', - sort_index=4, - type='float', - default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - - @property - @param('staggers', - _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), - type='int', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=4) - def staggers(self): - return max(self.get_int_param("staggers", 4), 1) - - @property - @cache - def paths(self): - paths = self.flatten(self.parse_path()) - # ensure path length - for i, path in enumerate(paths): - if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0] + - 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] - return paths - - @property - @cache - def outline(self): - return self.shape.boundary[0] - - @property - @cache - def outline_length(self): - return self.outline.length - - @property - @param('running_stitch_length_mm', - _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), - unit='mm', - type='float', - default=1.5, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=4) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) - def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=True) - - @property - @param('fill_underlay_angle', - _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), - unit='deg', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_angle(self): - underlay_angles = self.get_param('fill_underlay_angle', None) - default_value = [self.angle + math.pi / 2.0] - if underlay_angles is not None: - underlay_angles = underlay_angles.strip().split(',') - try: - underlay_angles = [math.radians( - float(angle)) for angle in underlay_angles] - except (TypeError, ValueError): - return default_value - else: - underlay_angles = default_value - - return underlay_angles - - @property - @param('fill_underlay_row_spacing_mm', - _('Row spacing'), - tooltip=_('default: 3x fill row spacing'), - unit='mm', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_row_spacing(self): - return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 - - @property - @param('fill_underlay_max_stitch_length_mm', - _('Max stitch length'), - tooltip=_('default: equal to fill max stitch length'), - unit='mm', - group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_max_stitch_length(self): - return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length - - @property - @param('fill_underlay_inset_mm', - _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), - unit='mm', - group=_('AutoFill Underlay'), - type='float', - default=0) - def fill_underlay_inset(self): - return self.get_float_param('fill_underlay_inset_mm', 0) - - @property - @param( - 'fill_underlay_skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - group=_('AutoFill Underlay'), - type='boolean', - default=False) - def fill_underlay_skip_last(self): - return self.get_boolean_param("fill_underlay_skip_last", False) - - @property - @param('expand_mm', - _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), - unit='mm', - type='float', - default=0, - sort_index=5, - select_items=[('fill_method', 0), ('fill_method', 2)]) - def expand(self): - return self.get_float_param('expand_mm', 0) - - @property - @param('underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - type='boolean', - default=True, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=6) - def underpath(self): - return self.get_boolean_param('underpath', True) - - @property - @param( - 'underlay_underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - group=_('AutoFill Underlay'), - type='boolean', - default=True) - def underlay_underpath(self): - return self.get_boolean_param('underlay_underpath', True) - - @property - @cache - def shape(self): - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon( - point_list).area, reverse=True) - # Very small holes will cause a shape to be rendered as an outline only - # they are too small to be rendered and only confuse the auto_fill algorithm. - # So let's ignore them - if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: - paths = [path for path in paths if shgeo.Polygon(path).area > 3] - - polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) - - # There is a great number of "crossing border" errors on fill shapes - # If the polygon fails, we can try to run buffer(0) on the polygon in the - # hope it will fix at least some of them - if not self.shape_is_valid(polygon): - why = explain_validity(polygon) - message = re.match(r".+?(?=\[)", why) - if message.group(0) == "Self-intersection": - buffered = polygon.buffer(0) - # we do not want to break apart into multiple objects (possibly in the future?!) - # best way to distinguish the resulting polygon is to compare the area size of the two - # and make sure users will not experience significantly altered shapes without a warning - if math.isclose(polygon.area, buffered.area): - polygon = shgeo.MultiPolygon([buffered]) - - return polygon - - def shape_is_valid(self, shape): - # Shapely will log to stdout to complain about the shape unless we make - # it shut up. - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid = shape.is_valid - - logger.setLevel(level) - - return valid - - def shrink_or_grow_shape(self, amount, validate=False): - if amount: - shape = self.shape.buffer(amount) - # changing the size can empty the shape - # in this case we want to use the original shape rather than returning an error - if shape.is_empty and not validate: - return self.shape - if not isinstance(shape, shgeo.MultiPolygon): - shape = shgeo.MultiPolygon([shape]) - return shape - else: - return self.shape - - @property - def underlay_shape(self): - return self.shrink_or_grow_shape(-self.fill_underlay_inset) - - @property - def fill_shape(self): - return self.shrink_or_grow_shape(self.expand) - - def get_starting_point(self, last_patch): - # If there is a "fill_start" Command, then use that; otherwise pick - # the point closest to the end of the last patch. - - if self.get_command('fill_start'): - return self.get_command('fill_start').target_point - elif last_patch: - return last_patch.stitches[-1] - else: - return None - - def get_ending_point(self): - if self.get_command('fill_end'): - return self.get_command('fill_end').target_point - else: - return None - - def to_stitch_groups(self, last_patch): # noqa: C901 - # TODO: split this up do_legacy_fill() etc. - stitch_groups = [] - - starting_point = self.get_starting_point(last_patch) - ending_point = self.get_ending_point() - - try: - if self.fill_underlay: - for i in range(len(self.fill_underlay_angle)): - underlay = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_underlay"), - stitches=auto_fill( - self.underlay_shape, - None, - self.fill_underlay_angle[i], - self.fill_underlay_row_spacing, - self.fill_underlay_row_spacing, - self.fill_underlay_max_stitch_length, - self.running_stitch_length, - self.staggers, - self.fill_underlay_skip_last, - starting_point, - underpath=self.underlay_underpath)) - stitch_groups.append(underlay) - starting_point = underlay.stitches[-1] - - if self.fill_method == 0: # Auto Fill - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - None, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - self.skip_last, - starting_point, - ending_point, - self.underpath)) - stitch_groups.append(stitch_group) - elif self.fill_method == 1: # Tangential Fill - polygons = list(self.fill_shape) - if not starting_point: - starting_point = (0, 0) - for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, - self.interlaced, - self.tangential_strategy, - shgeo.Point(starting_point)) - path = [InkstitchPoint(*p) for p in connectedLine] - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) - stitch_groups.append(stitch_group) - elif self.fill_method == 2: # Guided Auto Fill - lines = get_marker_elements(self.node, "guide-line", False, True) - lines = lines['stroke'] - if not lines or lines[0].is_empty: - inkex.errormsg( - _("No line marked as guide line found within the same group as patch")) - else: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - lines[0].geoms[0], - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - 0, - self.skip_last, - starting_point, - ending_point, - self.underpath, - self.interlaced)) - stitch_groups.append(stitch_group) - elif self.fill_method == 3: # Legacy Fill - stitch_lists = fill.legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers, - self.skip_last) - for stitch_list in stitch_lists: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=stitch_list) - stitch_groups.append(stitch_group) - - except Exception: - if hasattr(sys, 'gettrace') and sys.gettrace(): - # if we're debugging, let the exception bubble up - raise - - # for an uncaught exception, give a little more info so that they can create a bug report - message = "" - message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") - message += "\n\n" - # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new - message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") - message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" - message += version.get_inkstitch_version() + "\n\n" - message += traceback.format_exc() - - self.fatal(message) - - return stitch_groups - - -def validation_warnings(self): - if self.shape.area < 20: - label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") - yield SmallShapeWarning(self.shape.centroid, label) - - if self.shrink_or_grow_shape(self.expand, True).is_empty: - yield ExpandWarning(self.shape.centroid) - - if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: - yield UnderlayInsetWarning(self.shape.centroid) - - for warning in super(AutoFill, self).validation_warnings(): - yield warning diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 15e7591c..3f133471 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,18 +5,20 @@ from math import atan, degrees +<<<<<<< HEAD from ..commands import is_command, is_command_symbol +======= +import inkex + +from ..commands import is_command_symbol +>>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements -from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, - SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF) +from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG, + XLINK_HREF) from ..utils import cache -from .auto_fill import AutoFill from .element import EmbroideryElement, param -from .polyline import Polyline -from .satin_column import SatinColumn -from .stroke import Stroke from .validation import ObjectTypeWarning, ValidationWarning @@ -67,28 +69,8 @@ class Clone(EmbroideryElement): return self.get_float_param('angle', 0) def clone_to_element(self, node): - # we need to determine if the source element is polyline, stroke, fill or satin - element = EmbroideryElement(node) - - if node.tag == SVG_POLYLINE_TAG: - return [Polyline(node)] - - elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node): - return [SatinColumn(node)] - else: - elements = [] - if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) - if element.get_style("stroke", self.node) is not None: - if not is_command(element.node): - elements.append(Stroke(node)) - if element.get_boolean_param("stroke_first", False): - elements.reverse() - - return elements + from .utils import node_to_elements + return node_to_elements(node) def to_stitch_groups(self, last_patch=None): patches = [] diff --git a/lib/elements/element.py b/lib/elements/element.py index ef70510d..ee4eadbb 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -87,8 +87,11 @@ class EmbroideryElement(object): return params def replace_legacy_param(self, param): - value = self.node.get(param, "").strip() - self.set_param(param[10:], value) + # remove "embroider_" prefix + new_param = param[10:] + if new_param in INKSTITCH_ATTRIBS: + value = self.node.get(param, "").strip() + self.set_param(param[10:], value) del self.node.attrib[param] @cache @@ -266,6 +269,11 @@ class EmbroideryElement(object): def parse_path(self): return apply_transforms(self.path, self.node) + @property + @cache + def paths(self): + return self.flatten(self.parse_path()) + @property def shape(self): raise NotImplementedError( diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py new file mode 100644 index 00000000..ee56abfc --- /dev/null +++ b/lib/elements/fill_stitch.py @@ -0,0 +1,624 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import logging +import math +import re +import sys +import traceback + +from shapely import geometry as shgeo +from shapely.validation import explain_validity + +from ..i18n import _ +from ..marker import get_marker_elements +from ..stitch_plan import StitchGroup +from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..svg import PIXELS_PER_MM +from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationError, ValidationWarning +from shapely.ops import nearest_points + + +class SmallShapeWarning(ValidationWarning): + name = _("Small Fill") + description = _("This fill object is so small that it would probably look better as running stitch or satin column. " + "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " + "the outline instead.") + + +class ExpandWarning(ValidationWarning): + name = _("Expand") + description = _("The expand parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use original size instead.") + + +class UnderlayInsetWarning(ValidationWarning): + name = _("Inset") + description = _("The underlay inset parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use the original size instead.") + +class MissingGuideLineWarning(ValidationWarning): + name = _("Missing Guideline") + description = _('This object is set to "Guided AutoFill", but has no guide line.') + steps_to_solve = [ + _('* Create a stroke object'), + _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') + ] + +class DisjointGuideLineWarning(ValidationWarning): + name = _("Disjointed Guide Line") + description = _("The guide line of this object isn't within the object borders. " + "The guide line works best, if it is within the target element.") + steps_to_solve = [ + _('* Move the guide line into the element') + ] + +class MultipleGuideLineWarning(ValidationWarning): + name = _("Multiple Guide Lines") + description = _("This object has multiple guide lines, but only the first one will be used.") + steps_to_solve = [ + _("* Remove all guide lines, except for one.") + ] + +class UnconnectedError(ValidationError): + name = _("Unconnected") + description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " + "Ink/Stitch doesn't know what order to stitch them in. Please break this " + "object up into separate shapes.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'), + ] + + +class InvalidShapeError(ValidationError): + name = _("Border crosses itself") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') + ] + + +class FillStitch(EmbroideryElement): + element_name = _("FillStitch") + + @property + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) + def fill_method(self): + return self.get_int_param('fill_method', 0) + + @property + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + def tangential_strategy(self): + return self.get_int_param('tangential_strategy', 1) + + @property + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) + def join_style(self): + return self.get_int_param('join_style', 0) + + @property + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + def interlaced(self): + return self.get_boolean_param('interlaced', True) + + @property + @param('angle', + _('Angle of lines of stitches'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + unit='deg', + type='float', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") + + @property + @param( + 'skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def skip_last(self): + return self.get_boolean_param("skip_last", False) + + @property + @param( + 'flip', + _('Flip fill (start right-to-left)'), + tooltip=_('The flip option can help you with routing your stitch path. ' + 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', + _('Spacing between rows'), + tooltip=_('Distance between rows of stitches.'), + unit='mm', + sort_index=4, + type='float', + default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', + _('Maximum fill stitch length'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index=4, + type='float', + default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', + _('Stagger rows this many times before repeating'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + type='int', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=4) + def staggers(self): + return max(self.get_int_param("staggers", 4), 1) + + @property + @cache + def paths(self): + paths = self.flatten(self.parse_path()) + # ensure path length + for i, path in enumerate(paths): + if len(path) < 3: + paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + return paths + + @property + @cache + def shape(self): + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) + + # There is a great number of "crossing border" errors on fill shapes + # If the polygon fails, we can try to run buffer(0) on the polygon in the + # hope it will fix at least some of them + if not self.shape_is_valid(polygon): + why = explain_validity(polygon) + message = re.match(r".+?(?=\[)", why) + if message.group(0) == "Self-intersection": + buffered = polygon.buffer(0) + # if we receive a multipolygon, only use the first one of it + if type(buffered) == shgeo.MultiPolygon: + buffered = buffered[0] + # we do not want to break apart into multiple objects (possibly in the future?!) + # best way to distinguish the resulting polygon is to compare the area size of the two + # and make sure users will not experience significantly altered shapes without a warning + if type(buffered) == shgeo.Polygon and math.isclose(polygon.area, buffered.area, abs_tol=0.5): + polygon = shgeo.MultiPolygon([buffered]) + + return polygon + + def shape_is_valid(self, shape): + # Shapely will log to stdout to complain about the shape unless we make + # it shut up. + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid = shape.is_valid + + logger.setLevel(level) + + return valid + + def validation_errors(self): + if not self.shape_is_valid(self.shape): + why = explain_validity(self.shape) + message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) + + # I Wish this weren't so brittle... + if "Hole lies outside shell" in message: + yield UnconnectedError((x, y)) + else: + yield InvalidShapeError((x, y)) + + def validation_warnings(self): + if self.shape.area < 20: + label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") + yield SmallShapeWarning(self.shape.centroid, label) + + if self.shrink_or_grow_shape(self.expand, True).is_empty: + yield ExpandWarning(self.shape.centroid) + + if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: + yield UnderlayInsetWarning(self.shape.centroid) + + # guided fill warnings + if self.fill_method == 2: + guide_lines = self._get_guide_lines(True) + if not guide_lines or guide_lines[0].is_empty: + yield MissingGuideLineWarning(self.shape.centroid) + elif len(guide_lines) > 1: + yield MultipleGuideLineWarning(self.shape.centroid) + elif guide_lines[0].disjoint(self.shape): + yield DisjointGuideLineWarning(self.shape.centroid) + return None + + for warning in super(FillStitch, self).validation_warnings(): + yield warning + + @property + @cache + def outline(self): + return self.shape.boundary[0] + + @property + @cache + def outline_length(self): + return self.outline.length + + @property + @param('running_stitch_length_mm', + _('Running stitch length (traversal between sections)'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), + unit='mm', + type='float', + default=1.5, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) + def fill_underlay(self): + return self.get_boolean_param("fill_underlay", default=True) + + @property + @param('fill_underlay_angle', + _('Fill angle'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + unit='deg', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_angle(self): + underlay_angles = self.get_param('fill_underlay_angle', None) + default_value = [self.angle + math.pi / 2.0] + if underlay_angles is not None: + underlay_angles = underlay_angles.strip().split(',') + try: + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] + except (TypeError, ValueError): + return default_value + else: + underlay_angles = default_value + + return underlay_angles + + @property + @param('fill_underlay_row_spacing_mm', + _('Row spacing'), + tooltip=_('default: 3x fill row spacing'), + unit='mm', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_row_spacing(self): + return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 + + @property + @param('fill_underlay_max_stitch_length_mm', + _('Max stitch length'), + tooltip=_('default: equal to fill max stitch length'), + unit='mm', + group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_max_stitch_length(self): + return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length + + @property + @param('fill_underlay_inset_mm', + _('Inset'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + unit='mm', + group=_('AutoFill Underlay'), + type='float', + default=0) + def fill_underlay_inset(self): + return self.get_float_param('fill_underlay_inset_mm', 0) + + @property + @param( + 'fill_underlay_skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + group=_('AutoFill Underlay'), + type='boolean', + default=False) + def fill_underlay_skip_last(self): + return self.get_boolean_param("fill_underlay_skip_last", False) + + @property + @param('expand_mm', + _('Expand'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + unit='mm', + type='float', + default=0, + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) + def expand(self): + return self.get_float_param('expand_mm', 0) + + @property + @param('underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + type='boolean', + default=True, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) + def underpath(self): + return self.get_boolean_param('underpath', True) + + @property + @param( + 'underlay_underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + group=_('AutoFill Underlay'), + type='boolean', + default=True) + def underlay_underpath(self): + return self.get_boolean_param('underlay_underpath', True) + + def shrink_or_grow_shape(self, amount, validate=False): + if amount: + shape = self.shape.buffer(amount) + # changing the size can empty the shape + # in this case we want to use the original shape rather than returning an error + if shape.is_empty and not validate: + return self.shape + if not isinstance(shape, shgeo.MultiPolygon): + shape = shgeo.MultiPolygon([shape]) + return shape + else: + return self.shape + + @property + def underlay_shape(self): + return self.shrink_or_grow_shape(-self.fill_underlay_inset) + + @property + def fill_shape(self): + return self.shrink_or_grow_shape(self.expand) + + def get_starting_point(self, last_patch): + # If there is a "fill_start" Command, then use that; otherwise pick + # the point closest to the end of the last patch. + + if self.get_command('fill_start'): + return self.get_command('fill_start').target_point + elif last_patch: + return last_patch.stitches[-1] + else: + return None + + def get_ending_point(self): + if self.get_command('fill_end'): + return self.get_command('fill_end').target_point + else: + return None + + def to_stitch_groups(self, last_patch): + # backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False + if not self.auto_fill or self.fill_method == 3: + return self.do_legacy_fill() + else: + stitch_groups = [] + start = self.get_starting_point(last_patch) + end = self.get_ending_point() + + try: + if self.fill_underlay: + underlay_stitch_groups, start = self.do_underlay(start) + stitch_groups.extend(underlay_stitch_groups) + if self.fill_method == 0: + stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) + if self.fill_method == 1: + stitch_groups.extend(self.do_tangential_fill(last_patch, start)) + elif self.fill_method == 2: + stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) + except Exception: + self.fatal_fill_error() + + return stitch_groups + + def do_legacy_fill(self): + stitch_lists = legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers, + self.skip_last) + return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] + + def do_underlay(self, starting_point): + stitch_groups = [] + for i in range(len(self.fill_underlay_angle)): + underlay = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_underlay"), + stitches=auto_fill( + self.underlay_shape, + None, + self.fill_underlay_angle[i], + self.fill_underlay_row_spacing, + self.fill_underlay_row_spacing, + self.fill_underlay_max_stitch_length, + self.running_stitch_length, + self.staggers, + self.fill_underlay_skip_last, + starting_point, + underpath=self.underlay_underpath)) + stitch_groups.append(underlay) + + starting_point = underlay.stitches[-1] + return [stitch_groups, starting_point] + + def do_auto_fill(self, last_patch, starting_point, ending_point): + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + None, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + self.skip_last, + starting_point, + ending_point, + self.underpath)) + return [stitch_group] + + def do_tangential_fill(self, last_patch, starting_point): + stitch_groups = [] + polygons = list(self.fill_shape) + if not starting_point: + starting_point = (0, 0) + for poly in polygons: + connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, + self.interlaced, + self.tangential_strategy, + shgeo.Point(starting_point)) + path = [InkstitchPoint(*p) for p in connectedLine] + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) + stitch_groups.append(stitch_group) + + return stitch_groups + + def do_guided_fill(self, last_patch, starting_point, ending_point): + guide_line = self._get_guide_lines() + + # No guide line: fallback to normal autofill + if not guide_line: + return self.do_auto_fill(last_patch, starting_point, ending_point) + + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + guide_line.geoms[0], + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + 0, + self.skip_last, + starting_point, + ending_point, + self.underpath, + self.interlaced)) + return [stitch_group] + + @cache + def _get_guide_lines(self, multiple=False): + guide_lines = get_marker_elements(self.node, "guide-line", False, True) + # No or empty guide line + if not guide_lines or guide_lines['stroke'][0].is_empty: + return None + if multiple: + return guide_lines['stroke'] + else: + return guide_lines['stroke'][0] + + def fatal_fill_error(self): + if hasattr(sys, 'gettrace') and sys.gettrace(): + # if we're debugging, let the exception bubble up + raise + + # for an uncaught exception, give a little more info so that they can create a bug report + message = "" + message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") + message += "\n\n" + # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new + message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") + message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" + message += version.get_inkstitch_version() + "\n\n" + message += traceback.format_exc() + + self.fatal(message) diff --git a/lib/elements/marker.py b/lib/elements/marker.py new file mode 100644 index 00000000..574ce91e --- /dev/null +++ b/lib/elements/marker.py @@ -0,0 +1,32 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex + +from ..i18n import _ +from .element import EmbroideryElement +from .validation import ObjectTypeWarning + + +class MarkerWarning(ObjectTypeWarning): + name = _("Marker Element") + description = _("This element will not be embroidered. " + "It will be applied to objects in the same group. Objects in sub-groups will be ignored.") + steps_to_solve = [ + _("Turn back to normal embroidery element mode, remove the marker:"), + _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), + _('* Go to the Stroke style tab'), + _('* Under "Markers" choose the first (empty) option in the first dropdown list.') + ] + + +class MarkerObject(EmbroideryElement): + + def validation_warnings(self): + repr_point = next(inkex.Path(self.parse_path()).end_points) + yield MarkerWarning(repr_point) + + def to_stitch_groups(self, last_patch): + return [] diff --git a/lib/elements/pattern.py b/lib/elements/pattern.py deleted file mode 100644 index 4b92d366..00000000 --- a/lib/elements/pattern.py +++ /dev/null @@ -1,33 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import inkex - -from ..i18n import _ -from .element import EmbroideryElement -from .validation import ObjectTypeWarning - - -class PatternWarning(ObjectTypeWarning): - name = _("Pattern Element") - description = _("This element will not be embroidered. " - "It will appear as a pattern applied to objects in the same group as it. " - "Objects in sub-groups will be ignored.") - steps_to_solve = [ - _("To disable pattern mode, remove the pattern marker:"), - _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), - _('* Go to the Stroke style tab'), - _('* Under "Markers" choose the first (empty) option in the first dropdown list.') - ] - - -class PatternObject(EmbroideryElement): - - def validation_warnings(self): - repr_point = next(inkex.Path(self.parse_path()).end_points) - yield PatternWarning(repr_point) - - def to_stitch_groups(self, last_patch): - return [] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9b9b8f14..561188aa 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -7,12 +7,12 @@ from ..commands import is_command from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) -from .auto_fill import AutoFill +from .fill_stitch import FillStitch from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject from .image import ImageObject -from .pattern import PatternObject +from .marker import MarkerObject from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -29,8 +29,8 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif has_marker(node, 'pattern'): - return [PatternObject(node)] + elif has_marker(node): + return [MarkerObject(node)] elif node.tag in EMBROIDERABLE_TAGS: element = EmbroideryElement(node) @@ -40,10 +40,7 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) + elements.append(FillStitch(node)) if element.get_style("stroke"): if not is_command(element.node): elements.append(Stroke(node)) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index cf846324..949f947e 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -161,10 +161,10 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node, 'pattern'): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node): nodes.append(node) - # add images, text and patterns for the troubleshoot extension - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node, 'pattern')): + # add images, text and elements with a marker for the troubleshoot extension + elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node)): nodes.append(node) return nodes diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index ae95041b..4c350d62 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, errormsg -from ..elements import AutoFill, Stroke +from ..elements import FillStitch, Stroke from ..i18n import _ from .base import InkstitchExtension @@ -38,7 +38,7 @@ class Cleanup(InkstitchExtension): return for element in self.elements: - if (isinstance(element, AutoFill) and self.rm_fill and element.shape.area < self.fill_threshold): + if (isinstance(element, FillStitch) and self.rm_fill and element.shape.area < self.fill_threshold): element.node.getparent().remove(element.node) count += 1 if (isinstance(element, Stroke) and self.rm_stroke and diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 55963625..69a559ce 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -15,7 +15,7 @@ import wx from wx.lib.scrolledpanel import ScrolledPanel from ..commands import is_command, is_command_symbol -from ..elements import (AutoFill, Clone, EmbroideryElement, Polyline, +from ..elements import (FillStitch, Clone, EmbroideryElement, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..gui import PresetsPanel, SimulatorPreview, WarningPanel @@ -606,8 +606,7 @@ class Params(InkstitchExtension): classes.append(Clone) else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": - classes.append(AutoFill) - # classes.append(Fill) + classes.append(FillStitch) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: diff --git a/lib/marker.py b/lib/marker.py index 3c145145..56a43c3b 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -69,8 +69,11 @@ def get_marker_elements(node, marker, get_fills=True, get_strokes=True): return {'fill': fills, 'stroke': strokes} -def has_marker(node, marker): - if node.tag not in EMBROIDERABLE_TAGS: - return False - style = node.get('style') or '' - return "marker-start:url(#inkstitch-%s-marker)" % marker in style +def has_marker(node, marker=list()): + if not marker: + marker = MARKER + for m in marker: + style = node.get('style') or '' + if "marker-start:url(#inkstitch-%s-marker)" % m in style: + return True + return False diff --git a/symbols/marker.svg b/symbols/marker.svg index 28f4f44d..45219073 100644 --- a/symbols/marker.svg +++ b/symbols/marker.svg @@ -34,21 +34,21 @@ - - - - - + refX="10" + refY="5" + orient="auto" + id="inkstitch-guide-line-marker"> + + + + + -- cgit v1.3.1 From b14e445daeafd12984cb40af289a415a0cb90e5d Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 1 Feb 2022 19:47:19 +0100 Subject: small bug fix --- lib/elements/fill_stitch.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index ee56abfc..2e67c517 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -43,14 +43,17 @@ class UnderlayInsetWarning(ValidationWarning): description = _("The underlay inset parameter for this fill object cannot be applied. " "Ink/Stitch will ignore it and will use the original size instead.") + class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _('This object is set to "Guided AutoFill", but has no guide line.') + description = _( + 'This object is set to "Guided AutoFill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') ] + class DisjointGuideLineWarning(ValidationWarning): name = _("Disjointed Guide Line") description = _("The guide line of this object isn't within the object borders. " @@ -59,13 +62,16 @@ class DisjointGuideLineWarning(ValidationWarning): _('* Move the guide line into the element') ] + class MultipleGuideLineWarning(ValidationWarning): name = _("Multiple Guide Lines") - description = _("This object has multiple guide lines, but only the first one will be used.") + description = _( + "This object has multiple guide lines, but only the first one will be used.") steps_to_solve = [ _("* Remove all guide lines, except for one.") ] + class UnconnectedError(ValidationError): name = _("Unconnected") description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " @@ -78,7 +84,8 @@ class UnconnectedError(ValidationError): class InvalidShapeError(ValidationError): name = _("Border crosses itself") - description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") + description = _( + "Fill: Shape is not valid. This can happen if the border crosses over itself.") steps_to_solve = [ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') ] @@ -208,7 +215,8 @@ class FillStitch(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 @@ -218,7 +226,8 @@ class FillStitch(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 @@ -483,11 +492,14 @@ class FillStitch(EmbroideryElement): underlay_stitch_groups, start = self.do_underlay(start) stitch_groups.extend(underlay_stitch_groups) if self.fill_method == 0: - stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) + stitch_groups.extend( + self.do_auto_fill(last_patch, start, end)) if self.fill_method == 1: - stitch_groups.extend(self.do_tangential_fill(last_patch, start)) + stitch_groups.extend( + self.do_tangential_fill(last_patch, start)) elif self.fill_method == 2: - stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) + stitch_groups.extend( + self.do_guided_fill(last_patch, start, end)) except Exception: self.fatal_fill_error() @@ -599,8 +611,9 @@ class FillStitch(EmbroideryElement): def _get_guide_lines(self, multiple=False): guide_lines = get_marker_elements(self.node, "guide-line", False, True) # No or empty guide line - if not guide_lines or guide_lines['stroke'][0].is_empty: + if not guide_lines or not guide_lines['stroke']: return None + if multiple: return guide_lines['stroke'] else: -- cgit v1.3.1 From d514eac81937bb64815239dd3aa96e38d6556a32 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 2 Feb 2022 21:19:31 +0100 Subject: adjusting namings --- lib/elements/fill_stitch.py | 32 +- lib/extensions/params.py | 4 - lib/stitches/ConnectAndSamplePattern.py | 949 --------------------- lib/stitches/DebuggingMethods.py | 173 ---- lib/stitches/LineStringSampling.py | 354 -------- lib/stitches/PointTransfer.py | 503 ----------- lib/stitches/StitchPattern.py | 420 --------- lib/stitches/auto_fill.py | 14 +- lib/stitches/constants.py | 10 - lib/stitches/fill.py | 8 +- lib/stitches/point_transfer.py | 495 +++++++++++ lib/stitches/sample_linestring.py | 325 +++++++ .../tangential_fill_stitch_line_creator.py | 330 +++++++ .../tangential_fill_stitch_pattern_creator.py | 906 ++++++++++++++++++++ 14 files changed, 2072 insertions(+), 2451 deletions(-) delete mode 100644 lib/stitches/ConnectAndSamplePattern.py delete mode 100644 lib/stitches/DebuggingMethods.py delete mode 100644 lib/stitches/LineStringSampling.py delete mode 100644 lib/stitches/PointTransfer.py delete mode 100644 lib/stitches/StitchPattern.py create mode 100644 lib/stitches/point_transfer.py create mode 100644 lib/stitches/sample_linestring.py create mode 100644 lib/stitches/tangential_fill_stitch_line_creator.py create mode 100644 lib/stitches/tangential_fill_stitch_pattern_creator.py (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 2e67c517..3256c1ea 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,14 +15,13 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import Point as InkstitchPoint from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning -from shapely.ops import nearest_points class SmallShapeWarning(ValidationWarning): @@ -46,8 +45,7 @@ class UnderlayInsetWarning(ValidationWarning): class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _( - 'This object is set to "Guided AutoFill", but has no guide line.') + description = _('This object is set to "Guided AutoFill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') @@ -65,8 +63,7 @@ class DisjointGuideLineWarning(ValidationWarning): class MultipleGuideLineWarning(ValidationWarning): name = _("Multiple Guide Lines") - description = _( - "This object has multiple guide lines, but only the first one will be used.") + description = _("This object has multiple guide lines, but only the first one will be used.") steps_to_solve = [ _("* Remove all guide lines, except for one.") ] @@ -84,8 +81,7 @@ class UnconnectedError(ValidationError): class InvalidShapeError(ValidationError): name = _("Border crosses itself") - description = _( - "Fill: Shape is not valid. This can happen if the border crosses over itself.") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") steps_to_solve = [ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') ] @@ -125,8 +121,7 @@ class FillStitch(EmbroideryElement): @property @param('angle', _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', sort_index=4, @@ -199,8 +194,7 @@ class FillStitch(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', sort_index=4, select_items=[('fill_method', 0), ('fill_method', 3)], @@ -317,8 +311,7 @@ class FillStitch(EmbroideryElement): @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, @@ -335,8 +328,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -380,8 +372,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -404,8 +395,7 @@ class FillStitch(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, @@ -564,7 +554,7 @@ class FillStitch(EmbroideryElement): if not starting_point: starting_point = (0, 0) for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( poly, -self.row_spacing, self.join_style+1, diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 69a559ce..e50d97d0 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -86,7 +86,6 @@ class ParamsTab(ScrolledPanel): # end wxGlade def pair(self, tab): - # print self.name, "paired with", tab.name self.paired_tab = tab self.update_description() @@ -108,7 +107,6 @@ class ParamsTab(ScrolledPanel): def update_toggle_state(self, event=None, notify_pair=True): enable = self.enabled() - # print self.name, "update_toggle_state", enable for child in self.settings_grid.GetChildren(): widget = child.GetWindow() if widget: @@ -137,7 +135,6 @@ class ParamsTab(ScrolledPanel): event.Skip() def pair_changed(self, value): - # print self.name, "pair_changed", value new_value = not value if self.enabled() != new_value: @@ -192,7 +189,6 @@ class ParamsTab(ScrolledPanel): def apply(self): values = self.get_values() for node in self.nodes: - # print >> sys.stderr, "apply: ", self.name, node.id, values for name, value in values.items(): node.set_param(name, value) diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py deleted file mode 100644 index 1cf2b2a1..00000000 --- a/lib/stitches/ConnectAndSamplePattern.py +++ /dev/null @@ -1,949 +0,0 @@ -from shapely.geometry.polygon import LineString, LinearRing -from shapely.geometry import Point, MultiPoint -from shapely.ops import nearest_points -from collections import namedtuple -from depq import DEPQ -import trimesh -import numpy as np -from scipy import spatial -import math -from anytree import PreOrderIter -from ..stitches import LineStringSampling -from ..stitches import PointTransfer -from ..stitches import constants - -nearest_neighbor_tuple = namedtuple( - "nearest_neighbor_tuple", - [ - "nearest_point_parent", - "nearest_point_child", - "proj_distance_parent", - "child_node", - ], -) - - -def cut(line, distance): - """ - Cuts a closed line so that the new closed line starts at the - point with "distance" to the beginning of the old line. - """ - if distance <= 0.0 or distance >= line.length: - return [LineString(line)] - coords = list(line.coords) - for i, p in enumerate(coords): - if i > 0 and p == coords[0]: - pd = line.length - else: - pd = line.project(Point(p)) - if pd == distance: - if coords[0] == coords[-1]: - return LineString(coords[i:] + coords[1: i + 1]) - else: - return LineString(coords[i:] + coords[:i]) - if pd > distance: - cp = line.interpolate(distance) - if coords[0] == coords[-1]: - return LineString( - [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] - ) - else: - return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) - - -def connect_raster_tree_nearest_neighbor( # noqa: C901 - tree, used_offset, stitch_distance, close_point, offset_by_half): - """ - Takes the offsetted curves organized as tree, connects and samples them. - Strategy: A connection from parent to child is made where both curves - come closest together. - Input: - -tree: contains the offsetted curves in a hierachical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one line and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - current_coords = tree.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - # We cut the current item so that its index 0 is closest to close_point - start_distance = tree.val.project(close_point) - if start_distance > 0: - current_coords = cut(current_coords, start_distance) - tree.val = current_coords - - if not tree.transferred_point_priority_deque.is_empty(): - new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert( - item, - math.fmod( - priority - start_distance + current_coords.length, - current_coords.length, - ), - ) - tree.transferred_point_priority_deque = new_DEPQ - - stitching_direction = 1 - # This list should contain a tuple of nearest points between - # the current geometry and the subgeometry, the projected - # distance along the current geometry, and the belonging subtree node - nearest_points_list = [] - - for subnode in tree.children: - point_parent, point_child = nearest_points(current_coords, subnode.val) - proj_distance = current_coords.project(point_parent) - nearest_points_list.append( - nearest_neighbor_tuple( - nearest_point_parent=point_parent, - nearest_point_child=point_child, - proj_distance_parent=proj_distance, - child_node=subnode, - ) - ) - nearest_points_list.sort( - reverse=False, key=lambda tup: tup.proj_distance_parent) - - if nearest_points_list: - start_distance = min( - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[0].proj_distance_parent, - ) - end_distance = max( - current_coords.length - - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[-1].proj_distance_parent, - ) - else: - start_distance = abs_offset * constants.factor_offset_starting_points - end_distance = ( - current_coords.length - abs_offset * constants.factor_offset_starting_points - ) - - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - start_distance, # We add/subtract an offset to not sample - # the same point again (avoid double - # points for start and end) - end_distance, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - assert len(own_coords) == len(own_coords_origin) - own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT - tree.stitching_direction = stitching_direction - tree.already_rastered = True - - # Next we need to transfer our rastered points to siblings and childs - to_transfer_point_list = [] - to_transfer_point_list_origin = [] - for k in range(1, len(own_coords) - 1): - # Do not take the first and the last since they are ENTER_LEAVING_POINT - # points for sure - - if ( - not offset_by_half - and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED - ): - continue - if ( - own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT - or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT - ): - continue - to_transfer_point_list.append(Point(own_coords[k])) - point_origin = own_coords_origin[k] - to_transfer_point_list_origin.append(point_origin) - - # Since the projection is only in ccw direction towards inner we need - # to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - offset_by_half, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - False, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - if not nearest_points_list: - # If there is no child (inner geometry) we can simply take - # our own rastered coords as result - result_coords = own_coords - result_coords_origin = own_coords_origin - else: - # There are childs so we need to merge their coordinates + - # with our own rastered coords - - # To create a closed ring - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) - - # own_coords does not start with current_coords but has an offset - # (see call of raster_line_string_with_priority_points) - total_distance = start_distance - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [ - LineStringSampling.PointSource.ENTER_LEAVING_POINT] - for i in range(1, len(own_coords)): - next_distance = math.sqrt( - (own_coords[i][0] - own_coords[i - 1][0]) ** 2 - + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 - ) - while ( - cur_item < len(nearest_points_list) - and total_distance + next_distance + constants.eps - > nearest_points_list[cur_item].proj_distance_parent - ): - - item = nearest_points_list[cur_item] - ( - child_coords, - child_coords_origin, - ) = connect_raster_tree_nearest_neighbor( - item.child_node, - used_offset, - stitch_distance, - item.nearest_point_child, - offset_by_half, - ) - - d = item.nearest_point_parent.distance( - Point(own_coords[i - 1])) - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - # reversing avoids crossing when entering and - # leaving the child segment - result_coords.extend(child_coords[::-1]) - result_coords_origin.extend(child_coords_origin[::-1]) - - # And here we calculate the point for the leaving - d = item.nearest_point_parent.distance(Point(own_coords[i])) - if cur_item < len(nearest_points_list) - 1: - d = min( - d, - abs( - nearest_points_list[cur_item + - 1].proj_distance_parent - - item.proj_distance_parent - ), - ) - - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append( - current_coords.interpolate( - item.proj_distance_parent - + abs_offset * constants.factor_offset_starting_points - ).coords[0] - ) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - - cur_item += 1 - if i < len(own_coords) - 1: - if ( - Point(result_coords[-1]).distance(Point(own_coords[i])) - > abs_offset * constants.factor_offset_remove_points - ): - result_coords.append(own_coords[i]) - result_coords_origin.append(own_coords_origin[i]) - - # Since current_coords and temp are rastered differently - # there accumulate errors regarding the current distance. - # Since a projection of each point in temp would be very time - # consuming we project only every n-th point which resets - # the accumulated error every n-th point. - if i % 20 == 0: - total_distance = current_coords.project(Point(own_coords[i])) - else: - total_distance += next_distance - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin - - -def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): - """ - Takes a line and calculates the nearest distance along this - line to enter the next_line - Input: - -travel_line: The "parent" line for which the distance should - be minimized to enter next_line - -next_line: contains the next_line which need to be entered - -thresh: The distance between travel_line and next_line needs - to below thresh to be a valid point for entering - Output: - -tuple - the tuple structure is: - (nearest point in travel_line, nearest point in next_line) - """ - point_list = list(MultiPoint(travel_line.coords)) - - if point_list[0].distance(next_line) < thresh: - return nearest_points(point_list[0], next_line) - - for i in range(len(point_list) - 1): - line_segment = LineString([point_list[i], point_list[i + 1]]) - result = nearest_points(line_segment, next_line) - - if result[0].distance(result[1]) < thresh: - return result - line_segment = LineString([point_list[-1], point_list[0]]) - result = nearest_points(line_segment, next_line) - - if result[0].distance(result[1]) < thresh: - return result - else: - return None - - -def create_nearest_points_list( - travel_line, children_list, threshold, threshold_hard, preferred_direction=0): - """ - Takes a line and calculates the nearest distance along this line to - enter the childs in children_list - The method calculates the distances along the line and along the - reversed line to find the best direction which minimizes the overall - distance for all childs. - Input: - -travel_line: The "parent" line for which the distance should - be minimized to enter the childs - -children_list: contains the childs of travel_line which need to be entered - -threshold: The distance between travel_line and a child needs to be - below threshold to be a valid point for entering - -preferred_direction: Put a bias on the desired travel direction along - travel_line. If equals zero no bias is applied. - preferred_direction=1 means we prefer the direction of travel_line; - preferred_direction=-1 means we prefer the opposite direction. - Output: - -stitching direction for travel_line - -list of tuples (one tuple per child). The tuple structure is: - ((nearest point in travel_line, nearest point in child), - distance along travel_line, belonging child) - """ - - result_list_in_order = [] - result_list_reversed_order = [] - - travel_line_reversed = LinearRing(travel_line.coords[::-1]) - - weight_in_order = 0 - weight_reversed_order = 0 - for child in children_list: - result = get_nearest_points_closer_than_thresh( - travel_line, child.val, threshold - ) - if result is None: - # where holes meet outer borders a distance - # up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh( - travel_line, child.val, threshold_hard - ) - assert result is not None - proj = travel_line.project(result[0]) - weight_in_order += proj - result_list_in_order.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - result = get_nearest_points_closer_than_thresh( - travel_line_reversed, child.val, threshold - ) - if result is None: - # where holes meet outer borders a distance - # up to 2*used offset can arise - result = get_nearest_points_closer_than_thresh( - travel_line_reversed, child.val, threshold_hard - ) - assert result is not None - proj = travel_line_reversed.project(result[0]) - weight_reversed_order += proj - result_list_reversed_order.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - if preferred_direction == 1: - # Reduce weight_in_order to make in order stitching more preferred - weight_in_order = min( - weight_in_order / 2, max(0, weight_in_order - 10 * threshold) - ) - if weight_in_order == weight_reversed_order: - return (1, result_list_in_order) - elif preferred_direction == -1: - # Reduce weight_reversed_order to make reversed - # stitching more preferred - weight_reversed_order = min( - weight_reversed_order / - 2, max(0, weight_reversed_order - 10 * threshold) - ) - if weight_in_order == weight_reversed_order: - return (-1, result_list_reversed_order) - - if weight_in_order < weight_reversed_order: - return (1, result_list_in_order) - else: - return (-1, result_list_reversed_order) - - -def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): - """ - Takes a line segment (consisting of 3 points!) - and calculates a new middle point if the line_segment is - straight enough to be resampled by points max_stitch_distance apart FROM THE END OF line_segment. - Returns None if the middle point is not needed. - """ - angles = LineStringSampling.calculate_line_angles(line_segment) - if angles[1] < abs_offset * constants.limiting_angle_straight: - if line_segment.length < max_stitch_distance: - return None - else: - return line_segment.interpolate( - line_segment.length - max_stitch_distance - ).coords[0] - else: - return line_segment.coords[1] - - -def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 - """ - Takes the offsetted curves organized as tree, connects and samples them. - Strategy: A connection from parent to child is made as fast as possible to - reach the innermost child as fast as possible in order to stitch afterwards - from inner to outer. - Input: - -tree: contains the offsetted curves in a hierachical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one line and sampled with points obeying - stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was placed - at this position - """ - - current_coords = tree.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - start_distance = tree.val.project(close_point) - # We cut the current path so that its index 0 is closest to close_point - if start_distance > 0: - current_coords = cut(current_coords, start_distance) - tree.val = current_coords - - if not tree.transferred_point_priority_deque.is_empty(): - new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: - new_DEPQ.insert( - item, - math.fmod( - priority - start_distance + current_coords.length, - current_coords.length, - ), - ) - tree.transferred_point_priority_deque = new_DEPQ - - # We try to use always the opposite stitching direction with respect to the - # parent to avoid crossings when entering and leaving the child - parent_stitching_direction = -1 - if tree.parent is not None: - parent_stitching_direction = tree.parent.stitching_direction - - # Find the nearest point in current_coords and its children and - # sort it along the stitching direction - stitching_direction, nearest_points_list = create_nearest_points_list( - current_coords, - tree.children, - 1.5 * abs_offset, - 2.05 * abs_offset, - parent_stitching_direction, - ) - nearest_points_list.sort( - reverse=False, key=lambda tup: tup.proj_distance_parent) - - # Have a small offset for the starting and ending to avoid double points - # at start and end point (since the paths are closed rings) - if nearest_points_list: - start_offset = min( - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[0].proj_distance_parent, - ) - end_offset = max( - current_coords.length - - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[-1].proj_distance_parent, - ) - else: - start_offset = abs_offset * constants.factor_offset_starting_points - end_offset = ( - current_coords.length - abs_offset * constants.factor_offset_starting_points - ) - - if stitching_direction == 1: - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - start_offset, # We add start_offset to not sample the same - # point again (avoid double points for start - # and end) - end_offset, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - else: - ( - own_coords, - own_coords_origin, - ) = LineStringSampling.raster_line_string_with_priority_points( - current_coords, - current_coords.length - start_offset, # We subtract - # start_offset to not - # sample the same point - # again (avoid double - # points for start - # and end) - current_coords.length - end_offset, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False - ) - current_coords.coords = current_coords.coords[::-1] - - assert len(own_coords) == len(own_coords_origin) - - tree.stitching_direction = stitching_direction - tree.already_rastered = True - - to_transfer_point_list = [] - to_transfer_point_list_origin = [] - for k in range(0, len(own_coords)): - # TODO: maybe do not take the first and the last - # since they are ENTER_LEAVING_POINT points for sure - if ( - not offset_by_half - and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED - or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT - ): - continue - if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT: - continue - to_transfer_point_list.append(Point(own_coords[k])) - to_transfer_point_list_origin.append(own_coords_origin[k]) - - assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) - - # Next we need to transfer our rastered points to siblings and childs - # Since the projection is only in ccw direction towards inner we - # need to use "-used_offset" for stitching_direction==-1 - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - offset_by_half, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - tree, - stitching_direction * used_offset, - False, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - if not nearest_points_list: - # If there is no child (inner geometry) we can simply - # take our own rastered coords as result - result_coords = own_coords - result_coords_origin = own_coords_origin - else: - # There are childs so we need to merge their coordinates - # with our own rastered coords - - # Create a closed ring for the following code - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) - - # own_coords does not start with current_coords but has an offset - # (see call of raster_line_string_with_priority_points) - total_distance = start_offset - - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [own_coords_origin[0]] - - for i in range(1, len(own_coords)): - next_distance = math.sqrt( - (own_coords[i][0] - own_coords[i - 1][0]) ** 2 - + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 - ) - while ( - cur_item < len(nearest_points_list) - and total_distance + next_distance + constants.eps - > nearest_points_list[cur_item].proj_distance_parent - ): - # The current and the next point in own_coords enclose the - # nearest point tuple between this geometry and child - # geometry. Hence we need to insert the child geometry points - # here before the next point of own_coords. - item = nearest_points_list[cur_item] - ( - child_coords, - child_coords_origin, - ) = connect_raster_tree_from_inner_to_outer( - item.child_node, - used_offset, - stitch_distance, - item.nearest_point_child, - offset_by_half, - ) - - # Imagine the nearest point of the child is within a long - # segment of the parent. Without additonal points - # on the parent side this would cause noticeable deviations. - # Hence we add here points shortly before and after - # the entering of the child to have only minor deviations to - # the desired shape. - # Here is the point for the entering: - if ( - Point(result_coords[-1] - ).distance(item.nearest_point_parent) - > constants.factor_offset_starting_points * abs_offset - ): - result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - - # Check whether the number of points of the connecting lines - # from child to child can be reduced - if len(child_coords) > 1: - point = calculate_replacing_middle_point( - LineString( - [result_coords[-1], child_coords[0], child_coords[1]] - ), - abs_offset, - stitch_distance, - ) - - if point is not None: - result_coords.append(point) - result_coords_origin.append(child_coords_origin[0]) - - result_coords.extend(child_coords[1:]) - result_coords_origin.extend(child_coords_origin[1:]) - else: - result_coords.extend(child_coords) - result_coords_origin.extend(child_coords_origin) - - # And here is the point for the leaving of the child - # (distance to the own following point should not be too large) - d = item.nearest_point_parent.distance(Point(own_coords[i])) - if cur_item < len(nearest_points_list) - 1: - d = min( - d, - abs( - nearest_points_list[cur_item + - 1].proj_distance_parent - - item.proj_distance_parent - ), - ) - - if d > constants.factor_offset_starting_points * abs_offset: - result_coords.append( - current_coords.interpolate( - item.proj_distance_parent - + 2 * constants.factor_offset_starting_points * abs_offset - ).coords[0] - ) - result_coords_origin.append( - LineStringSampling.PointSource.ENTER_LEAVING_POINT - ) - # Check whether this additional point makes the last point - # of the child unnecessary - point = calculate_replacing_middle_point( - LineString( - [result_coords[-3], result_coords[-2], result_coords[-1]] - ), - abs_offset, - stitch_distance, - ) - if point is None: - result_coords.pop(-2) - result_coords_origin.pop(-2) - - cur_item += 1 - if i < len(own_coords) - 1: - if ( - Point(result_coords[-1]).distance(Point(own_coords[i])) - > abs_offset * constants.factor_offset_remove_points - ): - result_coords.append(own_coords[i]) - result_coords_origin.append(own_coords_origin[i]) - - # Since current_coords and own_coords are rastered differently - # there accumulate errors regarding the current distance. - # Since a projection of each point in own_coords would be very - # time consuming we project only every n-th point which resets - # the accumulated error every n-th point. - if i % 20 == 0: - total_distance = current_coords.project(Point(own_coords[i])) - else: - total_distance += next_distance - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin - - -# Partly taken from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py -def interpolate_LinearRings(a, b, start=None, step=.005): - """ - Interpolate between two LinearRings - Parameters - ------------- - a : shapely.geometry.Polygon.LinearRing - LinearRing start point will lie on - b : shapely.geometry.Polygon.LinearRing - LinearRing end point will lie on - start : (2,) float, or None - Point to start at - step : float - How far apart should points on - the path be. - Returns - ------------- - path : (n, 2) float - Path interpolated between two LinearRings - """ - - # resample the first LinearRing so every sample is spaced evenly - ra = trimesh.path.traversal.resample_path( - a, step=step) - if not a.is_ccw: - ra = ra[::-1] - - assert trimesh.path.util.is_ccw(ra) - if start is not None: - # find the closest index on LinerRing 'a' - # by creating a KDTree - tree_a = spatial.cKDTree(ra) - index = tree_a.query(start)[1] - ra = np.roll(ra, -index, axis=0) - - # resample the second LinearRing for even spacing - rb = trimesh.path.traversal.resample_path(b, - step=step) - if not b.is_ccw: - rb = rb[::-1] - - # we want points on 'b' that correspond index- wise - # the resampled points on 'a' - tree_b = spatial.cKDTree(rb) - # points on b with corresponding indexes to ra - pb = rb[tree_b.query(ra)[1]] - - # linearly interpolate between 'a' and 'b' - weights = np.linspace(0.0, 1.0, len(ra)).reshape((-1, 1)) - - # start on 'a' and end on 'b' - points = (ra * (1.0 - weights)) + (pb * weights) - - result = LineString(points) - - return result.simplify(constants.simplification_threshold, False) - - -def connect_raster_tree_spiral( - tree, used_offset, stitch_distance, close_point, offset_by_half): - """ - Takes the offsetted curves organized as tree, connects and samples them as a spiral. - It expects that each node in the tree has max. one child - Input: - -tree: contains the offsetted curves in a hierarchical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -close_point: defines the beginning point for stitching - (stitching starts always from the undisplaced curve) - -offset_by_half: If true the resulting points are interlaced otherwise not. - Returnvalues: - -All offsetted curves connected to one spiral and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - abs_offset = abs(used_offset) - if tree.is_leaf: - return LineStringSampling.raster_line_string_with_priority_points( - tree.val, - 0, - tree.val.length, - stitch_distance, - tree.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - result_coords = [] - result_coords_origin = [] - starting_point = close_point.coords[0] - # iterate to the second last level - for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): - ring1 = node.val - ring2 = node.children[0].val - - part_spiral = interpolate_LinearRings( - ring1, ring2, starting_point) - node.val = part_spiral - - for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): - (own_coords, own_coords_origin) = LineStringSampling.raster_line_string_with_priority_points( - node.val, - 0, - node.val.length, - stitch_distance, - node.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - PointTransfer.transfer_points_to_surrounding( - node, - -used_offset, - offset_by_half, - own_coords, - own_coords_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=False, - transfer_to_child=True) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - PointTransfer.transfer_points_to_surrounding( - node, - -used_offset, - False, - own_coords, - own_coords_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=False, - transfer_to_child=True) - - # Check whether starting of own_coords or end of result_coords can be removed - if not result_coords: - result_coords.extend(own_coords) - result_coords_origin.extend(own_coords_origin) - elif len(own_coords) > 0: - if Point(result_coords[-1]).distance(Point(own_coords[0])) > constants.line_lengh_seen_as_one_point: - lineseg = LineString( - [result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) - else: - lineseg = LineString( - [result_coords[-2], result_coords[-1], own_coords[1]]) - (temp_coords, _) = LineStringSampling.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, - DEPQ(), abs_offset, offset_by_half, False) - if len(temp_coords) == 2: # only start and end point of lineseg was needed - result_coords.pop() - result_coords_origin.pop() - result_coords.extend(own_coords[1:]) - result_coords_origin.extend(own_coords_origin[1:]) - elif len(temp_coords) == 3: # one middle point within lineseg was needed - result_coords.pop() - result_coords.append(temp_coords[1]) - result_coords.extend(own_coords[1:]) - result_coords_origin.extend(own_coords_origin[1:]) - else: # all points were needed - result_coords.extend(own_coords) - result_coords_origin.extend(own_coords_origin) - # make sure the next section starts where this - # section of the curve ends - starting_point = result_coords[-1] - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin diff --git a/lib/stitches/DebuggingMethods.py b/lib/stitches/DebuggingMethods.py deleted file mode 100644 index e239edba..00000000 --- a/lib/stitches/DebuggingMethods.py +++ /dev/null @@ -1,173 +0,0 @@ -import matplotlib.pyplot as plt -from shapely.geometry import Polygon - -from anytree import PreOrderIter - -# import LineStringSampling as Sampler -import numpy as np -import matplotlib.collections as mcoll - -# def offset_polygons(polys, offset,joinstyle): -# if polys.geom_type == 'Polygon': -# inners = polys.interiors -# outer = polys.exterior -# polyinners = [] -# for inner in inners: -# inner = inner.parallel_offset(offset,'left', 5, joinstyle, 1) -# polyinners.append(Polygon(inner)) -# outer = outer.parallel_offset(offset,'left', 5, joinstyle, 1) -# return Polygon(outer).difference(MultiPolygon(polyinners)) -# else: -# polyreturns = [] -# for poly in polys: -# inners = poly.interiors -# outer = poly.exterior -# polyinners = [] -# for inner in inners: -# inner = inner.parallel_offset(offset,'left', 5, joinstyle, 1) -# polyinners.append(Polygon(inner)) -# outer = outer.parallel_offset(offset,'left', 5, joinstyle, 1) -# result = Polygon(outer).difference(MultiPolygon(polyinners)) -# polyreturns.append(result) -# return MultiPolygon(polyreturns) - -# For debugging - - -def plot_MultiPolygon(MultiPoly, plt, colorString): - if MultiPoly.is_empty: - return - if MultiPoly.geom_type == "Polygon": - x2, y2 = MultiPoly.exterior.xy - plt.plot(x2, y2, colorString) - - for inners in MultiPoly.interiors: - x2, y2 = inners.coords.xy - plt.plot(x2, y2, colorString) - else: - for poly in MultiPoly: - x2, y2 = poly.exterior.xy - plt.plot(x2, y2, colorString) - - for inners in poly.interiors: - x2, y2 = inners.coords.xy - plt.plot(x2, y2, colorString) - - -# Test whether there are areas which would currently not be stitched but should be stitched - - -def subtractResult(poly, rootPoly, offsetThresh): - poly2 = Polygon(poly) - for node in PreOrderIter(rootPoly): - poly2 = poly2.difference(node.val.buffer(offsetThresh, 5, 3, 3)) - return poly2 - - -# Used for debugging - plots all polygon exteriors within an AnyTree which is provided by the root node rootPoly. - - -def drawPoly(rootPoly, colorString): - fig, axs = plt.subplots(1, 1) - axs.axis("equal") - plt.gca().invert_yaxis() - for node in PreOrderIter(rootPoly): - # if(node.id == "hole"): - # node.val = LinearRing(node.val.coords[::-1]) - print("Bounds:") - print(node.val.bounds) - x2, y2 = node.val.coords.xy - plt.plot(x2, y2, colorString) - plt.show(block=True) - - -def drawresult(resultcoords, resultcoords_Origin, colorString): - fig, axs = plt.subplots(1, 1) - axs.axis("equal") - plt.gca().invert_yaxis() - plt.plot(*zip(*resultcoords), colorString) - - colormap = np.array(["r", "g", "b", "c", "m", "y", "k", "gray", "m"]) - labelmap = np.array( - [ - "MUST_USE", - "REGULAR_SPACING", - "INITIAL_RASTERING", - "EDGE_NEEDED", - "NOT_NEEDED", - "ALREADY_TRANSFERRED", - "ADDITIONAL_TRACKING_POINT_NOT_NEEDED", - "EDGE_RASTERING_ALLOWED", - "EDGE_PREVIOUSLY_SHIFTED", - ] - ) - - for i in range(0, 8 + 1): - # if i != Sampler.PointSource.EDGE_NEEDED and i != Sampler.PointSource.INITIAL_RASTERING: - # continue - selection = [] - for j in range(len(resultcoords)): - if i == resultcoords_Origin[j]: - selection.append(resultcoords[j]) - if len(selection) > 0: - plt.scatter(*zip(*selection), c=colormap[i], label=labelmap[i]) - - # plt.scatter(*zip(*resultcoords), - # c=colormap[resultcoords_Origin]) - axs.legend() - plt.show(block=True) - - -# Just for debugging in order to draw the connected line with color gradient - - -def colorline( - x, - y, - z=None, - cmap=plt.get_cmap("copper"), - norm=plt.Normalize(0.0, 1.0), - linewidth=3, - alpha=1.0, -): - """ - http://nbviewer.ipython.org/github/dpsanders/matplotlib-examples/blob/master/colorline.ipynb - http://matplotlib.org/examples/pylab_examples/multicolored_line.html - Plot a colored line with coordinates x and y - Optionally specify colors in the array z - Optionally specify a colormap, a norm function and a line width - """ - - # Default colors equally spaced on [0,1]: - if z is None: - z = np.linspace(0.0, 1.0, len(x)) - - # Special case if a single number: - if not hasattr(z, "__iter__"): # to check for numerical input -- this is a hack - z = np.array([z]) - - z = np.asarray(z) - - segments = make_segments(x, y) - lc = mcoll.LineCollection( - segments, array=z, cmap=cmap, norm=norm, linewidth=linewidth, alpha=alpha - ) - - ax = plt.gca() - ax.add_collection(lc) - - return lc - - -# Used by colorline - - -def make_segments(x, y): - """ - Create list of line segments from x and y coordinates, in the correct format - for LineCollection: an array of the form numlines x (points per line) x 2 (x - and y) array - """ - points = np.array([x, y]).T.reshape(-1, 1, 2) - segments = np.concatenate([points[:-1], points[1:]], axis=1) - return segments diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py deleted file mode 100644 index 71660e2d..00000000 --- a/lib/stitches/LineStringSampling.py +++ /dev/null @@ -1,354 +0,0 @@ -from shapely.geometry.polygon import LineString -from shapely.geometry import Point -from shapely.ops import substring -import math -import numpy as np -from enum import IntEnum -from ..stitches import constants -from ..stitches import PointTransfer - - -class PointSource(IntEnum): - """ - Used to tag the origin of a rastered point - """ - # MUST_USE = 0 # Legacy - REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance - # INITIAL_RASTERING = 2 #Legacy - # point which must be stitched to avoid to large deviations to the desired path - EDGE_NEEDED = 3 - # NOT_NEEDED = 4 #Legacy - # ALREADY_TRANSFERRED = 5 #Legacy - # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy - # EDGE_RASTERING_ALLOWED = 7 #Legacy - # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy - ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child - # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - SOFT_EDGE_INTERNAL = 10 - # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - HARD_EDGE_INTERNAL = 11 - # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT - PROJECTED_POINT = 12 - REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance - # FORBIDDEN_POINT_INTERNAL=14 #Legacy - SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE - # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) - HARD_EDGE = 16 - FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden - # If one decides to avoid forbidden points new points to the left and to the right as replacement are created - REPLACED_FORBIDDEN_POINT = 18 - DIRECT = 19 # Calculated by next neighbor projection - OVERNEXT = 20 # Calculated by overnext neighbor projection - - -def calculate_line_angles(line): - """ - Calculates the angles between adjacent edges at each interior point - Note that the first and last values in the return array are zero since for the boundary points no - angle calculations were possible - """ - Angles = np.zeros(len(line.coords)) - for i in range(1, len(line.coords)-1): - vec1 = np.array(line.coords[i])-np.array(line.coords[i-1]) - vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) - vec1length = np.linalg.norm(vec1) - vec2length = np.linalg.norm(vec2) - # if vec1length <= 0: - # print("HIER FEHLER") - - # if vec2length <=0: - # print("HIER FEHLEr") - assert(vec1length > 0) - assert(vec2length > 0) - scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) - scalar_prod = min(max(scalar_prod, -1), 1) - # if scalar_prod > 1.0: - # scalar_prod = 1.0 - # elif scalar_prod < -1.0: - # scalar_prod = -1.0 - Angles[i] = math.acos(scalar_prod) - return Angles - - -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 - must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): - """ - Rasters a line between start_distance and end_distance. - Input: - -line: The line to be rastered - -start_distance: The distance along the line from which the rastering should start - -end_distance: The distance along the line until which the rastering should be done - -maxstitch_distance: The maximum allowed stitch distance - -Note that start_distance > end_distance for stitching_direction = -1 - -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - -abs_offset: used offset between to offsetted curves - -offset_by_half: Whether the points of neighboring lines shall be interlaced or not - -replace_forbidden_points: Whether points marked as forbidden in must_use_points_deque shall be replaced by adjacend points - Output: - -List of tuples with the rastered point coordinates - -List which defines the point origin for each point according to the PointSource enum. - """ - - if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): - return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] - - deque_points = list(must_use_points_deque) - - linecoords = line.coords - - if start_distance > end_distance: - start_distance, end_distance = line.length - \ - start_distance, line.length-end_distance - linecoords = linecoords[::-1] - for i in range(len(deque_points)): - deque_points[i] = (deque_points[i][0], - line.length-deque_points[i][1]) - else: - # Since points with highest priority (=distance along line) are first (descending sorted) - deque_points = deque_points[::-1] - - # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] - while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): - deque_points.pop(0) - while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): - deque_points.pop() - - -# Ordering in priority queue: -# (point, LineStringSampling.PointSource), priority) - # might be different from line for stitching_direction=-1 - aligned_line = LineString(linecoords) - path_coords = substring(aligned_line, - start_distance, end_distance) - - # aligned line is a line without doubled points. - # I had the strange situation in which the offset "start_distance" from the line beginning - # resulted in a starting point which was already present in aligned_line causing a doubled point. - # A double point is not allowed in the following calculations so we need to remove it: - if (abs(path_coords.coords[0][0]-path_coords.coords[1][0]) < constants.eps and - abs(path_coords.coords[0][1]-path_coords.coords[1][1]) < constants.eps): - path_coords.coords = path_coords.coords[1:] - if (abs(path_coords.coords[-1][0]-path_coords.coords[-2][0]) < constants.eps and - abs(path_coords.coords[-1][1]-path_coords.coords[-2][1]) < constants.eps): - path_coords.coords = path_coords.coords[:-1] - - angles = calculate_line_angles(path_coords) - # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge - angles[0] = 1.1*constants.limiting_angle - angles[-1] = 1.1*constants.limiting_angle - - current_distance = 0 - last_point = Point(path_coords.coords[0]) - # Next we merge the line points and the projected (deque) points into one list - merged_point_list = [] - dq_iter = 0 - for point, angle in zip(path_coords.coords, angles): - # if abs(point[0]-7) < 0.2 and abs(point[1]-3.3) < 0.2: - # print("GEFUNDEN") - current_distance += last_point.distance(Point(point)) - last_point = Point(point) - while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance+start_distance: - # We want to avoid setting points at soft edges close to forbidden points - if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: - # Check whether a previous added point is a soft edge close to the forbidden point - if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and - abs(merged_point_list[-1][1]-deque_points[dq_iter][1]+start_distance < abs_offset*constants.factor_offset_forbidden_point)): - item = merged_point_list.pop() - merged_point_list.append((PointTransfer.projected_point_tuple( - point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1]-start_distance)) - else: - merged_point_list.append( - (deque_points[dq_iter][0], deque_points[dq_iter][1]-start_distance)) - # merged_point_list.append(deque_points[dq_iter]) - dq_iter += 1 - # Check whether the current point is close to a forbidden point - if (dq_iter < len(deque_points) and - deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and - angle < constants.limiting_angle and - abs(deque_points[dq_iter-1][1]-current_distance-start_distance) < abs_offset*constants.factor_offset_forbidden_point): - point_source = PointSource.FORBIDDEN_POINT - else: - if angle < constants.limiting_angle: - point_source = PointSource.SOFT_EDGE_INTERNAL - else: - point_source = PointSource.HARD_EDGE_INTERNAL - merged_point_list.append((PointTransfer.projected_point_tuple( - point=Point(point), point_source=point_source), current_distance)) - - result_list = [merged_point_list[0]] - - # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified - # to a straight line by shapelys simplify method. - # Then, look at the points within this segment and choose the best fitting one - # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment - # and start point for the next segment (so we do not always take the maximum possible length for a segment) - segment_start_index = 0 - segment_end_index = 1 - forbidden_point_list = [] - while segment_end_index < len(merged_point_list): - # if abs(merged_point_list[segment_end_index-1][0].point.coords[0][0]-67.9) < 0.2 and - # abs(merged_point_list[segment_end_index-1][0].point.coords[0][1]-161.0)< 0.2: - # print("GEFUNDEN") - - # Collection of points for the current segment - current_point_list = [merged_point_list[segment_start_index][0].point] - - while segment_end_index < len(merged_point_list): - segment_length = merged_point_list[segment_end_index][1] - \ - merged_point_list[segment_start_index][1] - if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: - new_distance = merged_point_list[segment_start_index][1] + \ - maxstitch_distance - merged_point_list.insert(segment_end_index, (PointTransfer.projected_point_tuple( - point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) - # if (abs(merged_point_list[segment_end_index][0].point.coords[0][0]-12.2) < 0.2 and - # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-0.9) < 0.2): - # print("GEFUNDEN") - segment_end_index += 1 - break - # if abs(merged_point_list[segment_end_index][0].point.coords[0][0]-93.6) < 0.2 and - # abs(merged_point_list[segment_end_index][0].point.coords[0][1]-122.7)< 0.2: - # print("GEFUNDEN") - - current_point_list.append( - merged_point_list[segment_end_index][0].point) - simplified_len = len(LineString(current_point_list).simplify( - constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) - if simplified_len > 2: # not all points have been simplified - so we need to add it - break - - if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: - segment_end_index += 1 - break - segment_end_index += 1 - - segment_end_index -= 1 - - # Now we choose the best fitting point within this segment - index_overnext = -1 - index_direct = -1 - index_hard_edge = -1 - - iter = segment_start_index+1 - while (iter <= segment_end_index): - if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: - index_overnext = iter - elif merged_point_list[iter][0].point_source == PointSource.DIRECT: - index_direct = iter - elif merged_point_list[iter][0].point_source == PointSource.HARD_EDGE_INTERNAL: - index_hard_edge = iter - iter += 1 - if index_hard_edge != -1: - segment_end_index = index_hard_edge - else: - if offset_by_half: - index_preferred = index_overnext - index_less_preferred = index_direct - else: - index_preferred = index_direct - index_less_preferred = index_overnext - - if index_preferred != -1: - if (index_less_preferred != -1 and index_less_preferred > index_preferred and - (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= - constants.factor_segment_length_direct_preferred_over_overnext * - (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): - # We allow to take the direct projected point instead of the overnext projected point if it would result in a - # significant longer segment length - segment_end_index = index_less_preferred - else: - segment_end_index = index_preferred - elif index_less_preferred != -1: - segment_end_index = index_less_preferred - - # Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges - # If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: - new_point_left_proj = result_list[index][1]-distance_left - if new_point_left_proj < 0: - new_point_left_proj += line.length - new_point_right_proj = result_list[index][1]+distance_right - if new_point_right_proj > line.length: - new_point_right_proj -= line.length - point_left = line.interpolate(new_point_left_proj) - point_right = line.interpolate(new_point_right_proj) - forbidden_point_distance = result_list[index][0].point.distance( - LineString([point_left, point_right])) - if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: - del result_list[index] - result_list.insert(index, (PointTransfer.projected_point_tuple( - point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) - result_list.insert(index, (PointTransfer.projected_point_tuple( - point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) - current_index_shift += 1 - break - else: - distance_left /= 2.0 - distance_right /= 2.0 - return result_list - - -if __name__ == "__main__": - line = LineString([(0, 0), (1, 0), (2, 1), (3, 0), (4, 0)]) - - print(calculate_line_angles(line)*180.0/math.pi) diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py deleted file mode 100644 index 93fe02c5..00000000 --- a/lib/stitches/PointTransfer.py +++ /dev/null @@ -1,503 +0,0 @@ -from shapely.geometry import Point, MultiPoint -from shapely.geometry.polygon import LineString, LinearRing -from collections import namedtuple -from shapely.ops import nearest_points -import math -from ..stitches import constants -from ..stitches import LineStringSampling - -projected_point_tuple = namedtuple( - 'projected_point_tuple', ['point', 'point_source']) - - -def calc_transferred_point(bisectorline, child): - """ - Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). - It returns the intersection point and its distance along the coordinates of the child or "None, None" if no - intersection was found. - """ - result = bisectorline.intersection(child.val) - if result.is_empty: - return None, None - desired_point = Point() - if result.geom_type == 'Point': - desired_point = result - elif result.geom_type == 'LineString': - desired_point = Point(result.coords[0]) - else: - resultlist = list(result) - desired_point = resultlist[0] - if len(resultlist) > 1: - desired_point = nearest_points( - result, Point(bisectorline.coords[0]))[0] - - priority = child.val.project(desired_point) - point = desired_point - return point, priority - - -def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 - overnext_neighbor=False, transfer_forbidden_points=False, - transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): - """ - Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs - To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. - Input: - -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. - -used_offset: The used offset when the curves where offsetted - -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" - -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points - can be handled as closed ring - -to_transfer_points_origin: The origin tag of each point in to_transfer_points - -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) - -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as - forbidden points to the neighbor to avoid a point placing there - -transfer_to_parent: If True, points will be transferred to the parent - -transfer_to_sibling: If True, points will be transferred to the siblings - -transfer_to_child: If True, points will be transferred to the childs - Output: - -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - """ - - assert(len(to_transfer_points) == len(to_transfer_points_origin) - or len(to_transfer_points_origin) == 0) - assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and ( - offset_by_half or not offset_by_half and overnext_neighbor)) - - if len(to_transfer_points) == 0: - return - - # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: - childs_tuple = treenode.children - siblings_tuple = treenode.siblings - # Take only neighbors which have not rastered before - # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) - child_list = [] - child_list_forbidden = [] - neighbor_list = [] - neighbor_list_forbidden = [] - - if transfer_to_child: - for child in childs_tuple: - if not child.already_rastered: - if not overnext_neighbor: - child_list.append(child) - if transfer_forbidden_points: - child_list_forbidden.append(child) - if overnext_neighbor: - for subchild in child.children: - if not subchild.already_rastered: - child_list.append(subchild) - - if transfer_to_sibling: - for sibling in siblings_tuple: - if not sibling.already_rastered: - if not overnext_neighbor: - neighbor_list.append(sibling) - if transfer_forbidden_points: - neighbor_list_forbidden.append(sibling) - if overnext_neighbor: - for subchild in sibling.children: - if not subchild.already_rastered: - neighbor_list.append(subchild) - - if transfer_to_parent and treenode.parent is not None: - if not treenode.parent.already_rastered: - if not overnext_neighbor: - neighbor_list.append(treenode.parent) - if transfer_forbidden_points: - neighbor_list_forbidden.append(treenode.parent) - if overnext_neighbor: - if treenode.parent.parent is not None: - if not treenode.parent.parent.already_rastered: - neighbor_list.append(treenode.parent.parent) - - if not neighbor_list and not child_list: - return - - # Go through all rastered points of treenode and check where they should be transferred to its neighbar - point_list = list(MultiPoint(to_transfer_points)) - point_source_list = to_transfer_points_origin.copy() - - # For a linear ring the last point is the same as the starting point which we delete - # since we do not want to transfer the starting and end point twice - closed_line = LineString(to_transfer_points) - if point_list[0].distance(point_list[-1]) < constants.point_spacing_to_be_considered_equal: - point_list.pop() - if(point_source_list): - point_source_list.pop() - if len(point_list) == 0: - return - else: - # closed line is needed if we offset by half since we need to determine the line - # length including the closing segment - closed_line = LinearRing(to_transfer_points) - - bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor * \ - (2.0 if overnext_neighbor else 1.0) - - bisectorline_length_forbidden_points = abs(used_offset) * \ - constants.transfer_point_distance_factor - - linesign_child = math.copysign(1, used_offset) - - i = 0 - currentDistance = 0 - while i < len(point_list): - assert(point_source_list[i] != - LineStringSampling.PointSource.ENTER_LEAVING_POINT) - # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: - # print("HIIIIIIIIIIIERRR") - - # We create a bisecting line through the current point - normalized_vector_prev_x = ( - point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape - normalized_vector_prev_y = ( - point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) - prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + - normalized_vector_prev_y*normalized_vector_prev_y) - - normalized_vector_prev_x /= prev_spacing - normalized_vector_prev_y /= prev_spacing - - normalized_vector_next_x = normalized_vector_next_y = 0 - next_spacing = 0 - while True: - normalized_vector_next_x = ( - point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) - normalized_vector_next_y = ( - point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) - next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + - normalized_vector_next_y*normalized_vector_next_y) - if next_spacing < constants.line_lengh_seen_as_one_point: - point_list.pop(i) - if(point_source_list): - point_source_list.pop(i) - currentDistance += next_spacing - continue - - normalized_vector_next_x /= next_spacing - normalized_vector_next_y /= next_spacing - break - - vecx = (normalized_vector_next_x+normalized_vector_prev_x) - vecy = (normalized_vector_next_y+normalized_vector_prev_y) - vec_length = math.sqrt(vecx*vecx+vecy*vecy) - - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - # The two sides are (anti)parallel - construct normal vector (bisector) manually: - # If we offset by half we are offseting normal to the next segment - if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): - vecx = linesign_child*bisectorline_length*normalized_vector_next_y - vecy = -linesign_child*bisectorline_length*normalized_vector_next_x - - if transfer_forbidden_points: - vecx_forbidden_point = linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_x - - else: - vecx *= bisectorline_length/vec_length - vecy *= bisectorline_length/vec_length - - if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: - vecx = -vecx - vecy = -vecy - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - assert((vecx*normalized_vector_next_y-vecy * - normalized_vector_next_x)*linesign_child >= 0) - - originPoint = point_list[i] - originPoint_forbidden_point = point_list[i] - if(offset_by_half): - off = currentDistance+next_spacing/2 - if off > closed_line.length: - off -= closed_line.length - originPoint = closed_line.interpolate(off) - - bisectorline_child = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) - - bisectorline_neighbor = LineString([(originPoint.coords[0][0], - originPoint.coords[0][1]), - (originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy)]) - - bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - - bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], - originPoint_forbidden_point.coords[0][1]), - (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) - - for child in child_list: - point, priority = calc_transferred_point(bisectorline_child, child) - if point is None: - continue - child.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for child in child_list_forbidden: - point, priority = calc_transferred_point( - bisectorline_forbidden_point_child, child) - if point is None: - continue - child.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - for neighbor in neighbor_list: - point, priority = calc_transferred_point( - bisectorline_neighbor, neighbor) - if point is None: - continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for neighbor in neighbor_list_forbidden: - point, priority = calc_transferred_point( - bisectorline_forbidden_point_neighbor, neighbor) - if point is None: - continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - i += 1 - currentDistance += next_spacing - - assert(len(point_list) == len(point_source_list)) - - -# Calculates the nearest interserction point of "bisectorline" with the coordinates of child. -# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no -# intersection was found. -def calc_transferred_point_graph(bisectorline, edge_geometry): - result = bisectorline.intersection(edge_geometry) - if result.is_empty: - return None, None - desired_point = Point() - if result.geom_type == 'Point': - desired_point = result - elif result.geom_type == 'LineString': - desired_point = Point(result.coords[0]) - else: - resultlist = list(result) - desired_point = resultlist[0] - if len(resultlist) > 1: - desired_point = nearest_points( - result, Point(bisectorline.coords[0]))[0] - - priority = edge_geometry.project(desired_point) - point = desired_point - return point, priority - - -def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 - overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): - """ - Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) - To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. - Input: - -fill_stitch_graph: Graph data structure of the stitching lines - -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered - -used_offset: The used offset when the curves where offsetted - -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" - -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points - can be handled as closed ring - -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) - -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as - forbidden points to the neighbor to avoid a point placing there - -transfer_to_previous: If True, points will be transferred to the previous edge in the graph - -transfer_to_next: If True, points will be transferred to the next edge in the graph - Output: - -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque - is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) - index of point_origin is the index of the point in the neighboring line - """ - - assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) - assert(not transfer_forbidden_points or transfer_forbidden_points and ( - offset_by_half or not offset_by_half and overnext_neighbor)) - - if len(to_transfer_points) == 0: - return - - # Take only neighbors which have not rastered before - # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) - previous_edge_list = [] - previous_edge_list_forbidden = [] - next_edge_list = [] - next_edge_list_forbidden = [] - - if transfer_to_previous: - previous_neighbors_tuples = current_edge['previous_neighbors'] - for neighbor in previous_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0] - ][neighbor[-1]]['segment'] - if not neighbor_edge['already_rastered']: - if not overnext_neighbor: - previous_edge_list.append(neighbor_edge) - if transfer_forbidden_points: - previous_edge_list_forbidden.append(neighbor_edge) - if overnext_neighbor: - overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] - for overnext_neighbor in overnext_previous_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] - ][overnext_neighbor[-1]]['segment'] - if not overnext_neighbor_edge['already_rastered']: - previous_edge_list.append(overnext_neighbor_edge) - - if transfer_to_next: - next_neighbors_tuples = current_edge['next_neighbors'] - for neighbor in next_neighbors_tuples: - neighbor_edge = fill_stitch_graph[neighbor[0] - ][neighbor[-1]]['segment'] - if not neighbor_edge['already_rastered']: - if not overnext_neighbor: - next_edge_list.append(neighbor_edge) - if transfer_forbidden_points: - next_edge_list_forbidden.append(neighbor_edge) - if overnext_neighbor: - overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] - for overnext_neighbor in overnext_next_neighbors_tuples: - overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] - ][overnext_neighbor[-1]]['segment'] - if not overnext_neighbor_edge['already_rastered']: - next_edge_list.append(overnext_neighbor_edge) - - if not previous_edge_list and not next_edge_list: - return - - # Go through all rastered points of treenode and check where they should be transferred to its neighbar - point_list = list(MultiPoint(to_transfer_points)) - line = LineString(to_transfer_points) - - bisectorline_length = abs(used_offset) * \ - constants.transfer_point_distance_factor * \ - (2.0 if overnext_neighbor else 1.0) - - bisectorline_length_forbidden_points = abs(used_offset) * \ - constants.transfer_point_distance_factor - - linesign_child = math.copysign(1, used_offset) - - i = 0 - currentDistance = 0 - while i < len(point_list): - - # if abs(point_list[i].coords[0][0]-47) < 0.3 and abs(point_list[i].coords[0][1]-4.5) < 0.3: - # print("HIIIIIIIIIIIERRR") - - # We create a bisecting line through the current point - normalized_vector_prev_x = ( - point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape - normalized_vector_prev_y = ( - point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) - prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + - normalized_vector_prev_y*normalized_vector_prev_y) - - # if prev_spacing == 0: - # print("HIER FEHLER") - - normalized_vector_prev_x /= prev_spacing - normalized_vector_prev_y /= prev_spacing - - normalized_vector_next_x = normalized_vector_next_y = 0 - next_spacing = 0 - while True: - normalized_vector_next_x = ( - point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) - normalized_vector_next_y = ( - point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) - next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + - normalized_vector_next_y*normalized_vector_next_y) - if next_spacing < constants.line_lengh_seen_as_one_point: - point_list.pop(i) - currentDistance += next_spacing - continue - - normalized_vector_next_x /= next_spacing - normalized_vector_next_y /= next_spacing - break - - vecx = (normalized_vector_next_x+normalized_vector_prev_x) - vecy = (normalized_vector_next_y+normalized_vector_prev_y) - vec_length = math.sqrt(vecx*vecx+vecy*vecy) - - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - # The two sides are (anti)parallel - construct normal vector (bisector) manually: - # If we offset by half we are offseting normal to the next segment - if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): - vecx = linesign_child*bisectorline_length*normalized_vector_next_y - vecy = -linesign_child*bisectorline_length*normalized_vector_next_x - - if transfer_forbidden_points: - vecx_forbidden_point = linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_y - vecy_forbidden_point = -linesign_child * \ - bisectorline_length_forbidden_points*normalized_vector_next_x - - else: - vecx *= bisectorline_length/vec_length - vecy *= bisectorline_length/vec_length - - if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: - vecx = -vecx - vecy = -vecy - vecx_forbidden_point = vecx - vecy_forbidden_point = vecy - - assert((vecx*normalized_vector_next_y-vecy * - normalized_vector_next_x)*linesign_child >= 0) - - originPoint = point_list[i] - originPoint_forbidden_point = point_list[i] - if(offset_by_half): - off = currentDistance+next_spacing/2 - if off > line.length: - break - originPoint = line.interpolate(off) - - bisectorline = LineString([(originPoint.coords[0][0]-vecx, - originPoint.coords[0][1]-vecy), - (originPoint.coords[0][0]+vecx, - originPoint.coords[0][1]+vecy)]) - - bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), - (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, - originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) - - for edge in previous_edge_list+next_edge_list: - point, priority = calc_transferred_point_graph( - bisectorline, edge['geometry']) - if point is None: - continue - edge['projected_points'].insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.OVERNEXT if overnext_neighbor - else LineStringSampling.PointSource.DIRECT), priority) - for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: - point, priority = calc_transferred_point_graph( - bisectorline_forbidden_point, edge_forbidden['geometry']) - if point is None: - continue - edge_forbidden['projected_points'].insert(projected_point_tuple( - point=point, point_source=LineStringSampling.PointSource.FORBIDDEN_POINT), priority) - - i += 1 - currentDistance += next_spacing diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py deleted file mode 100644 index 4a38c0bc..00000000 --- a/lib/stitches/StitchPattern.py +++ /dev/null @@ -1,420 +0,0 @@ -from anytree.render import RenderTree -from shapely.geometry.polygon import LinearRing, LineString -from shapely.geometry import Polygon, MultiLineString -from shapely.ops import polygonize -from shapely.geometry import MultiPolygon -from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter -from shapely.geometry.polygon import orient -from depq import DEPQ -from enum import IntEnum -from ..stitches import ConnectAndSamplePattern -from ..stitches import constants - - -def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): - """ - Solves following problem: When shapely offsets a LinearRing the - start/end point might be handled wrongly since they - are only treated as LineString. - (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) - This method checks first whether the start/end point form a problematic - edge with respect to the offset side. If it is not a problematic - edge we can use the normal offset_routine. Otherwise we need to - perform two offsets: - -offset the ring - -offset the start/end point + its two neighbors left and right - Finally both offsets are merged together to get the correct - offset of a LinearRing - """ - - coords = ring.coords[:] - # check whether edge at index 0 is concave or convex. Only for - # concave edges we need to spend additional effort - dx_seg1 = dy_seg1 = 0 - if coords[0] != coords[-1]: - dx_seg1 = coords[0][0] - coords[-1][0] - dy_seg1 = coords[0][1] - coords[-1][1] - else: - dx_seg1 = coords[0][0] - coords[-2][0] - dy_seg1 = coords[0][1] - coords[-2][1] - dx_seg2 = coords[1][0] - coords[0][0] - dy_seg2 = coords[1][1] - coords[0][1] - # use cross product: - crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 - sidesign = 1 - if side == "left": - sidesign = -1 - - # We do not need to take care of the joint n-0 since we - # offset along a concave edge: - if sidesign * offset * crossvalue <= 0: - return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) - - # We offset along a convex edge so we offset the joint n-0 separately: - if coords[0] != coords[-1]: - coords.append(coords[0]) - offset_ring1 = ring.parallel_offset( - offset, side, resolution, join_style, mitre_limit - ) - offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( - offset, side, resolution, join_style, mitre_limit - ) - - # Next we need to merge the results: - if offset_ring1.geom_type == "LineString": - return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) - else: - # We have more than one resulting LineString for offset of - # the geometry (ring) = offset_ring1. - # Hence we need to find the LineString which belongs to the - # offset of element 0 in coords =offset_ring2 - # in order to add offset_ring2 geometry to it: - result_list = [] - thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) - for offsets in offset_ring1: - if ( - abs(offsets.coords[0][0] - coords[0][0]) < thresh - and abs(offsets.coords[0][1] - coords[0][1]) < thresh - ): - result_list.append( - LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) - ) - else: - result_list.append(LinearRing(offsets)) - return MultiLineString(result_list) - - -def take_only_valid_linear_rings(rings): - """ - Removes all geometries which do not form a "valid" LinearRing - (meaning a ring which does not form a straight line) - """ - if rings.geom_type == "MultiLineString": - new_list = [] - for ring in rings: - if len(ring.coords) > 3 or ( - len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] - ): - new_list.append(ring) - if len(new_list) == 1: - return LinearRing(new_list[0]) - else: - return MultiLineString(new_list) - else: - if len(rings.coords) <= 2: - return LinearRing() - elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]: - return LinearRing() - else: - return rings - - -def make_tree_uniform_ccw(root): - """ - Since naturally holes have the opposite point ordering than non-holes we - make all lines within the tree "root" uniform (having all the same - ordering direction) - """ - for node in PreOrderIter(root): - if node.id == "hole": - node.val.coords = list(node.val.coords)[::-1] - - -# Used to define which stitching strategy shall be used -class StitchingStrategy(IntEnum): - CLOSEST_POINT = 0 - INNER_TO_OUTER = 1 - SPIRAL = 2 - - -def check_and_prepare_tree_for_valid_spiral(root): - """ - Takes a tree consisting of offsetted curves. If a parent has more than one child we - cannot create a spiral. However, to make the routine more robust, we allow more than - one child if only one of the childs has own childs. The other childs are removed in this - routine then. If the routine returns true, the tree will have been cleaned up from unwanted - childs. If the routine returns false even under the mentioned weaker conditions the - tree cannot be connected by one spiral. - """ - for children in LevelOrderGroupIter(root): - if len(children) > 1: - count = 0 - child_with_children = None - for child in children: - if not child.is_leaf: - count += 1 - child_with_children = child - if count > 1: - return False - elif count == 1: - child_with_children.parent.children = [child_with_children] - else: # count == 0 means all childs have no children so we take only the longest child - max_length = 0 - longest_child = None - for child in children: - if child.val.length > max_length: - max_length = child.val.length - longest_child = child - longest_child.parent.children = [longest_child] - return True - - -def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 - """ - Takes a polygon (which can have holes) as input and creates offsetted - versions until the polygon is filled with these smaller offsets. - These created geometries are afterwards connected to each other and - resampled with a maximum stitch_distance. - The return value is a LineString which should cover the full polygon. - Input: - -poly: The shapely polygon which can have holes - -offset: The used offset for the curves - -join_style: Join style for the offset - can be round, mitered or bevel - (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) - For examples look at - https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png - -stitch_distance maximum allowed stitch distance between two points - -offset_by_half: True if the points shall be interlaced - -strategy: According to StitchingStrategy enum class you can select between - different strategies for the connection between parent and childs. In - addition it offers the option "SPIRAL" which creates a real spiral towards inner. - In contrast to the other two options, "SPIRAL" does not end at the starting point - but at the innermost point - -starting_point: Defines the starting point for the stitching - Output: - -List of point coordinate tuples - -Tag (origin) of each point to analyze why a point was placed - at this position - """ - - if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: - raise ValueError( - "Single spiral geometry must not have more than one hole!") - - ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify( - constants.simplification_threshold, False) - root = AnyNode( - id="node", - val=ordered_poly.exterior, - already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), - ) - active_polys = [root] - active_holes = [[]] - - for holes in ordered_poly.interiors: - active_holes[0].append( - AnyNode( - id="hole", - val=holes, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None), - ) - ) - - while len(active_polys) > 0: - current_poly = active_polys.pop() - current_holes = active_holes.pop() - poly_inners = [] - - outer = offset_linear_ring( - current_poly.val, - offset, - "left", - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - outer = outer.simplify(constants.simplification_threshold, False) - outer = take_only_valid_linear_rings(outer) - - for j in range(len(current_holes)): - inner = offset_linear_ring( - current_holes[j].val, - offset, - "left", - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - inner = inner.simplify(constants.simplification_threshold, False) - inner = take_only_valid_linear_rings(inner) - if not inner.is_empty: - poly_inners.append(Polygon(inner)) - if not outer.is_empty: - if len(poly_inners) == 0: - if outer.geom_type == "LineString": - result = Polygon(outer) - else: - result = MultiPolygon(polygonize(outer)) - else: - if outer.geom_type == "LineString": - result = Polygon(outer).difference( - MultiPolygon(poly_inners)) - else: - result = MultiPolygon(outer).difference( - MultiPolygon(poly_inners)) - - if not result.is_empty and result.area > offset * offset / 10: - result_list = [] - if result.geom_type == "Polygon": - result_list = [result] - else: - result_list = list(result) - - for polygon in result_list: - polygon = orient(polygon, -1) - - if polygon.area < offset * offset / 10: - continue - - polygon = polygon.simplify( - constants.simplification_threshold, False - ) - poly_coords = polygon.exterior - poly_coords = take_only_valid_linear_rings(poly_coords) - if poly_coords.is_empty: - continue - - node = AnyNode( - id="node", - parent=current_poly, - val=poly_coords, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None - ), - ) - active_polys.append(node) - hole_node_list = [] - for hole in polygon.interiors: - hole_node = AnyNode( - id="hole", - val=hole, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None - ), - ) - for previous_hole in current_holes: - if Polygon(hole).contains(Polygon(previous_hole.val)): - previous_hole.parent = hole_node - hole_node_list.append(hole_node) - active_holes.append(hole_node_list) - for previous_hole in current_holes: - # If the previous holes are not - # contained in the new holes they - # have been merged with the - # outer polygon - if previous_hole.parent is None: - previous_hole.parent = current_poly - - # DebuggingMethods.drawPoly(root, 'r-') - - make_tree_uniform_ccw(root) - # print(RenderTree(root)) - if strategy == StitchingStrategy.CLOSEST_POINT: - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor( - root, offset, stitch_distance, starting_point, offset_by_half) - elif strategy == StitchingStrategy.INNER_TO_OUTER: - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer( - root, offset, stitch_distance, starting_point, offset_by_half) - elif strategy == StitchingStrategy.SPIRAL: - if not check_and_prepare_tree_for_valid_spiral(root): - raise ValueError("Geometry cannot be filled with one spiral!") - (connected_line, connected_line_origin) = ConnectAndSamplePattern.connect_raster_tree_spiral( - root, offset, stitch_distance, starting_point, offset_by_half) - else: - raise ValueError("Invalid stitching stratety!") - - return connected_line, connected_line_origin - - -if __name__ == "__main__": - line1 = LineString([(0, 0), (1, 0)]) - line2 = LineString([(0, 0), (3, 0)]) - - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=root) - - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=child1) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=child1) - child3 = AnyNode( - id="node", - val=line2, - parent=child2) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) - - print("---------------------------") - root = AnyNode( - id="root", - val=line1) - child1 = AnyNode( - id="node", - val=line1, - parent=root) - child2 = AnyNode( - id="node", - val=line1, - parent=root) - child3 = AnyNode( - id="node", - val=line2, - parent=child1) - child4 = AnyNode( - id="node", - val=line2, - parent=child2) - print(RenderTree(root)) - print(check_and_prepare_tree_for_valid_spiral(root)) - print(RenderTree(root)) diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index b63f4be1..7af99560 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -20,8 +20,8 @@ from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import line_string_to_point_list from .fill import intersect_region_with_grating, intersect_region_with_grating_line, stitch_row from .running_stitch import running_stitch -from .PointTransfer import transfer_points_to_surrounding_graph -from .LineStringSampling import raster_line_string_with_priority_points +from .point_transfer import transfer_points_to_surrounding_graph +from .sample_linestring import raster_line_string_with_priority_points class PathEdge(object): @@ -165,10 +165,6 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st for i in range(len(rows_of_segments)): for segment in rows_of_segments[i]: - # if abs(segment[0][0]-396.5081896849414) < 0.01: - # print("HIER") - # if segment[0][0] == segment[-1][0] and segment[0][1] == segment[-1][1]: - # print("FEHLER HIER!") # First, add the grating segments as edges. We'll use the coordinates # of the endpoints as nodes, which networkx will add automatically. @@ -666,10 +662,6 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): - # print(start_point) - # print(geometry[0]) - # if stitching_direction == -1: - # geometry.coords = geometry.coords[::-1] if stitching_direction == 1: stitched_line, _ = raster_line_string_with_priority_points( geometry, 0.0, geometry.length, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) @@ -688,8 +680,6 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s else: stitches.append( Stitch(*geometry.coords[0], tags=('fill_row_end',))) - # if stitches[-1].x == stitches[-2].x and stitches[-1].y == stitches[-2].y: - # print("FEHLER") @debug.time diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py index 162c4cfb..012fac7c 100644 --- a/lib/stitches/constants.py +++ b/lib/stitches/constants.py @@ -3,10 +3,6 @@ import math # Used in the simplify routine of shapely simplification_threshold = 0.01 -# If a transferred point is closer than this value to one of its neighbors, -# it will be checked whether it can be removed -distance_thresh_remove_transferred_point = 0.15 - # If a line segment is shorter than this threshold it is handled as a single point line_lengh_seen_as_one_point = 0.05 @@ -35,12 +31,6 @@ factor_offset_starting_points = 0.5 # if points are closer than abs_offset*factor_offset_remove_points one of it is removed factor_offset_remove_points = 0.5 -# if an unshifted relevant edge is closer than -# abs_offset*fac_offset_edge_shift -# to the line segment created by the shifted edge, -# the shift is allowed - otherwise the edge must not be shifted. -fac_offset_edge_shift = 0.25 - # decides whether the point belongs to a hard edge (must use this point during sampling) # or soft edge (do not necessarily need to use this point) limiting_angle = math.pi * 15 / 180.0 diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index ceac56d9..b5f86641 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -176,8 +176,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing rows.append(runs) else: rows.insert(0, runs) - # if len(runs) > 1: - # print("HIERRRR!") + line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( @@ -192,7 +191,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing if row_spacing > 0 and not isinstance(res, (shapely.geometry.GeometryCollection, shapely.geometry.MultiLineString)): if (res.is_empty or len(res.coords) == 1): row_spacing = -row_spacing - # print("Set to right") + line_offsetted = line.parallel_offset(row_spacing, 'left', 5) if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest line_offsetted = repair_multiple_parallel_offset_curves( @@ -203,8 +202,7 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing line_offsetted.coords = line_offsetted.coords[::-1] line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - # if res.geom_type != 'LineString': - # print("HIER!!") + return rows diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py new file mode 100644 index 00000000..a01e69cd --- /dev/null +++ b/lib/stitches/point_transfer.py @@ -0,0 +1,495 @@ +from shapely.geometry import Point, MultiPoint +from shapely.geometry.polygon import LineString, LinearRing +from collections import namedtuple +from shapely.ops import nearest_points +import math +from ..stitches import constants +from ..stitches import sample_linestring + +"""This file contains routines which shall project already selected points for stitching to remaining +unstitched lines in the neighborhood to create a regular pattern of points.""" + +projected_point_tuple = namedtuple( + 'projected_point_tuple', ['point', 'point_source']) + + +def calc_transferred_point(bisectorline, child): + """ + Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). + It returns the intersection point and its distance along the coordinates of the child or "None, None" if no + intersection was found. + """ + result = bisectorline.intersection(child.val) + if result.is_empty: + return None, None + desired_point = Point() + if result.geom_type == 'Point': + desired_point = result + elif result.geom_type == 'LineString': + desired_point = Point(result.coords[0]) + else: + resultlist = list(result) + desired_point = resultlist[0] + if len(resultlist) > 1: + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] + + priority = child.val.project(desired_point) + point = desired_point + return point, priority + + +def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_transfer_points, to_transfer_points_origin=[], # noqa: C901 + overnext_neighbor=False, transfer_forbidden_points=False, + transfer_to_parent=True, transfer_to_sibling=True, transfer_to_child=True): + """ + Takes the current tree item and its rastered points (to_transfer_points) and transfers these points to its parent, siblings and childs + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -treenode: Tree node whose points stored in "to_transfer_points" shall be transferred to its neighbors. + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -to_transfer_points_origin: The origin tag of each point in to_transfer_points + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_parent: If True, points will be transferred to the parent + -transfer_to_sibling: If True, points will be transferred to the siblings + -transfer_to_child: If True, points will be transferred to the childs + Output: + -Fills the attribute "transferred_point_priority_deque" of the siblings and parent in the tree datastructure. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert(len(to_transfer_points) == len(to_transfer_points_origin) + or len(to_transfer_points_origin) == 0) + assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) + + if len(to_transfer_points) == 0: + return + + # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: + childs_tuple = treenode.children + siblings_tuple = treenode.siblings + # Take only neighbors which have not rastered before + # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) + child_list = [] + child_list_forbidden = [] + neighbor_list = [] + neighbor_list_forbidden = [] + + if transfer_to_child: + for child in childs_tuple: + if not child.already_rastered: + if not overnext_neighbor: + child_list.append(child) + if transfer_forbidden_points: + child_list_forbidden.append(child) + if overnext_neighbor: + for subchild in child.children: + if not subchild.already_rastered: + child_list.append(subchild) + + if transfer_to_sibling: + for sibling in siblings_tuple: + if not sibling.already_rastered: + if not overnext_neighbor: + neighbor_list.append(sibling) + if transfer_forbidden_points: + neighbor_list_forbidden.append(sibling) + if overnext_neighbor: + for subchild in sibling.children: + if not subchild.already_rastered: + neighbor_list.append(subchild) + + if transfer_to_parent and treenode.parent is not None: + if not treenode.parent.already_rastered: + if not overnext_neighbor: + neighbor_list.append(treenode.parent) + if transfer_forbidden_points: + neighbor_list_forbidden.append(treenode.parent) + if overnext_neighbor: + if treenode.parent.parent is not None: + if not treenode.parent.parent.already_rastered: + neighbor_list.append(treenode.parent.parent) + + if not neighbor_list and not child_list: + return + + # Go through all rastered points of treenode and check where they should be transferred to its neighbar + point_list = list(MultiPoint(to_transfer_points)) + point_source_list = to_transfer_points_origin.copy() + + # For a linear ring the last point is the same as the starting point which we delete + # since we do not want to transfer the starting and end point twice + closed_line = LineString(to_transfer_points) + if point_list[0].distance(point_list[-1]) < constants.point_spacing_to_be_considered_equal: + point_list.pop() + if(point_source_list): + point_source_list.pop() + if len(point_list) == 0: + return + else: + # closed line is needed if we offset by half since we need to determine the line + # length including the closing segment + closed_line = LinearRing(to_transfer_points) + + bisectorline_length = abs(used_offset) * constants.transfer_point_distance_factor * (2.0 if overnext_neighbor else 1.0) + + bisectorline_length_forbidden_points = abs(used_offset) * constants.transfer_point_distance_factor + + linesign_child = math.copysign(1, used_offset) + + i = 0 + currentDistance = 0 + while i < len(point_list): + assert(point_source_list[i] != + sample_linestring.PointSource.ENTER_LEAVING_POINT) + + # We create a bisecting line through the current point + normalized_vector_prev_x = ( + point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape + normalized_vector_prev_y = ( + point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) + prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + + normalized_vector_prev_y*normalized_vector_prev_y) + + normalized_vector_prev_x /= prev_spacing + normalized_vector_prev_y /= prev_spacing + + normalized_vector_next_x = normalized_vector_next_y = 0 + next_spacing = 0 + while True: + normalized_vector_next_x = ( + point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) + normalized_vector_next_y = ( + point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) + next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + + normalized_vector_next_y*normalized_vector_next_y) + if next_spacing < constants.line_lengh_seen_as_one_point: + point_list.pop(i) + if(point_source_list): + point_source_list.pop(i) + currentDistance += next_spacing + continue + + normalized_vector_next_x /= next_spacing + normalized_vector_next_y /= next_spacing + break + + vecx = (normalized_vector_next_x+normalized_vector_prev_x) + vecy = (normalized_vector_next_y+normalized_vector_prev_y) + vec_length = math.sqrt(vecx*vecx+vecy*vecy) + + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + # The two sides are (anti)parallel - construct normal vector (bisector) manually: + # If we offset by half we are offseting normal to the next segment + if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): + vecx = linesign_child*bisectorline_length*normalized_vector_next_y + vecy = -linesign_child*bisectorline_length*normalized_vector_next_x + + if transfer_forbidden_points: + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x + + else: + vecx *= bisectorline_length/vec_length + vecy *= bisectorline_length/vec_length + + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: + vecx = -vecx + vecy = -vecy + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + assert((vecx*normalized_vector_next_y-vecy * + normalized_vector_next_x)*linesign_child >= 0) + + originPoint = point_list[i] + originPoint_forbidden_point = point_list[i] + if(offset_by_half): + off = currentDistance+next_spacing/2 + if off > closed_line.length: + off -= closed_line.length + originPoint = closed_line.interpolate(off) + + bisectorline_child = LineString([(originPoint.coords[0][0], + originPoint.coords[0][1]), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) + + bisectorline_neighbor = LineString([(originPoint.coords[0][0], + originPoint.coords[0][1]), + (originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy)]) + + bisectorline_forbidden_point_child = LineString([(originPoint_forbidden_point.coords[0][0], + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + + bisectorline_forbidden_point_neighbor = LineString([(originPoint_forbidden_point.coords[0][0], + originPoint_forbidden_point.coords[0][1]), + (originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) + + for child in child_list: + point, priority = calc_transferred_point(bisectorline_child, child) + if point is None: + continue + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for child in child_list_forbidden: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_child, child) + if point is None: + continue + child.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + for neighbor in neighbor_list: + point, priority = calc_transferred_point( + bisectorline_neighbor, neighbor) + if point is None: + continue + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for neighbor in neighbor_list_forbidden: + point, priority = calc_transferred_point( + bisectorline_forbidden_point_neighbor, neighbor) + if point is None: + continue + neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + i += 1 + currentDistance += next_spacing + + assert(len(point_list) == len(point_source_list)) + + +# Calculates the nearest interserction point of "bisectorline" with the coordinates of child. +# It returns the intersection point and its distance along the coordinates of the child or "None, None" if no +# intersection was found. +def calc_transferred_point_graph(bisectorline, edge_geometry): + result = bisectorline.intersection(edge_geometry) + if result.is_empty: + return None, None + desired_point = Point() + if result.geom_type == 'Point': + desired_point = result + elif result.geom_type == 'LineString': + desired_point = Point(result.coords[0]) + else: + resultlist = list(result) + desired_point = resultlist[0] + if len(resultlist) > 1: + desired_point = nearest_points( + result, Point(bisectorline.coords[0]))[0] + + priority = edge_geometry.project(desired_point) + point = desired_point + return point, priority + + +def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_offset, offset_by_half, to_transfer_points, # noqa: C901 + overnext_neighbor=False, transfer_forbidden_points=False, transfer_to_previous=True, transfer_to_next=True): + """ + Takes the current graph edge and its rastered points (to_transfer_points) and transfers these points to its previous and next edges (if selected) + To do so it calculates the current normal and determines its intersection with the neighbors which gives the transferred points. + Input: + -fill_stitch_graph: Graph data structure of the stitching lines + -current_edge: Current graph edge whose neighbors in fill_stitch_graph shall be considered + -used_offset: The used offset when the curves where offsetted + -offset_by_half: True if the transferred points shall be interlaced with respect to the points in "to_transfer_points" + -to_transfer_points: List of points belonging to treenode which shall be transferred - it is assumed that to_transfer_points + can be handled as closed ring + -overnext_neighbor: Transfer the points to the overnext neighbor (gives a more stable interlacing) + -transfer_forbidden_points: Only allowed for interlacing (offset_by_half): Might be used to transfer points unshifted as + forbidden points to the neighbor to avoid a point placing there + -transfer_to_previous: If True, points will be transferred to the previous edge in the graph + -transfer_to_next: If True, points will be transferred to the next edge in the graph + Output: + -Fills the attribute "transferred_point_priority_deque" of the next/previous edges. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + """ + + assert((overnext_neighbor and not offset_by_half) or not overnext_neighbor) + assert(not transfer_forbidden_points or transfer_forbidden_points and ( + offset_by_half or not offset_by_half and overnext_neighbor)) + + if len(to_transfer_points) == 0: + return + + # Take only neighbors which have not rastered before + # We need to distinguish between childs (project towards inner) and parent/siblings (project towards outer) + previous_edge_list = [] + previous_edge_list_forbidden = [] + next_edge_list = [] + next_edge_list_forbidden = [] + + if transfer_to_previous: + previous_neighbors_tuples = current_edge['previous_neighbors'] + for neighbor in previous_neighbors_tuples: + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] + if not neighbor_edge['already_rastered']: + if not overnext_neighbor: + previous_edge_list.append(neighbor_edge) + if transfer_forbidden_points: + previous_edge_list_forbidden.append(neighbor_edge) + if overnext_neighbor: + overnext_previous_neighbors_tuples = neighbor_edge['previous_neighbors'] + for overnext_neighbor in overnext_previous_neighbors_tuples: + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] + if not overnext_neighbor_edge['already_rastered']: + previous_edge_list.append(overnext_neighbor_edge) + + if transfer_to_next: + next_neighbors_tuples = current_edge['next_neighbors'] + for neighbor in next_neighbors_tuples: + neighbor_edge = fill_stitch_graph[neighbor[0] + ][neighbor[-1]]['segment'] + if not neighbor_edge['already_rastered']: + if not overnext_neighbor: + next_edge_list.append(neighbor_edge) + if transfer_forbidden_points: + next_edge_list_forbidden.append(neighbor_edge) + if overnext_neighbor: + overnext_next_neighbors_tuples = neighbor_edge['next_neighbors'] + for overnext_neighbor in overnext_next_neighbors_tuples: + overnext_neighbor_edge = fill_stitch_graph[overnext_neighbor[0] + ][overnext_neighbor[-1]]['segment'] + if not overnext_neighbor_edge['already_rastered']: + next_edge_list.append(overnext_neighbor_edge) + + if not previous_edge_list and not next_edge_list: + return + + # Go through all rastered points of treenode and check where they should be transferred to its neighbar + point_list = list(MultiPoint(to_transfer_points)) + line = LineString(to_transfer_points) + + bisectorline_length = abs(used_offset) * \ + constants.transfer_point_distance_factor * \ + (2.0 if overnext_neighbor else 1.0) + + bisectorline_length_forbidden_points = abs(used_offset) * \ + constants.transfer_point_distance_factor + + linesign_child = math.copysign(1, used_offset) + + i = 0 + currentDistance = 0 + while i < len(point_list): + + # We create a bisecting line through the current point + normalized_vector_prev_x = ( + point_list[i].coords[0][0]-point_list[i-1].coords[0][0]) # makes use of closed shape + normalized_vector_prev_y = ( + point_list[i].coords[0][1]-point_list[i-1].coords[0][1]) + prev_spacing = math.sqrt(normalized_vector_prev_x*normalized_vector_prev_x + + normalized_vector_prev_y*normalized_vector_prev_y) + + normalized_vector_prev_x /= prev_spacing + normalized_vector_prev_y /= prev_spacing + + normalized_vector_next_x = normalized_vector_next_y = 0 + next_spacing = 0 + while True: + normalized_vector_next_x = ( + point_list[i].coords[0][0]-point_list[(i+1) % len(point_list)].coords[0][0]) + normalized_vector_next_y = ( + point_list[i].coords[0][1]-point_list[(i+1) % len(point_list)].coords[0][1]) + next_spacing = math.sqrt(normalized_vector_next_x*normalized_vector_next_x + + normalized_vector_next_y*normalized_vector_next_y) + if next_spacing < constants.line_lengh_seen_as_one_point: + point_list.pop(i) + currentDistance += next_spacing + continue + + normalized_vector_next_x /= next_spacing + normalized_vector_next_y /= next_spacing + break + + vecx = (normalized_vector_next_x+normalized_vector_prev_x) + vecy = (normalized_vector_next_y+normalized_vector_prev_y) + vec_length = math.sqrt(vecx*vecx+vecy*vecy) + + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + # The two sides are (anti)parallel - construct normal vector (bisector) manually: + # If we offset by half we are offseting normal to the next segment + if(vec_length < constants.line_lengh_seen_as_one_point or offset_by_half): + vecx = linesign_child*bisectorline_length*normalized_vector_next_y + vecy = -linesign_child*bisectorline_length*normalized_vector_next_x + + if transfer_forbidden_points: + vecx_forbidden_point = linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_y + vecy_forbidden_point = -linesign_child * \ + bisectorline_length_forbidden_points*normalized_vector_next_x + + else: + vecx *= bisectorline_length/vec_length + vecy *= bisectorline_length/vec_length + + if (vecx*normalized_vector_next_y-vecy * normalized_vector_next_x)*linesign_child < 0: + vecx = -vecx + vecy = -vecy + vecx_forbidden_point = vecx + vecy_forbidden_point = vecy + + assert((vecx*normalized_vector_next_y-vecy * + normalized_vector_next_x)*linesign_child >= 0) + + originPoint = point_list[i] + originPoint_forbidden_point = point_list[i] + if(offset_by_half): + off = currentDistance+next_spacing/2 + if off > line.length: + break + originPoint = line.interpolate(off) + + bisectorline = LineString([(originPoint.coords[0][0]-vecx, + originPoint.coords[0][1]-vecy), + (originPoint.coords[0][0]+vecx, + originPoint.coords[0][1]+vecy)]) + + bisectorline_forbidden_point = LineString([(originPoint_forbidden_point.coords[0][0]-vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point), + (originPoint_forbidden_point.coords[0][0]+vecx_forbidden_point, + originPoint_forbidden_point.coords[0][1]+vecy_forbidden_point)]) + + for edge in previous_edge_list+next_edge_list: + point, priority = calc_transferred_point_graph( + bisectorline, edge['geometry']) + if point is None: + continue + edge['projected_points'].insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.OVERNEXT if overnext_neighbor + else sample_linestring.PointSource.DIRECT), priority) + for edge_forbidden in previous_edge_list_forbidden+next_edge_list_forbidden: + point, priority = calc_transferred_point_graph( + bisectorline_forbidden_point, edge_forbidden['geometry']) + if point is None: + continue + edge_forbidden['projected_points'].insert(projected_point_tuple( + point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) + + i += 1 + currentDistance += next_spacing diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py new file mode 100644 index 00000000..fb4bbc52 --- /dev/null +++ b/lib/stitches/sample_linestring.py @@ -0,0 +1,325 @@ +from shapely.geometry.polygon import LineString +from shapely.geometry import Point +from shapely.ops import substring +import math +import numpy as np +from enum import IntEnum +from ..stitches import constants +from ..stitches import point_transfer + + +class PointSource(IntEnum): + """ + Used to tag the origin of a rastered point + """ + # MUST_USE = 0 # Legacy + REGULAR_SPACING = 1 # introduced to not exceed maximal stichting distance + # INITIAL_RASTERING = 2 #Legacy + # point which must be stitched to avoid to large deviations to the desired path + EDGE_NEEDED = 3 + # NOT_NEEDED = 4 #Legacy + # ALREADY_TRANSFERRED = 5 #Legacy + # ADDITIONAL_TRACKING_POINT_NOT_NEEDED = 6 #Legacy + # EDGE_RASTERING_ALLOWED = 7 #Legacy + # EDGE_PREVIOUSLY_SHIFTED = 8 #Legacy + ENTER_LEAVING_POINT = 9 # Whether this point is used to enter or leave a child + # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + SOFT_EDGE_INTERNAL = 10 + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE_INTERNAL = 11 + # If the point was created by a projection (transferred point) of a neighbor it is marked as PROJECTED_POINT + PROJECTED_POINT = 12 + REGULAR_SPACING_INTERNAL = 13 # introduced to not exceed maximal stichting distance + # FORBIDDEN_POINT_INTERNAL=14 #Legacy + SOFT_EDGE = 15 # If the angle at a point is <= constants.limiting_angle this point is marked as SOFT_EDGE + # If the angle at a point is > constants.limiting_angle this point is marked as HARD_EDGE (HARD_EDGES will always be stitched) + HARD_EDGE = 16 + FORBIDDEN_POINT = 17 # Only relevant for desired interlacing - non-shifted point positions at the next neighbor are marked as forbidden + # If one decides to avoid forbidden points new points to the left and to the right as replacement are created + REPLACED_FORBIDDEN_POINT = 18 + DIRECT = 19 # Calculated by next neighbor projection + OVERNEXT = 20 # Calculated by overnext neighbor projection + + +def calculate_line_angles(line): + """ + Calculates the angles between adjacent edges at each interior point + Note that the first and last values in the return array are zero since for the boundary points no + angle calculations were possible + """ + Angles = np.zeros(len(line.coords)) + for i in range(1, len(line.coords)-1): + vec1 = np.array(line.coords[i])-np.array(line.coords[i-1]) + vec2 = np.array(line.coords[i+1])-np.array(line.coords[i]) + vec1length = np.linalg.norm(vec1) + vec2length = np.linalg.norm(vec2) + + assert(vec1length > 0) + assert(vec2length > 0) + scalar_prod = np.dot(vec1, vec2)/(vec1length*vec2length) + scalar_prod = min(max(scalar_prod, -1), 1) + + Angles[i] = math.acos(scalar_prod) + return Angles + + +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 + must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): + """ + Rasters a line between start_distance and end_distance. + Input: + -line: The line to be rastered + -start_distance: The distance along the line from which the rastering should start + -end_distance: The distance along the line until which the rastering should be done + -maxstitch_distance: The maximum allowed stitch distance + -Note that start_distance > end_distance for stitching_direction = -1 + -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque + is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) + index of point_origin is the index of the point in the neighboring line + -abs_offset: used offset between to offsetted curves + -offset_by_half: Whether the points of neighboring lines shall be interlaced or not + -replace_forbidden_points: Whether points marked as forbidden in must_use_points_deque shall be replaced by adjacend points + Output: + -List of tuples with the rastered point coordinates + -List which defines the point origin for each point according to the PointSource enum. + """ + + if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): + return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] + + deque_points = list(must_use_points_deque) + + linecoords = line.coords + + if start_distance > end_distance: + start_distance, end_distance = line.length - \ + start_distance, line.length-end_distance + linecoords = linecoords[::-1] + for i in range(len(deque_points)): + deque_points[i] = (deque_points[i][0], + line.length-deque_points[i][1]) + else: + # Since points with highest priority (=distance along line) are first (descending sorted) + deque_points = deque_points[::-1] + + # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] + while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): + deque_points.pop(0) + while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): + deque_points.pop() + + +# Ordering in priority queue: +# (point, LineStringSampling.PointSource), priority) + # might be different from line for stitching_direction=-1 + aligned_line = LineString(linecoords) + path_coords = substring(aligned_line, + start_distance, end_distance) + + # aligned line is a line without doubled points. + # I had the strange situation in which the offset "start_distance" from the line beginning + # resulted in a starting point which was already present in aligned_line causing a doubled point. + # A double point is not allowed in the following calculations so we need to remove it: + if (abs(path_coords.coords[0][0]-path_coords.coords[1][0]) < constants.eps and + abs(path_coords.coords[0][1]-path_coords.coords[1][1]) < constants.eps): + path_coords.coords = path_coords.coords[1:] + if (abs(path_coords.coords[-1][0]-path_coords.coords[-2][0]) < constants.eps and + abs(path_coords.coords[-1][1]-path_coords.coords[-2][1]) < constants.eps): + path_coords.coords = path_coords.coords[:-1] + + angles = calculate_line_angles(path_coords) + # For the first and last point we cannot calculate an angle. Set it to above the limit to make it a hard edge + angles[0] = 1.1*constants.limiting_angle + angles[-1] = 1.1*constants.limiting_angle + + current_distance = 0 + last_point = Point(path_coords.coords[0]) + # Next we merge the line points and the projected (deque) points into one list + merged_point_list = [] + dq_iter = 0 + for point, angle in zip(path_coords.coords, angles): + current_distance += last_point.distance(Point(point)) + last_point = Point(point) + while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance+start_distance: + # We want to avoid setting points at soft edges close to forbidden points + if deque_points[dq_iter][0].point_source == PointSource.FORBIDDEN_POINT: + # Check whether a previous added point is a soft edge close to the forbidden point + if (merged_point_list[-1][0].point_source == PointSource.SOFT_EDGE_INTERNAL and + abs(merged_point_list[-1][1]-deque_points[dq_iter][1]+start_distance < abs_offset*constants.factor_offset_forbidden_point)): + item = merged_point_list.pop() + merged_point_list.append((point_transfer.projected_point_tuple( + point=item[0].point, point_source=PointSource.FORBIDDEN_POINT), item[1]-start_distance)) + else: + merged_point_list.append( + (deque_points[dq_iter][0], deque_points[dq_iter][1]-start_distance)) + # merged_point_list.append(deque_points[dq_iter]) + dq_iter += 1 + # Check whether the current point is close to a forbidden point + if (dq_iter < len(deque_points) and + deque_points[dq_iter-1][0].point_source == PointSource.FORBIDDEN_POINT and + angle < constants.limiting_angle and + abs(deque_points[dq_iter-1][1]-current_distance-start_distance) < abs_offset*constants.factor_offset_forbidden_point): + point_source = PointSource.FORBIDDEN_POINT + else: + if angle < constants.limiting_angle: + point_source = PointSource.SOFT_EDGE_INTERNAL + else: + point_source = PointSource.HARD_EDGE_INTERNAL + merged_point_list.append((point_transfer.projected_point_tuple( + point=Point(point), point_source=point_source), current_distance)) + + result_list = [merged_point_list[0]] + + # General idea: Take one point of merged_point_list after another into the current segment until this segment is not simplified + # to a straight line by shapelys simplify method. + # Then, look at the points within this segment and choose the best fitting one + # (HARD_EDGE > OVERNEXT projected point > DIRECT projected point) as termination of this segment + # and start point for the next segment (so we do not always take the maximum possible length for a segment) + segment_start_index = 0 + segment_end_index = 1 + forbidden_point_list = [] + while segment_end_index < len(merged_point_list): + # Collection of points for the current segment + current_point_list = [merged_point_list[segment_start_index][0].point] + + while segment_end_index < len(merged_point_list): + segment_length = merged_point_list[segment_end_index][1] - \ + merged_point_list[segment_start_index][1] + if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: + new_distance = merged_point_list[segment_start_index][1] + \ + maxstitch_distance + merged_point_list.insert(segment_end_index, (point_transfer.projected_point_tuple( + point=aligned_line.interpolate(new_distance), point_source=PointSource.REGULAR_SPACING_INTERNAL), new_distance)) + segment_end_index += 1 + break + + current_point_list.append( + merged_point_list[segment_end_index][0].point) + simplified_len = len(LineString(current_point_list).simplify( + constants.factor_offset_remove_dense_points*abs_offset, preserve_topology=False).coords) + if simplified_len > 2: # not all points have been simplified - so we need to add it + break + + if merged_point_list[segment_end_index][0].point_source == PointSource.HARD_EDGE_INTERNAL: + segment_end_index += 1 + break + segment_end_index += 1 + + segment_end_index -= 1 + + # Now we choose the best fitting point within this segment + index_overnext = -1 + index_direct = -1 + index_hard_edge = -1 + + iter = segment_start_index+1 + while (iter <= segment_end_index): + if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: + index_overnext = iter + elif merged_point_list[iter][0].point_source == PointSource.DIRECT: + index_direct = iter + elif merged_point_list[iter][0].point_source == PointSource.HARD_EDGE_INTERNAL: + index_hard_edge = iter + iter += 1 + if index_hard_edge != -1: + segment_end_index = index_hard_edge + else: + if offset_by_half: + index_preferred = index_overnext + index_less_preferred = index_direct + else: + index_preferred = index_direct + index_less_preferred = index_overnext + + if index_preferred != -1: + if (index_less_preferred != -1 and index_less_preferred > index_preferred and + (merged_point_list[index_less_preferred][1]-merged_point_list[index_preferred][1]) >= + constants.factor_segment_length_direct_preferred_over_overnext * + (merged_point_list[index_preferred][1]-merged_point_list[segment_start_index][1])): + # We allow to take the direct projected point instead of the overnext projected point if it would result in a + # significant longer segment length + segment_end_index = index_less_preferred + else: + segment_end_index = index_preferred + elif index_less_preferred != -1: + segment_end_index = index_less_preferred + + # Usually OVERNEXT and DIRECT points are close to each other and in some cases both were selected as segment edges + # If they are too close ( constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal: + new_point_left_proj = result_list[index][1]-distance_left + if new_point_left_proj < 0: + new_point_left_proj += line.length + new_point_right_proj = result_list[index][1]+distance_right + if new_point_right_proj > line.length: + new_point_right_proj -= line.length + point_left = line.interpolate(new_point_left_proj) + point_right = line.interpolate(new_point_right_proj) + forbidden_point_distance = result_list[index][0].point.distance( + LineString([point_left, point_right])) + if forbidden_point_distance < constants.factor_offset_remove_dense_points*abs_offset: + del result_list[index] + result_list.insert(index, (point_transfer.projected_point_tuple( + point=point_right, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_right_proj)) + result_list.insert(index, (point_transfer.projected_point_tuple( + point=point_left, point_source=PointSource.REPLACED_FORBIDDEN_POINT), new_point_left_proj)) + current_index_shift += 1 + break + else: + distance_left /= 2.0 + distance_right /= 2.0 + return result_list diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py new file mode 100644 index 00000000..af14ea0f --- /dev/null +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -0,0 +1,330 @@ +from shapely.geometry.polygon import LinearRing, LineString +from shapely.geometry import Polygon, MultiLineString +from shapely.ops import polygonize +from shapely.geometry import MultiPolygon +from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter +from shapely.geometry.polygon import orient +from depq import DEPQ +from enum import IntEnum +from ..stitches import tangential_fill_stitch_pattern_creator +from ..stitches import constants + + +def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit): + """ + Solves following problem: When shapely offsets a LinearRing the + start/end point might be handled wrongly since they + are only treated as LineString. + (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example) + This method checks first whether the start/end point form a problematic + edge with respect to the offset side. If it is not a problematic + edge we can use the normal offset_routine. Otherwise we need to + perform two offsets: + -offset the ring + -offset the start/end point + its two neighbors left and right + Finally both offsets are merged together to get the correct + offset of a LinearRing + """ + + coords = ring.coords[:] + # check whether edge at index 0 is concave or convex. Only for + # concave edges we need to spend additional effort + dx_seg1 = dy_seg1 = 0 + if coords[0] != coords[-1]: + dx_seg1 = coords[0][0] - coords[-1][0] + dy_seg1 = coords[0][1] - coords[-1][1] + else: + dx_seg1 = coords[0][0] - coords[-2][0] + dy_seg1 = coords[0][1] - coords[-2][1] + dx_seg2 = coords[1][0] - coords[0][0] + dy_seg2 = coords[1][1] - coords[0][1] + # use cross product: + crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 + sidesign = 1 + if side == "left": + sidesign = -1 + + # We do not need to take care of the joint n-0 since we + # offset along a concave edge: + if sidesign * offset * crossvalue <= 0: + return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) + + # We offset along a convex edge so we offset the joint n-0 separately: + if coords[0] != coords[-1]: + coords.append(coords[0]) + offset_ring1 = ring.parallel_offset( + offset, side, resolution, join_style, mitre_limit + ) + offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( + offset, side, resolution, join_style, mitre_limit + ) + + # Next we need to merge the results: + if offset_ring1.geom_type == "LineString": + return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) + else: + # We have more than one resulting LineString for offset of + # the geometry (ring) = offset_ring1. + # Hence we need to find the LineString which belongs to the + # offset of element 0 in coords =offset_ring2 + # in order to add offset_ring2 geometry to it: + result_list = [] + thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) + for offsets in offset_ring1: + if ( + abs(offsets.coords[0][0] - coords[0][0]) < thresh + and abs(offsets.coords[0][1] - coords[0][1]) < thresh + ): + result_list.append( + LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) + ) + else: + result_list.append(LinearRing(offsets)) + return MultiLineString(result_list) + + +def take_only_valid_linear_rings(rings): + """ + Removes all geometries which do not form a "valid" LinearRing + (meaning a ring which does not form a straight line) + """ + if rings.geom_type == "MultiLineString": + new_list = [] + for ring in rings: + if len(ring.coords) > 3 or ( + len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1] + ): + new_list.append(ring) + if len(new_list) == 1: + return LinearRing(new_list[0]) + else: + return MultiLineString(new_list) + else: + if len(rings.coords) <= 2: + return LinearRing() + elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]: + return LinearRing() + else: + return rings + + +def make_tree_uniform_ccw(root): + """ + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree "root" uniform (having all the same + ordering direction) + """ + for node in PreOrderIter(root): + if node.id == "hole": + node.val.coords = list(node.val.coords)[::-1] + + +# Used to define which stitching strategy shall be used +class StitchingStrategy(IntEnum): + CLOSEST_POINT = 0 + INNER_TO_OUTER = 1 + SPIRAL = 2 + + +def check_and_prepare_tree_for_valid_spiral(root): + """ + Takes a tree consisting of offsetted curves. If a parent has more than one child we + cannot create a spiral. However, to make the routine more robust, we allow more than + one child if only one of the childs has own childs. The other childs are removed in this + routine then. If the routine returns true, the tree will have been cleaned up from unwanted + childs. If the routine returns false even under the mentioned weaker conditions the + tree cannot be connected by one spiral. + """ + for children in LevelOrderGroupIter(root): + if len(children) > 1: + count = 0 + child_with_children = None + for child in children: + if not child.is_leaf: + count += 1 + child_with_children = child + if count > 1: + return False + elif count == 1: + child_with_children.parent.children = [child_with_children] + else: # count == 0 means all childs have no children so we take only the longest child + max_length = 0 + longest_child = None + for child in children: + if child.val.length > max_length: + max_length = child.val.length + longest_child = child + longest_child.parent.children = [longest_child] + return True + + +def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 + """ + Takes a polygon (which can have holes) as input and creates offsetted + versions until the polygon is filled with these smaller offsets. + These created geometries are afterwards connected to each other and + resampled with a maximum stitch_distance. + The return value is a LineString which should cover the full polygon. + Input: + -poly: The shapely polygon which can have holes + -offset: The used offset for the curves + -join_style: Join style for the offset - can be round, mitered or bevel + (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE) + For examples look at + https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png + -stitch_distance maximum allowed stitch distance between two points + -offset_by_half: True if the points shall be interlaced + -strategy: According to StitchingStrategy enum class you can select between + different strategies for the connection between parent and childs. In + addition it offers the option "SPIRAL" which creates a real spiral towards inner. + In contrast to the other two options, "SPIRAL" does not end at the starting point + but at the innermost point + -starting_point: Defines the starting point for the stitching + Output: + -List of point coordinate tuples + -Tag (origin) of each point to analyze why a point was placed + at this position + """ + + if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: + raise ValueError( + "Single spiral geometry must not have more than one hole!") + + ordered_poly = orient(poly, -1) + ordered_poly = ordered_poly.simplify( + constants.simplification_threshold, False) + root = AnyNode( + id="node", + val=ordered_poly.exterior, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) + active_polys = [root] + active_holes = [[]] + + for holes in ordered_poly.interiors: + active_holes[0].append( + AnyNode( + id="hole", + val=holes, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None), + ) + ) + + while len(active_polys) > 0: + current_poly = active_polys.pop() + current_holes = active_holes.pop() + poly_inners = [] + + outer = offset_linear_ring( + current_poly.val, + offset, + "left", + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + outer = outer.simplify(constants.simplification_threshold, False) + outer = take_only_valid_linear_rings(outer) + + for j in range(len(current_holes)): + inner = offset_linear_ring( + current_holes[j].val, + offset, + "left", + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + inner = inner.simplify(constants.simplification_threshold, False) + inner = take_only_valid_linear_rings(inner) + if not inner.is_empty: + poly_inners.append(Polygon(inner)) + if not outer.is_empty: + if len(poly_inners) == 0: + if outer.geom_type == "LineString": + result = Polygon(outer) + else: + result = MultiPolygon(polygonize(outer)) + else: + if outer.geom_type == "LineString": + result = Polygon(outer).difference( + MultiPolygon(poly_inners)) + else: + result = MultiPolygon(outer).difference( + MultiPolygon(poly_inners)) + + if not result.is_empty and result.area > offset * offset / 10: + result_list = [] + if result.geom_type == "Polygon": + result_list = [result] + else: + result_list = list(result) + + for polygon in result_list: + polygon = orient(polygon, -1) + + if polygon.area < offset * offset / 10: + continue + + polygon = polygon.simplify( + constants.simplification_threshold, False + ) + poly_coords = polygon.exterior + poly_coords = take_only_valid_linear_rings(poly_coords) + if poly_coords.is_empty: + continue + + node = AnyNode( + id="node", + parent=current_poly, + val=poly_coords, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) + active_polys.append(node) + hole_node_list = [] + for hole in polygon.interiors: + hole_node = AnyNode( + id="hole", + val=hole, + already_rastered=False, + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None + ), + ) + for previous_hole in current_holes: + if Polygon(hole).contains(Polygon(previous_hole.val)): + previous_hole.parent = hole_node + hole_node_list.append(hole_node) + active_holes.append(hole_node_list) + for previous_hole in current_holes: + # If the previous holes are not + # contained in the new holes they + # have been merged with the + # outer polygon + if previous_hole.parent is None: + previous_hole.parent = current_poly + + + make_tree_uniform_ccw(root) + + if strategy == StitchingStrategy.CLOSEST_POINT: + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_nearest_neighbor( + root, offset, stitch_distance, starting_point, offset_by_half) + elif strategy == StitchingStrategy.INNER_TO_OUTER: + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( + root, offset, stitch_distance, starting_point, offset_by_half) + elif strategy == StitchingStrategy.SPIRAL: + if not check_and_prepare_tree_for_valid_spiral(root): + raise ValueError("Geometry cannot be filled with one spiral!") + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_spiral( + root, offset, stitch_distance, starting_point, offset_by_half) + else: + raise ValueError("Invalid stitching stratety!") + + return connected_line, connected_line_origin diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py new file mode 100644 index 00000000..d7afad0c --- /dev/null +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -0,0 +1,906 @@ +from shapely.geometry.polygon import LineString, LinearRing +from shapely.geometry import Point, MultiPoint +from shapely.ops import nearest_points +from collections import namedtuple +from depq import DEPQ +import trimesh +import numpy as np +from scipy import spatial +import math +from anytree import PreOrderIter +from ..stitches import sample_linestring +from ..stitches import point_transfer +from ..stitches import constants + +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) + + +def cut(line, distance): + """ + Cuts a closed line so that the new closed line starts at the + point with "distance" to the beginning of the old line. + """ + if distance <= 0.0 or distance >= line.length: + return [LineString(line)] + coords = list(line.coords) + for i, p in enumerate(coords): + if i > 0 and p == coords[0]: + pd = line.length + else: + pd = line.project(Point(p)) + if pd == distance: + if coords[0] == coords[-1]: + return LineString(coords[i:] + coords[1: i + 1]) + else: + return LineString(coords[i:] + coords[:i]) + if pd > distance: + cp = line.interpolate(distance) + if coords[0] == coords[-1]: + return LineString( + [(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)] + ) + else: + return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) + + +def connect_raster_tree_nearest_neighbor( # noqa: C901 + tree, used_offset, stitch_distance, close_point, offset_by_half): + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made where both curves + come closest together. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ + + current_coords = tree.val + abs_offset = abs(used_offset) + result_coords = [] + result_coords_origin = [] + + # We cut the current item so that its index 0 is closest to close_point + start_distance = tree.val.project(close_point) + if start_distance > 0: + current_coords = cut(current_coords, start_distance) + tree.val = current_coords + + if not tree.transferred_point_priority_deque.is_empty(): + new_DEPQ = DEPQ(iterable=None, maxlen=None) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) + tree.transferred_point_priority_deque = new_DEPQ + + stitching_direction = 1 + # This list should contain a tuple of nearest points between + # the current geometry and the subgeometry, the projected + # distance along the current geometry, and the belonging subtree node + nearest_points_list = [] + + for subnode in tree.children: + point_parent, point_child = nearest_points(current_coords, subnode.val) + proj_distance = current_coords.project(point_parent) + nearest_points_list.append( + nearest_neighbor_tuple( + nearest_point_parent=point_parent, + nearest_point_child=point_child, + proj_distance_parent=proj_distance, + child_node=subnode) + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + if nearest_points_list: + start_distance = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_distance = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) + else: + start_distance = abs_offset * constants.factor_offset_starting_points + end_distance = (current_coords.length - abs_offset * constants.factor_offset_starting_points) + + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + start_distance, # We add/subtract an offset to not sample + # the same point again (avoid double + # points for start and end) + end_distance, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + assert len(own_coords) == len(own_coords_origin) + own_coords_origin[0] = sample_linestring.PointSource.ENTER_LEAVING_POINT + own_coords_origin[-1] = sample_linestring.PointSource.ENTER_LEAVING_POINT + tree.stitching_direction = stitching_direction + tree.already_rastered = True + + # Next we need to transfer our rastered points to siblings and childs + to_transfer_point_list = [] + to_transfer_point_list_origin = [] + for k in range(1, len(own_coords) - 1): + # Do not take the first and the last since they are ENTER_LEAVING_POINT + # points for sure + + if (not offset_by_half and own_coords_origin[k] == sample_linestring.PointSource.EDGE_NEEDED): + continue + if (own_coords_origin[k] == sample_linestring.PointSource.ENTER_LEAVING_POINT or + own_coords_origin[k] == sample_linestring.PointSource.FORBIDDEN_POINT): + continue + to_transfer_point_list.append(Point(own_coords[k])) + point_origin = own_coords_origin[k] + to_transfer_point_list_origin.append(point_origin) + + # Since the projection is only in ccw direction towards inner we need + # to use "-used_offset" for stitching_direction==-1 + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + if not nearest_points_list: + # If there is no child (inner geometry) we can simply take + # our own rastered coords as result + result_coords = own_coords + result_coords_origin = own_coords_origin + else: + # There are childs so we need to merge their coordinates + + # with our own rastered coords + + # To create a closed ring + own_coords.append(own_coords[0]) + own_coords_origin.append(own_coords_origin[0]) + + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) + total_distance = start_distance + cur_item = 0 + result_coords = [own_coords[0]] + result_coords_origin = [ + sample_linestring.PointSource.ENTER_LEAVING_POINT] + for i in range(1, len(own_coords)): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + + item = nearest_points_list[cur_item] + (child_coords, child_coords_origin) = connect_raster_tree_nearest_neighbor( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + d = item.nearest_point_parent.distance( + Point(own_coords[i - 1])) + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append(item.nearest_point_parent.coords[0]) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + # reversing avoids crossing when entering and + # leaving the child segment + result_coords.extend(child_coords[::-1]) + result_coords_origin.extend(child_coords_origin[::-1]) + + # And here we calculate the point for the leaving + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs(nearest_points_list[cur_item+1].proj_distance_parent-item.proj_distance_parent) + ) + + if d > abs_offset * constants.factor_offset_starting_points: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + abs_offset * constants.factor_offset_starting_points + ).coords[0] + ) + result_coords_origin.append(sample_linestring.PointSource.ENTER_LEAVING_POINT) + + cur_item += 1 + if i < len(own_coords) - 1: + if (Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset * constants.factor_offset_remove_points): + result_coords.append(own_coords[i]) + result_coords_origin.append(own_coords_origin[i]) + + # Since current_coords and temp are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in temp would be very time + # consuming we project only every n-th point which resets + # the accumulated error every n-th point. + if i % 20 == 0: + total_distance = current_coords.project(Point(own_coords[i])) + else: + total_distance += next_distance + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin + + +def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): + """ + Takes a line and calculates the nearest distance along this + line to enter the next_line + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter next_line + -next_line: contains the next_line which need to be entered + -thresh: The distance between travel_line and next_line needs + to below thresh to be a valid point for entering + Output: + -tuple - the tuple structure is: + (nearest point in travel_line, nearest point in next_line) + """ + point_list = list(MultiPoint(travel_line.coords)) + + if point_list[0].distance(next_line) < thresh: + return nearest_points(point_list[0], next_line) + + for i in range(len(point_list) - 1): + line_segment = LineString([point_list[i], point_list[i + 1]]) + result = nearest_points(line_segment, next_line) + + if result[0].distance(result[1]) < thresh: + return result + line_segment = LineString([point_list[-1], point_list[0]]) + result = nearest_points(line_segment, next_line) + + if result[0].distance(result[1]) < thresh: + return result + else: + return None + + +def create_nearest_points_list( + travel_line, children_list, threshold, threshold_hard, preferred_direction=0): + """ + Takes a line and calculates the nearest distance along this line to + enter the childs in children_list + The method calculates the distances along the line and along the + reversed line to find the best direction which minimizes the overall + distance for all childs. + Input: + -travel_line: The "parent" line for which the distance should + be minimized to enter the childs + -children_list: contains the childs of travel_line which need to be entered + -threshold: The distance between travel_line and a child needs to be + below threshold to be a valid point for entering + -preferred_direction: Put a bias on the desired travel direction along + travel_line. If equals zero no bias is applied. + preferred_direction=1 means we prefer the direction of travel_line; + preferred_direction=-1 means we prefer the opposite direction. + Output: + -stitching direction for travel_line + -list of tuples (one tuple per child). The tuple structure is: + ((nearest point in travel_line, nearest point in child), + distance along travel_line, belonging child) + """ + + result_list_in_order = [] + result_list_reversed_order = [] + + travel_line_reversed = LinearRing(travel_line.coords[::-1]) + + weight_in_order = 0 + weight_reversed_order = 0 + for child in children_list: + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line, child.val, threshold_hard + ) + assert result is not None + proj = travel_line.project(result[0]) + weight_in_order += proj + result_list_in_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold + ) + if result is None: + # where holes meet outer borders a distance + # up to 2*used offset can arise + result = get_nearest_points_closer_than_thresh( + travel_line_reversed, child.val, threshold_hard + ) + assert result is not None + proj = travel_line_reversed.project(result[0]) + weight_reversed_order += proj + result_list_reversed_order.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + if preferred_direction == 1: + # Reduce weight_in_order to make in order stitching more preferred + weight_in_order = min( + weight_in_order / 2, max(0, weight_in_order - 10 * threshold) + ) + if weight_in_order == weight_reversed_order: + return (1, result_list_in_order) + elif preferred_direction == -1: + # Reduce weight_reversed_order to make reversed + # stitching more preferred + weight_reversed_order = min( + weight_reversed_order / + 2, max(0, weight_reversed_order - 10 * threshold) + ) + if weight_in_order == weight_reversed_order: + return (-1, result_list_reversed_order) + + if weight_in_order < weight_reversed_order: + return (1, result_list_in_order) + else: + return (-1, result_list_reversed_order) + + +def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): + """ + Takes a line segment (consisting of 3 points!) + and calculates a new middle point if the line_segment is + straight enough to be resampled by points max_stitch_distance apart FROM THE END OF line_segment. + Returns None if the middle point is not needed. + """ + angles = sample_linestring.calculate_line_angles(line_segment) + if angles[1] < abs_offset * constants.limiting_angle_straight: + if line_segment.length < max_stitch_distance: + return None + else: + return line_segment.interpolate(line_segment.length - max_stitch_distance).coords[0] + else: + return line_segment.coords[1] + + +def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 + """ + Takes the offsetted curves organized as tree, connects and samples them. + Strategy: A connection from parent to child is made as fast as possible to + reach the innermost child as fast as possible in order to stitch afterwards + from inner to outer. + Input: + -tree: contains the offsetted curves in a hierachical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one line and sampled with points obeying + stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was placed + at this position + """ + + current_coords = tree.val + abs_offset = abs(used_offset) + result_coords = [] + result_coords_origin = [] + + start_distance = tree.val.project(close_point) + # We cut the current path so that its index 0 is closest to close_point + if start_distance > 0: + current_coords = cut(current_coords, start_distance) + tree.val = current_coords + + if not tree.transferred_point_priority_deque.is_empty(): + new_DEPQ = DEPQ(iterable=None, maxlen=None) + for item, priority in tree.transferred_point_priority_deque: + new_DEPQ.insert( + item, + math.fmod( + priority - start_distance + current_coords.length, + current_coords.length, + ), + ) + tree.transferred_point_priority_deque = new_DEPQ + + # We try to use always the opposite stitching direction with respect to the + # parent to avoid crossings when entering and leaving the child + parent_stitching_direction = -1 + if tree.parent is not None: + parent_stitching_direction = tree.parent.stitching_direction + + # Find the nearest point in current_coords and its children and + # sort it along the stitching direction + stitching_direction, nearest_points_list = create_nearest_points_list( + current_coords, + tree.children, + constants.offset_factor_for_adjacent_geometry * abs_offset, + 2.05 * abs_offset, + parent_stitching_direction, + ) + nearest_points_list.sort( + reverse=False, key=lambda tup: tup.proj_distance_parent) + + # Have a small offset for the starting and ending to avoid double points + # at start and end point (since the paths are closed rings) + if nearest_points_list: + start_offset = min( + abs_offset * constants.factor_offset_starting_points, + nearest_points_list[0].proj_distance_parent, + ) + end_offset = max( + current_coords.length + - abs_offset * constants.factor_offset_starting_points, + nearest_points_list[-1].proj_distance_parent, + ) + else: + start_offset = abs_offset * constants.factor_offset_starting_points + end_offset = (current_coords.length - abs_offset * constants.factor_offset_starting_points) + + if stitching_direction == 1: + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + start_offset, # We add start_offset to not sample the same + # point again (avoid double points for start + # and end) + end_offset, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False + ) + else: + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_coords, + current_coords.length - start_offset, # We subtract + # start_offset to not + # sample the same point + # again (avoid double + # points for start + # and end) + current_coords.length - end_offset, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False + ) + current_coords.coords = current_coords.coords[::-1] + + assert len(own_coords) == len(own_coords_origin) + + tree.stitching_direction = stitching_direction + tree.already_rastered = True + + to_transfer_point_list = [] + to_transfer_point_list_origin = [] + for k in range(0, len(own_coords)): + # TODO: maybe do not take the first and the last + # since they are ENTER_LEAVING_POINT points for sure + if ( + not offset_by_half + and own_coords_origin[k] == sample_linestring.PointSource.EDGE_NEEDED + or own_coords_origin[k] == sample_linestring.PointSource.FORBIDDEN_POINT): + continue + if own_coords_origin[k] == sample_linestring.PointSource.ENTER_LEAVING_POINT: + continue + to_transfer_point_list.append(Point(own_coords[k])) + to_transfer_point_list_origin.append(own_coords_origin[k]) + + assert len(to_transfer_point_list) == len(to_transfer_point_list_origin) + + # Next we need to transfer our rastered points to siblings and childs + # Since the projection is only in ccw direction towards inner we + # need to use "-used_offset" for stitching_direction==-1 + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + offset_by_half, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + tree, + stitching_direction * used_offset, + False, + to_transfer_point_list, + to_transfer_point_list_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=True, + transfer_to_child=True, + ) + + if not nearest_points_list: + # If there is no child (inner geometry) we can simply + # take our own rastered coords as result + result_coords = own_coords + result_coords_origin = own_coords_origin + else: + # There are childs so we need to merge their coordinates + # with our own rastered coords + + # Create a closed ring for the following code + own_coords.append(own_coords[0]) + own_coords_origin.append(own_coords_origin[0]) + + # own_coords does not start with current_coords but has an offset + # (see call of raster_line_string_with_priority_points) + total_distance = start_offset + + cur_item = 0 + result_coords = [own_coords[0]] + result_coords_origin = [own_coords_origin[0]] + + for i in range(1, len(own_coords)): + next_distance = math.sqrt( + (own_coords[i][0] - own_coords[i - 1][0]) ** 2 + + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 + ) + while ( + cur_item < len(nearest_points_list) + and total_distance + next_distance + constants.eps + > nearest_points_list[cur_item].proj_distance_parent + ): + # The current and the next point in own_coords enclose the + # nearest point tuple between this geometry and child + # geometry. Hence we need to insert the child geometry points + # here before the next point of own_coords. + item = nearest_points_list[cur_item] + ( + child_coords, + child_coords_origin, + ) = connect_raster_tree_from_inner_to_outer( + item.child_node, + used_offset, + stitch_distance, + item.nearest_point_child, + offset_by_half, + ) + + # Imagine the nearest point of the child is within a long + # segment of the parent. Without additonal points + # on the parent side this would cause noticeable deviations. + # Hence we add here points shortly before and after + # the entering of the child to have only minor deviations to + # the desired shape. + # Here is the point for the entering: + if (Point(result_coords[-1]).distance(item.nearest_point_parent) > constants.factor_offset_starting_points * abs_offset): + result_coords.append(item.nearest_point_parent.coords[0]) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + + # Check whether the number of points of the connecting lines + # from child to child can be reduced + if len(child_coords) > 1: + point = calculate_replacing_middle_point( + LineString( + [result_coords[-1], child_coords[0], child_coords[1]] + ), + abs_offset, + stitch_distance, + ) + + if point is not None: + result_coords.append(point) + result_coords_origin.append(child_coords_origin[0]) + + result_coords.extend(child_coords[1:]) + result_coords_origin.extend(child_coords_origin[1:]) + else: + result_coords.extend(child_coords) + result_coords_origin.extend(child_coords_origin) + + # And here is the point for the leaving of the child + # (distance to the own following point should not be too large) + d = item.nearest_point_parent.distance(Point(own_coords[i])) + if cur_item < len(nearest_points_list) - 1: + d = min( + d, + abs( + nearest_points_list[cur_item + + 1].proj_distance_parent + - item.proj_distance_parent + ), + ) + + if d > constants.factor_offset_starting_points * abs_offset: + result_coords.append( + current_coords.interpolate( + item.proj_distance_parent + + 2 * constants.factor_offset_starting_points * abs_offset + ).coords[0] + ) + result_coords_origin.append( + sample_linestring.PointSource.ENTER_LEAVING_POINT + ) + # Check whether this additional point makes the last point + # of the child unnecessary + point = calculate_replacing_middle_point( + LineString( + [result_coords[-3], result_coords[-2], result_coords[-1]] + ), + abs_offset, + stitch_distance, + ) + if point is None: + result_coords.pop(-2) + result_coords_origin.pop(-2) + + cur_item += 1 + if i < len(own_coords) - 1: + if (Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset * constants.factor_offset_remove_points): + result_coords.append(own_coords[i]) + result_coords_origin.append(own_coords_origin[i]) + + # Since current_coords and own_coords are rastered differently + # there accumulate errors regarding the current distance. + # Since a projection of each point in own_coords would be very + # time consuming we project only every n-th point which resets + # the accumulated error every n-th point. + if i % 20 == 0: + total_distance = current_coords.project(Point(own_coords[i])) + else: + total_distance += next_distance + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin + + +# Partly taken from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py +def interpolate_LinearRings(a, b, start=None, step=.005): + """ + Interpolate between two LinearRings + Parameters + ------------- + a : shapely.geometry.Polygon.LinearRing + LinearRing start point will lie on + b : shapely.geometry.Polygon.LinearRing + LinearRing end point will lie on + start : (2,) float, or None + Point to start at + step : float + How far apart should points on + the path be. + Returns + ------------- + path : (n, 2) float + Path interpolated between two LinearRings + """ + + # resample the first LinearRing so every sample is spaced evenly + ra = trimesh.path.traversal.resample_path( + a, step=step) + if not a.is_ccw: + ra = ra[::-1] + + assert trimesh.path.util.is_ccw(ra) + if start is not None: + # find the closest index on LinerRing 'a' + # by creating a KDTree + tree_a = spatial.cKDTree(ra) + index = tree_a.query(start)[1] + ra = np.roll(ra, -index, axis=0) + + # resample the second LinearRing for even spacing + rb = trimesh.path.traversal.resample_path(b, + step=step) + if not b.is_ccw: + rb = rb[::-1] + + # we want points on 'b' that correspond index- wise + # the resampled points on 'a' + tree_b = spatial.cKDTree(rb) + # points on b with corresponding indexes to ra + pb = rb[tree_b.query(ra)[1]] + + # linearly interpolate between 'a' and 'b' + weights = np.linspace(0.0, 1.0, len(ra)).reshape((-1, 1)) + + # start on 'a' and end on 'b' + points = (ra * (1.0 - weights)) + (pb * weights) + + result = LineString(points) + + return result.simplify(constants.simplification_threshold, False) + + +def connect_raster_tree_spiral( + tree, used_offset, stitch_distance, close_point, offset_by_half): + """ + Takes the offsetted curves organized as tree, connects and samples them as a spiral. + It expects that each node in the tree has max. one child + Input: + -tree: contains the offsetted curves in a hierarchical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -close_point: defines the beginning point for stitching + (stitching starts always from the undisplaced curve) + -offset_by_half: If true the resulting points are interlaced otherwise not. + Returnvalues: + -All offsetted curves connected to one spiral and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ + + abs_offset = abs(used_offset) + if tree.is_leaf: + return sample_linestring.raster_line_string_with_priority_points( + tree.val, + 0, + tree.val.length, + stitch_distance, + tree.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + result_coords = [] + result_coords_origin = [] + starting_point = close_point.coords[0] + # iterate to the second last level + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): + ring1 = node.val + ring2 = node.children[0].val + + part_spiral = interpolate_LinearRings( + ring1, ring2, starting_point) + node.val = part_spiral + + for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + node.val, + 0, + node.val.length, + stitch_distance, + node.transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) + + point_transfer.transfer_points_to_surrounding( + node, + -used_offset, + offset_by_half, + own_coords, + own_coords_origin, + overnext_neighbor=False, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + # We transfer also to the overnext child to get a more straight + # arrangement of points perpendicular to the stitching lines + if offset_by_half: + point_transfer.transfer_points_to_surrounding( + node, + -used_offset, + False, + own_coords, + own_coords_origin, + overnext_neighbor=True, + transfer_forbidden_points=False, + transfer_to_parent=False, + transfer_to_sibling=False, + transfer_to_child=True) + + # Check whether starting of own_coords or end of result_coords can be removed + if not result_coords: + result_coords.extend(own_coords) + result_coords_origin.extend(own_coords_origin) + elif len(own_coords) > 0: + if Point(result_coords[-1]).distance(Point(own_coords[0])) > constants.line_lengh_seen_as_one_point: + lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) + else: + lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[1]]) + (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, + DEPQ(), abs_offset, offset_by_half, False) + if len(temp_coords) == 2: # only start and end point of lineseg was needed + result_coords.pop() + result_coords_origin.pop() + result_coords.extend(own_coords[1:]) + result_coords_origin.extend(own_coords_origin[1:]) + elif len(temp_coords) == 3: # one middle point within lineseg was needed + result_coords.pop() + result_coords.append(temp_coords[1]) + result_coords.extend(own_coords[1:]) + result_coords_origin.extend(own_coords_origin[1:]) + else: # all points were needed + result_coords.extend(own_coords) + result_coords_origin.extend(own_coords_origin) + # make sure the next section starts where this + # section of the curve ends + starting_point = result_coords[-1] + + assert len(result_coords) == len(result_coords_origin) + return result_coords, result_coords_origin -- cgit v1.3.1 From 515ed3ea2fc8357482527d6e4a170db154baa205 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Fri, 18 Feb 2022 15:36:01 +0100 Subject: separate guided fill methods --- lib/elements/fill_stitch.py | 14 +- lib/stitches/__init__.py | 1 + lib/stitches/auto_fill.py | 114 +++--------- lib/stitches/fill.py | 117 +------------ lib/stitches/guided_fill.py | 355 ++++++++++++++++++++++++++++++++++++++ lib/stitches/point_transfer.py | 10 +- lib/stitches/sample_linestring.py | 12 +- 7 files changed, 395 insertions(+), 228 deletions(-) create mode 100644 lib/stitches/guided_fill.py (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 3256c1ea..eaddcfe0 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,7 +15,7 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill +from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import Point as InkstitchPoint @@ -45,7 +45,7 @@ class UnderlayInsetWarning(ValidationWarning): class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _('This object is set to "Guided AutoFill", but has no guide line.') + description = _('This object is set to "Guided Fill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') @@ -97,7 +97,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) + options=[_("Auto Fill"), _("Tangential"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @@ -514,7 +514,6 @@ class FillStitch(EmbroideryElement): tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( self.underlay_shape, - None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, self.fill_underlay_row_spacing, @@ -535,7 +534,6 @@ class FillStitch(EmbroideryElement): tags=("auto_fill", "auto_fill_top"), stitches=auto_fill( self.fill_shape, - None, self.angle, self.row_spacing, self.end_row_spacing, @@ -580,16 +578,14 @@ class FillStitch(EmbroideryElement): stitch_group = StitchGroup( color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( + tags=("guided_fill", "auto_fill_top"), + stitches=guided_fill( self.fill_shape, guide_line.geoms[0], self.angle, self.row_spacing, - self.end_row_spacing, self.max_stitch_length, self.running_stitch_length, - 0, self.skip_last, starting_point, ending_point, diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py index 4de88733..8b2738bc 100644 --- a/lib/stitches/__init__.py +++ b/lib/stitches/__init__.py @@ -5,6 +5,7 @@ from .auto_fill import auto_fill from .fill import legacy_fill +from .guided_fill import guided_fill from .running_stitch import * # Can't put this here because we get a circular import :( diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 7af99560..52dc6a81 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -12,16 +12,14 @@ import networkx from shapely import geometry as shgeo from shapely.ops import snap from shapely.strtree import STRtree -from depq import DEPQ + from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import line_string_to_point_list -from .fill import intersect_region_with_grating, intersect_region_with_grating_line, stitch_row +from .fill import intersect_region_with_grating, stitch_row from .running_stitch import running_stitch -from .point_transfer import transfer_points_to_surrounding_graph -from .sample_linestring import raster_line_string_with_priority_points class PathEdge(object): @@ -51,7 +49,6 @@ class PathEdge(object): @debug.time def auto_fill(shape, - line, angle, row_spacing, end_row_spacing, @@ -61,14 +58,10 @@ def auto_fill(shape, skip_last, starting_point, ending_point=None, - underpath=True, - offset_by_half=True): - # offset_by_half only relevant for line != None; staggers only relevant for line == None! - + underpath=True): 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, 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) @@ -81,7 +74,7 @@ def auto_fill(shape, 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 is not None, offset_by_half) + max_stitch_length, running_stitch_length, staggers, skip_last) return result @@ -116,7 +109,7 @@ def project(shape, coords, outline_index): @debug.time -def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None): +def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None): """build a graph representation of the grating segments This function builds a specialized graph (as in graph theory) that will @@ -151,37 +144,18 @@ def build_fill_stitch_graph(shape, line, angle, row_spacing, end_row_spacing, st debug.add_layer("auto-fill fill stitch") - 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) - else: - 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] + # Convert the shape into a set of parallel line segments. + rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + 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 - # of the endpoints as nodes, which networkx will add automatically. - - # 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[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) + # First, add the grating segments as edges. We'll use the coordinates + # of the endpoints as nodes, which networkx will add automatically. + for segment in segments: + # 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=[]) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -360,8 +334,7 @@ def get_segments(graph): segments = [] 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 @@ -400,10 +373,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): # segments that _might_ intersect ls. Refining the result is # necessary but the STRTree still saves us a ton of time. if segment.crosses(ls): - start = segment.coords[0] - end = segment.coords[-1] - fill_stitch_graph[start][end]['segment']['underpath_edges'].append( - edge) + start, end = segment.coords + 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) @@ -661,30 +632,8 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): return stitches[1:] -def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): - if stitching_direction == 1: - stitched_line, _ = raster_line_string_with_priority_points( - geometry, 0.0, geometry.length, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) - else: - stitched_line, _ = raster_line_string_with_priority_points( - geometry, geometry.length, 0.0, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) - - stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) - for i in range(1, len(stitched_line)-1): - 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',))) - else: - 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): path = collapse_sequential_outline_edges(path) stitches = [] @@ -695,29 +644,8 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, for edge in path: if edge.is_segment(): - if offsetted_line: - new_stitches = [] - current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment'] - path_geometry = current_edge['geometry'] - 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])): - stitching_direction = -1 - 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) - - 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)) diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index b5f86641..1fdc6fac 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -6,8 +6,8 @@ import math import shapely -from shapely.geometry.linestring import LineString -from shapely.ops import linemerge, unary_union + +from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache @@ -94,119 +94,6 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge stitches.append(end) -def extend_line(line, minx, maxx, miny, maxy): - line = line.simplify(0.01, False) - - upper_left = InkstitchPoint(minx, miny) - lower_right = InkstitchPoint(maxx, maxy) - length = (upper_left - lower_right).length() - - point1 = InkstitchPoint(*line.coords[0]) - point2 = InkstitchPoint(*line.coords[1]) - new_starting_point = point1-(point2-point1).unit()*length - - point3 = InkstitchPoint(*line.coords[-2]) - point4 = InkstitchPoint(*line.coords[-1]) - new_ending_point = point4+(point4-point3).unit()*length - - return LineString([new_starting_point.as_tuple()] + - line.coords[1:-1]+[new_ending_point.as_tuple()]) - - -def repair_multiple_parallel_offset_curves(multi_line): - lines = linemerge(multi_line) - lines = list(multi_line.geoms) - max_length = -1 - max_length_idx = -1 - for idx, subline in enumerate(lines): - if subline.length > max_length: - max_length = subline.length - max_length_idx = idx - # need simplify to avoid doubled points caused by linemerge - return lines[max_length_idx].simplify(0.01, False) - - -def repair_non_simple_lines(line): - repaired = unary_union(line) - counter = 0 - # Do several iterations since we might have several concatenated selfcrossings - while repaired.geom_type != 'LineString' and counter < 4: - line_segments = [] - for line_seg in repaired.geoms: - if not line_seg.is_ring: - line_segments.append(line_seg) - - repaired = unary_union(linemerge(line_segments)) - counter += 1 - if repaired.geom_type != 'LineString': - raise ValueError( - "Guide line (or offsetted instance) is self crossing!") - else: - return repaired - - -def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): # noqa: C901 - - row_spacing = abs(row_spacing) - (minx, miny, maxx, maxy) = shape.bounds - upper_left = InkstitchPoint(minx, miny) - rows = [] - - if line.geom_type != 'LineString' or not line.is_simple: - line = repair_non_simple_lines(line) - # extend the line towards the ends to increase probability that all offsetted curves cross the shape - line = 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)] - else: - runs = [res.coords] - - runs.sort(key=lambda seg: ( - InkstitchPoint(*seg[0]) - upper_left).length()) - if flip: - runs.reverse() - runs = [tuple(reversed(run)) for run in runs] - - if row_spacing > 0: - rows.append(runs) - else: - rows.insert(0, runs) - - line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) - if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest - line_offsetted = repair_multiple_parallel_offset_curves( - line_offsetted) - if not line_offsetted.is_simple: - line_offsetted = repair_non_simple_lines(line_offsetted) - - if row_spacing < 0: - line_offsetted.coords = line_offsetted.coords[::-1] - line_offsetted = line_offsetted.simplify(0.01, False) - res = line_offsetted.intersection(shape) - 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 - - line_offsetted = line.parallel_offset(row_spacing, 'left', 5) - if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest - line_offsetted = repair_multiple_parallel_offset_curves( - line_offsetted) - if not line_offsetted.is_simple: - line_offsetted = repair_non_simple_lines(line_offsetted) - # 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 - - def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=None, flip=False): # the max line length I'll need to intersect the whole shape is the diagonal (minx, miny, maxx, maxy) = shape.bounds diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py new file mode 100644 index 00000000..4cc250ef --- /dev/null +++ b/lib/stitches/guided_fill.py @@ -0,0 +1,355 @@ +import networkx +from depq import DEPQ +from shapely.geometry import GeometryCollection, LineString, MultiLineString +from shapely.ops import linemerge, unary_union +from shapely.strtree import STRtree + +from ..debug import debug +from ..i18n import _ +from ..stitch_plan import Stitch +from ..svg import PIXELS_PER_MM +from ..utils.geometry import Point as InkstitchPoint +from .auto_fill import (add_edges_between_outline_nodes, build_travel_graph, + collapse_sequential_outline_edges, fallback, + find_stitch_path, graph_is_valid, insert_node, + tag_nodes_with_outline_and_projection, travel, + weight_edges_by_length) +from .point_transfer import transfer_points_to_surrounding_graph +from .sample_linestring import raster_line_string_with_priority_points + + +@debug.time +def guided_fill(shape, + guideline, + angle, + row_spacing, + max_stitch_length, + running_stitch_length, + skip_last, + starting_point, + ending_point=None, + underpath=True, + offset_by_half=True): + + fill_stitch_graph = [] + try: + fill_stitch_graph = build_guided_fill_stitch_graph( + shape, guideline, 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) + + 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) + result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, + max_stitch_length, running_stitch_length, skip_last, offset_by_half) + + return result + + +@debug.time +def build_guided_fill_stitch_graph(shape, guideline, row_spacing, starting_point=None, ending_point=None): + """build a graph representation of the grating segments + + This function builds a specialized graph (as in graph theory) that will + help us determine a stitching path. The idea comes from this paper: + + http://www.sciencedirect.com/science/article/pii/S0925772100000158 + + The goal is to build a graph that we know must have an Eulerian Path. + An Eulerian Path is a path from edge to edge in the graph that visits + every edge exactly once and ends at the node it started at. Algorithms + exist to build such a path, and we'll use Hierholzer's algorithm. + + A graph must have an Eulerian Path if every node in the graph has an + even number of edges touching it. Our goal here is to build a graph + that will have this property. + + Based on the paper linked above, we'll build the graph as follows: + + * nodes are the endpoints of the grating segments, where they meet + with the outer outline of the region the outlines of the interior + holes in the region. + * edges are: + * each section of the outer and inner outlines of the region, + between nodes + * double every other edge in the outer and inner hole outlines + + Doubling up on some of the edges seems as if it will just mean we have + to stitch those spots twice. This may be true, but it also ensures + that every node has 4 edges touching it, ensuring that a valid stitch + path must exist. + """ + + debug.add_layer("auto-fill fill stitch") + + rows_of_segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing) + + # 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 + # of the endpoints as nodes, which networkx will add automatically. + + # 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[0], segment[-1], key="segment", underpath_edges=[], + geometry=LineString(segment), previous_neighbors=previous_neighbors, next_neighbors=next_neighbors, + projected_points=DEPQ(iterable=None, maxlen=None), already_rastered=False) + + tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) + add_edges_between_outline_nodes(graph, duplicate_every_other=True) + + if starting_point: + insert_node(graph, shape, starting_point) + + if ending_point: + insert_node(graph, shape, ending_point) + + debug.log_graph(graph, "graph") + + return graph + + +def get_segments(graph): + segments = [] + for start, end, key, data in graph.edges(keys=True, data=True): + if key == 'segment': + segments.append(data["geometry"]) + + return segments + + +def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): + """Weight the interior edges and pre-calculate intersection with fill stitch rows.""" + + # Set the weight equal to 5x the edge length, to encourage travel() + # to avoid them. + weight_edges_by_length(graph, 5) + + segments = get_segments(fill_stitch_graph) + + # The shapely documentation is pretty unclear on this. An STRtree + # allows for building a set of shapes and then efficiently testing + # the set for intersection. This allows us to do blazing-fast + # queries of which line segments overlap each underpath edge. + strtree = STRtree(segments) + + # 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) + + for ls in travel_edges: + # In most cases, ls will be a simple line segment. If we're + # unlucky, in rare cases we can get a tiny little extra squiggle + # at the end that can be ignored. + points = [InkstitchPoint(*coord) for coord in ls.coords] + p1, p2 = points[0], points[-1] + + edge = (p1.as_tuple(), p2.as_tuple(), 'travel') + + for segment in strtree.query(ls): + # It seems like the STRTree only gives an approximate answer of + # segments that _might_ intersect ls. Refining the result is + # necessary but the STRTree still saves us a ton of time. + if segment.crosses(ls): + start = segment.coords[0] + end = segment.coords[-1] + 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) + + # Give a bonus to edges that are far from the outline of the shape. + # This includes the outer outline and the outlines of the holes. + # The result is that travel stitching will tend to hug the center + # of the shape. + weight /= ls.distance(outline) + 0.1 + + graph.add_edge(*edge, weight=weight) + + # without this, we sometimes get exceptions like this: + # Exception AttributeError: "'NoneType' object has no attribute 'GEOSSTRtree_destroy'" in + # > ignored + del strtree + + +def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): + if stitching_direction == 1: + stitched_line, _ = raster_line_string_with_priority_points( + geometry, 0.0, geometry.length, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) + else: + stitched_line, _ = raster_line_string_with_priority_points( + geometry, geometry.length, 0.0, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) + + stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) + for i in range(1, len(stitched_line)-1): + 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',))) + else: + 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, skip_last, offset_by_half): + path = collapse_sequential_outline_edges(path) + + stitches = [] + + # If the very first stitch is travel, we'll omit it in travel(), so add it here. + if not path[0].is_segment(): + stitches.append(Stitch(*path[0].nodes[0])) + + for edge in path: + if edge.is_segment(): + new_stitches = [] + current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment'] + path_geometry = current_edge['geometry'] + 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])): + stitching_direction = -1 + 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) + + stitches.extend(new_stitches) + else: + stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) + + return stitches + + +def extend_line(line, minx, maxx, miny, maxy): + line = line.simplify(0.01, False) + + upper_left = InkstitchPoint(minx, miny) + lower_right = InkstitchPoint(maxx, maxy) + length = (upper_left - lower_right).length() + + point1 = InkstitchPoint(*line.coords[0]) + point2 = InkstitchPoint(*line.coords[1]) + new_starting_point = point1-(point2-point1).unit()*length + + point3 = InkstitchPoint(*line.coords[-2]) + point4 = InkstitchPoint(*line.coords[-1]) + new_ending_point = point4+(point4-point3).unit()*length + + return LineString([new_starting_point.as_tuple()] + + line.coords[1:-1]+[new_ending_point.as_tuple()]) + + +def repair_multiple_parallel_offset_curves(multi_line): + # TODO: linemerge is overritten by the very next line?!? + lines = linemerge(multi_line) + lines = list(multi_line.geoms) + max_length = -1 + max_length_idx = -1 + for idx, subline in enumerate(lines): + if subline.length > max_length: + max_length = subline.length + max_length_idx = idx + # need simplify to avoid doubled points caused by linemerge + return lines[max_length_idx].simplify(0.01, False) + + +def repair_non_simple_lines(line): + repaired = unary_union(line) + counter = 0 + # Do several iterations since we might have several concatenated selfcrossings + while repaired.geom_type != 'LineString' and counter < 4: + line_segments = [] + for line_seg in repaired.geoms: + if not line_seg.is_ring: + line_segments.append(line_seg) + + repaired = unary_union(linemerge(line_segments)) + counter += 1 + if repaired.geom_type != 'LineString': + raise ValueError( + _("Guide line (or offsetted instance) is self crossing!")) + else: + return repaired + + +def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False): # noqa: C901 + + row_spacing = abs(row_spacing) + (minx, miny, maxx, maxy) = shape.bounds + upper_left = InkstitchPoint(minx, miny) + rows = [] + + if line.geom_type != 'LineString' or not line.is_simple: + line = repair_non_simple_lines(line) + # extend the line towards the ends to increase probability that all offsetted curves cross the shape + line = extend_line(line, minx, maxx, miny, maxy) + + line_offsetted = line + res = line_offsetted.intersection(shape) + while isinstance(res, (GeometryCollection, MultiLineString)) or (not res.is_empty and len(res.coords) > 1): + if isinstance(res, (GeometryCollection, MultiLineString)): + 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()) + if flip: + runs.reverse() + runs = [tuple(reversed(run)) for run in runs] + + if row_spacing > 0: + rows.append(runs) + else: + rows.insert(0, runs) + + line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5) + if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest + line_offsetted = repair_multiple_parallel_offset_curves(line_offsetted) + if not line_offsetted.is_simple: + line_offsetted = repair_non_simple_lines(line_offsetted) + + if row_spacing < 0: + line_offsetted.coords = line_offsetted.coords[::-1] + line_offsetted = line_offsetted.simplify(0.01, False) + res = line_offsetted.intersection(shape) + if row_spacing > 0 and not isinstance(res, (GeometryCollection, MultiLineString)): + if (res.is_empty or len(res.coords) == 1): + row_spacing = -row_spacing + + line_offsetted = line.parallel_offset(row_spacing, 'left', 5) + if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest + line_offsetted = repair_multiple_parallel_offset_curves( + line_offsetted) + if not line_offsetted.is_simple: + line_offsetted = repair_non_simple_lines(line_offsetted) + # 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 diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py index a01e69cd..cf4597dd 100644 --- a/lib/stitches/point_transfer.py +++ b/lib/stitches/point_transfer.py @@ -1,10 +1,10 @@ -from shapely.geometry import Point, MultiPoint -from shapely.geometry.polygon import LineString, LinearRing +import math from collections import namedtuple + +from shapely.geometry import LinearRing, LineString, MultiPoint, Point from shapely.ops import nearest_points -import math -from ..stitches import constants -from ..stitches import sample_linestring + +from ..stitches import constants, sample_linestring """This file contains routines which shall project already selected points for stitching to remaining unstitched lines in the neighborhood to create a regular pattern of points.""" diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py index fb4bbc52..b2298984 100644 --- a/lib/stitches/sample_linestring.py +++ b/lib/stitches/sample_linestring.py @@ -1,11 +1,11 @@ -from shapely.geometry.polygon import LineString -from shapely.geometry import Point -from shapely.ops import substring import math -import numpy as np from enum import IntEnum -from ..stitches import constants -from ..stitches import point_transfer + +import numpy as np +from shapely.geometry import LineString, Point +from shapely.ops import substring + +from ..stitches import constants, point_transfer class PointSource(IntEnum): -- cgit v1.3.1 From 6916a3371695205ca388daa37e3b9a0cc8d51de6 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 20 Mar 2022 17:15:39 +0100 Subject: bug fixing + introduction of min_stitch_distance parameter --- lib/elements/fill_stitch.py | 15 ++++++++ lib/stitches/guided_fill.py | 16 ++++----- lib/stitches/point_transfer.py | 2 +- lib/stitches/sample_linestring.py | 19 +++++++--- .../tangential_fill_stitch_line_creator.py | 10 +++--- .../tangential_fill_stitch_pattern_creator.py | 40 +++++++++++++--------- lib/svg/tags.py | 1 + 7 files changed, 69 insertions(+), 34 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index eaddcfe0..e0de0f22 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -191,6 +191,19 @@ class FillStitch(EmbroideryElement): def max_stitch_length(self): return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + @property + @param('min_stitch_length_mm', + _('Minimum fill stitch length'), + tooltip=_( + 'The minimum length of a stitch in a row. Larger values might introduce deviations from the desired path. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index=4, + select_items=[('fill_method', 1), ('fill_method', 2)], + type='float', + default=0.0) + def min_stitch_length(self): + return self.get_float_param("min_stitch_length_mm", 0.0) + @property @param('staggers', _('Stagger rows this many times before repeating'), @@ -557,6 +570,7 @@ class FillStitch(EmbroideryElement): -self.row_spacing, self.join_style+1, self.max_stitch_length, + min(self.min_stitch_length, self.max_stitch_length), self.interlaced, self.tangential_strategy, shgeo.Point(starting_point)) @@ -585,6 +599,7 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, + min(self.min_stitch_length,self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 4cc250ef..6948a086 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -24,6 +24,7 @@ def guided_fill(shape, angle, row_spacing, max_stitch_length, + min_stitch_length, running_stitch_length, skip_last, starting_point, @@ -45,7 +46,7 @@ def guided_fill(shape, 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, skip_last, offset_by_half) + max_stitch_length, min_stitch_length, running_stitch_length, skip_last, offset_by_half) return result @@ -187,13 +188,13 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): del strtree -def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, row_spacing, skip_last, offset_by_half): +def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, min_stitch_length, row_spacing, skip_last, offset_by_half): if stitching_direction == 1: stitched_line, _ = raster_line_string_with_priority_points( - geometry, 0.0, geometry.length, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) + geometry, 0.0, geometry.length, max_stitch_length, min_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) else: stitched_line, _ = raster_line_string_with_priority_points( - geometry, geometry.length, 0.0, max_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) + geometry, geometry.length, 0.0, max_stitch_length, min_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) for i in range(1, len(stitched_line)-1): @@ -209,7 +210,7 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s @debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, +def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, min_stitch_length, running_stitch_length, skip_last, offset_by_half): path = collapse_sequential_outline_edges(path) @@ -230,7 +231,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, 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) + max_stitch_length, min_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) @@ -264,9 +265,8 @@ def extend_line(line, minx, maxx, miny, maxy): def repair_multiple_parallel_offset_curves(multi_line): - # TODO: linemerge is overritten by the very next line?!? lines = linemerge(multi_line) - lines = list(multi_line.geoms) + lines = list(lines.geoms) max_length = -1 max_length_idx = -1 for idx, subline in enumerate(lines): diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py index cf4597dd..5506324d 100644 --- a/lib/stitches/point_transfer.py +++ b/lib/stitches/point_transfer.py @@ -70,7 +70,7 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra 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: + if len(to_transfer_points) < 3: return # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py index b2298984..838b1792 100644 --- a/lib/stitches/sample_linestring.py +++ b/lib/stitches/sample_linestring.py @@ -63,7 +63,7 @@ def calculate_line_angles(line): return Angles -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, # noqa: C901 +def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, minstitch_distance, # noqa: C901 must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): """ Rasters a line between start_distance and end_distance. @@ -72,6 +72,7 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, -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 + -minstitch_distance: The minimum allowed stitch distance -Note that start_distance > end_distance for stitching_direction = -1 -must_use_points_deque: deque with projected points on line from its neighbors. An item of the deque is setup as follows: ((projected point on line, LineStringSampling.PointSource), priority=distance along line) @@ -84,7 +85,7 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, -List which defines the point origin for each point according to the PointSource enum. """ - if (abs(end_distance-start_distance) < constants.line_lengh_seen_as_one_point): + if (abs(end_distance-start_distance) < max(minstitch_distance, constants.line_lengh_seen_as_one_point)): return [line.interpolate(start_distance).coords[0]], [PointSource.HARD_EDGE] deque_points = list(must_use_points_deque) @@ -103,9 +104,9 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, 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)): + while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): deque_points.pop(0) - while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, constants.point_spacing_to_be_considered_equal)): + while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): deque_points.pop() @@ -185,6 +186,9 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, while segment_end_index < len(merged_point_list): segment_length = merged_point_list[segment_end_index][1] - \ merged_point_list[segment_start_index][1] + if segment_length < minstitch_distance: + segment_end_index += 1 + continue if segment_length > maxstitch_distance+constants.point_spacing_to_be_considered_equal: new_distance = merged_point_list[segment_start_index][1] + \ maxstitch_distance @@ -214,6 +218,13 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, iter = segment_start_index+1 while (iter <= segment_end_index): + segment_length = merged_point_list[iter][1] - \ + merged_point_list[segment_start_index][1] + if segment_length < minstitch_distance and merged_point_list[iter][0].point_source != PointSource.HARD_EDGE_INTERNAL: + #We need to create this hard edge exception - otherwise there are some too large deviations posible + iter += 1 + continue + if merged_point_list[iter][0].point_source == PointSource.OVERNEXT: index_overnext = iter elif merged_point_list[iter][0].point_source == PointSource.DIRECT: diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index af14ea0f..4d4377f0 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -158,7 +158,7 @@ def check_and_prepare_tree_for_valid_spiral(root): return True -def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 +def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. @@ -173,6 +173,8 @@ def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strat For examples look at https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png -stitch_distance maximum allowed stitch distance between two points + -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting + offsetted paths might be shorter. -offset_by_half: True if the points shall be interlaced -strategy: According to StitchingStrategy enum class you can select between different strategies for the connection between parent and childs. In @@ -315,15 +317,15 @@ def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strat if strategy == StitchingStrategy.CLOSEST_POINT: (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_nearest_neighbor( - root, offset, stitch_distance, starting_point, offset_by_half) + root, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) elif strategy == StitchingStrategy.INNER_TO_OUTER: (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( - root, offset, stitch_distance, starting_point, offset_by_half) + root, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) elif strategy == StitchingStrategy.SPIRAL: if not check_and_prepare_tree_for_valid_spiral(root): raise ValueError("Geometry cannot be filled with one spiral!") (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_spiral( - root, offset, stitch_distance, starting_point, offset_by_half) + root, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) else: raise ValueError("Invalid stitching stratety!") diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index d7afad0c..95143bce 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -52,7 +52,7 @@ def cut(line, distance): def connect_raster_tree_nearest_neighbor( # noqa: C901 - tree, used_offset, stitch_distance, close_point, offset_by_half): + tree, used_offset, stitch_distance, min_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 @@ -63,6 +63,8 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 -used_offset: used offset when the offsetted curves were generated -stitch_distance: maximum allowed distance between two points after sampling + -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting + offsetted paths might be shorter. -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. @@ -136,6 +138,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 # points for start and end) end_distance, stitch_distance, + min_stitch_distance, tree.transferred_point_priority_deque, abs_offset, offset_by_half, @@ -230,6 +233,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 item.child_node, used_offset, stitch_distance, + min_stitch_distance, item.nearest_point_child, offset_by_half, ) @@ -432,7 +436,7 @@ def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distan return line_segment.coords[1] -def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, close_point, offset_by_half): # noqa: C901 +def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them. Strategy: A connection from parent to child is made as fast as possible to @@ -444,6 +448,8 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, -used_offset: used offset when the offsetted curves were generated -stitch_distance: maximum allowed distance between two points after sampling + -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting + offsetted paths might be shorter. -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. @@ -514,11 +520,12 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, if stitching_direction == 1: (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( current_coords, - start_offset, # We add start_offset to not sample the same - # point again (avoid double points for start + start_offset, # We add start_offset to not sample the initial/end + # point twice (avoid double points for start # and end) end_offset, stitch_distance, + min_stitch_distance, tree.transferred_point_priority_deque, abs_offset, offset_by_half, @@ -529,12 +536,13 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, current_coords, current_coords.length - start_offset, # We subtract # start_offset to not - # sample the same point - # again (avoid double + # sample the initial/end point + # twice (avoid double # points for start # and end) current_coords.length - end_offset, stitch_distance, + min_stitch_distance, tree.transferred_point_priority_deque, abs_offset, offset_by_half, @@ -639,6 +647,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, item.child_node, used_offset, stitch_distance, + min_stitch_distance, item.nearest_point_child, offset_by_half, ) @@ -683,19 +692,12 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, 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 - ), + 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] + current_coords.interpolate(item.proj_distance_parent + 2 * constants.factor_offset_starting_points * abs_offset).coords[0] ) result_coords_origin.append( sample_linestring.PointSource.ENTER_LEAVING_POINT @@ -792,7 +794,7 @@ def interpolate_LinearRings(a, b, start=None, step=.005): def connect_raster_tree_spiral( - tree, used_offset, stitch_distance, close_point, offset_by_half): + tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): """ Takes the offsetted curves organized as tree, connects and samples them as a spiral. It expects that each node in the tree has max. one child @@ -802,6 +804,8 @@ def connect_raster_tree_spiral( -used_offset: used offset when the offsetted curves were generated -stitch_distance: maximum allowed distance between two points after sampling + -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting + offsetted paths might be shorter. -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. @@ -819,6 +823,7 @@ def connect_raster_tree_spiral( 0, tree.val.length, stitch_distance, + min_stitch_distance, tree.transferred_point_priority_deque, abs_offset, offset_by_half, @@ -842,6 +847,7 @@ def connect_raster_tree_spiral( 0, node.val.length, stitch_distance, + min_stitch_distance, node.transferred_point_priority_deque, abs_offset, offset_by_half, @@ -883,7 +889,7 @@ def connect_raster_tree_spiral( lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) else: lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[1]]) - (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, + (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, min_stitch_distance, DEPQ(), abs_offset, offset_by_half, False) if len(temp_coords) == 2: # only start and end point of lineseg was needed result_coords.pop() diff --git a/lib/svg/tags.py b/lib/svg/tags.py index f3118661..37eb5752 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -64,6 +64,7 @@ inkstitch_attribs = [ 'fill_underlay_row_spacing_mm', 'fill_underlay_skip_last', 'max_stitch_length_mm', + 'min_stitch_length_mm', 'row_spacing_mm', 'end_row_spacing_mm', 'skip_last', -- cgit v1.3.1 From 920063b324fd59798bc462c644bce8fc543f535b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Apr 2022 08:12:45 -0400 Subject: fix style --- lib/elements/fill_stitch.py | 5 +- lib/stitches/guided_fill.py | 25 +-- lib/stitches/point_transfer.py | 2 +- lib/stitches/sample_linestring.py | 40 +++-- .../tangential_fill_stitch_line_creator.py | 169 +++++++++++---------- .../tangential_fill_stitch_pattern_creator.py | 31 ++-- 6 files changed, 148 insertions(+), 124 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index e0de0f22..9233b6cf 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -195,7 +195,8 @@ class FillStitch(EmbroideryElement): @param('min_stitch_length_mm', _('Minimum fill stitch length'), tooltip=_( - 'The minimum length of a stitch in a row. Larger values might introduce deviations from the desired path. Shorter stitch may be used at the start or end of a row.'), + 'The minimum length of a stitches in a row. Larger values might introduce deviations from the desired path.' + 'Shorter stitch may be used at the start or end of a row.'), unit='mm', sort_index=4, select_items=[('fill_method', 1), ('fill_method', 2)], @@ -599,7 +600,7 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, - min(self.min_stitch_length,self.max_stitch_length), + min(self.min_stitch_length, self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 51b0618f..fb122fba 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -2,20 +2,17 @@ import networkx from depq import DEPQ from shapely.geometry import GeometryCollection, LineString, MultiLineString from shapely.ops import linemerge, unary_union -from shapely.strtree import STRtree -from ..debug import debug -from ..i18n import _ -from ..stitch_plan import Stitch -from ..svg import PIXELS_PER_MM -from ..utils.geometry import Point as InkstitchPoint from .auto_fill import (add_edges_between_outline_nodes, build_travel_graph, collapse_sequential_outline_edges, fallback, find_stitch_path, graph_is_valid, insert_node, - tag_nodes_with_outline_and_projection, travel, - weight_edges_by_length) + tag_nodes_with_outline_and_projection, travel) from .point_transfer import transfer_points_to_surrounding_graph from .sample_linestring import raster_line_string_with_priority_points +from ..debug import debug +from ..i18n import _ +from ..stitch_plan import Stitch +from ..utils.geometry import Point as InkstitchPoint @debug.time @@ -124,7 +121,15 @@ def build_guided_fill_stitch_graph(shape, guideline, row_spacing, starting_point return graph -def stitch_line(stitches, stitching_direction, geometry, projected_points, max_stitch_length, min_stitch_length, row_spacing, skip_last, offset_by_half): +def stitch_line(stitches, + stitching_direction, + geometry, + projected_points, + max_stitch_length, + min_stitch_length, + row_spacing, + skip_last, + offset_by_half): if stitching_direction == 1: stitched_line, _ = raster_line_string_with_priority_points( geometry, 0.0, geometry.length, max_stitch_length, min_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) @@ -133,7 +138,7 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s geometry, geometry.length, 0.0, max_stitch_length, min_stitch_length, projected_points, abs(row_spacing), offset_by_half, True) stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) - for i in range(1, len(stitched_line)-1): + for i in range(1, len(stitched_line) - 1): stitches.append(Stitch(*stitched_line[i], tags=('fill_row'))) if not skip_last: diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py index 5506324d..553ffbda 100644 --- a/lib/stitches/point_transfer.py +++ b/lib/stitches/point_transfer.py @@ -1,7 +1,7 @@ import math from collections import namedtuple -from shapely.geometry import LinearRing, LineString, MultiPoint, Point +from shapely.geometry import LineString, LinearRing, MultiPoint, Point from shapely.ops import nearest_points from ..stitches import constants, sample_linestring diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py index 838b1792..85592984 100644 --- a/lib/stitches/sample_linestring.py +++ b/lib/stitches/sample_linestring.py @@ -63,8 +63,15 @@ def calculate_line_angles(line): return Angles -def raster_line_string_with_priority_points(line, start_distance, end_distance, maxstitch_distance, minstitch_distance, # noqa: C901 - must_use_points_deque, abs_offset, offset_by_half, replace_forbidden_points): +def raster_line_string_with_priority_points(line, # noqa: C901 + start_distance, + end_distance, + maxstitch_distance, + minstitch_distance, + must_use_points_deque, + abs_offset, + offset_by_half, + replace_forbidden_points): """ Rasters a line between start_distance and end_distance. Input: @@ -94,24 +101,25 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, if start_distance > end_distance: start_distance, end_distance = line.length - \ - start_distance, line.length-end_distance + start_distance, line.length - end_distance linecoords = linecoords[::-1] for i in range(len(deque_points)): deque_points[i] = (deque_points[i][0], - line.length-deque_points[i][1]) + line.length - deque_points[i][1]) else: # Since points with highest priority (=distance along line) are first (descending sorted) deque_points = deque_points[::-1] # Remove all points from the deque which do not fall in the segment [start_distance; end_distance] - while (len(deque_points) > 0 and deque_points[0][1] <= start_distance+min(maxstitch_distance/20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): + while (len(deque_points) > 0 and + deque_points[0][1] <= start_distance + min(maxstitch_distance / 20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): deque_points.pop(0) - while (len(deque_points) > 0 and deque_points[-1][1] >= end_distance-min(maxstitch_distance/20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): + while (len(deque_points) > 0 and + deque_points[-1][1] >= end_distance - min(maxstitch_distance / 20, minstitch_distance, constants.point_spacing_to_be_considered_equal)): deque_points.pop() - -# Ordering in priority queue: -# (point, LineStringSampling.PointSource), priority) + # Ordering in priority queue: + # (point, LineStringSampling.PointSource), priority) # might be different from line for stitching_direction=-1 aligned_line = LineString(linecoords) path_coords = substring(aligned_line, @@ -121,17 +129,17 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, # 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): + 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) # 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[0] = 1.1 * constants.limiting_angle + angles[-1] = 1.1 * constants.limiting_angle current_distance = 0 last_point = Point(path_coords.coords[0]) @@ -221,7 +229,7 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, segment_length = merged_point_list[iter][1] - \ merged_point_list[segment_start_index][1] if segment_length < minstitch_distance and merged_point_list[iter][0].point_source != PointSource.HARD_EDGE_INTERNAL: - #We need to create this hard edge exception - otherwise there are some too large deviations posible + # We need to create this hard edge exception - otherwise there are some too large deviations posible iter += 1 continue diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 01124478..6b141611 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -1,13 +1,15 @@ -from shapely.geometry.polygon import LinearRing, LineString -from shapely.geometry import Polygon, MultiLineString -from shapely.ops import polygonize +from enum import IntEnum + +from anytree import AnyNode, LevelOrderGroupIter, PreOrderIter +from depq import DEPQ +from shapely.geometry import MultiLineString, Polygon from shapely.geometry import MultiPolygon -from anytree import AnyNode, PreOrderIter, LevelOrderGroupIter, RenderTree +from shapely.geometry.polygon import LinearRing from shapely.geometry.polygon import orient -from depq import DEPQ -from enum import IntEnum -from ..stitches import tangential_fill_stitch_pattern_creator +from shapely.ops import polygonize + from ..stitches import constants +from ..stitches import tangential_fill_stitch_pattern_creator def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): @@ -21,81 +23,80 @@ def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): result_list.append(poly.exterior) return MultiLineString(result_list) - -# """ -# 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 -# """ - -#PROBLEM: Did not work in rare cases since it expects the point order be maintained after offsetting the curve -#(e.g. the first point in the offsetted curve shall belong to the first point in the original curve). However, this -#assumption seems to be not always true that is why this code was replaced by the buffer routine. - -# coords = ring.coords[:] -# # check whether edge at index 0 is concave or convex. Only for -# # concave edges we need to spend additional effort -# dx_seg1 = dy_seg1 = 0 -# if coords[0] != coords[-1]: -# dx_seg1 = coords[0][0] - coords[-1][0] -# dy_seg1 = coords[0][1] - coords[-1][1] -# else: -# dx_seg1 = coords[0][0] - coords[-2][0] -# dy_seg1 = coords[0][1] - coords[-2][1] -# dx_seg2 = coords[1][0] - coords[0][0] -# dy_seg2 = coords[1][1] - coords[0][1] -# # use cross product: -# crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 -# sidesign = 1 -# if side == "left": -# sidesign = -1 - -# # We do not need to take care of the joint n-0 since we -# # offset along a concave edge: -# if sidesign * offset * crossvalue <= 0: -# return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) - -# # We offset along a convex edge so we offset the joint n-0 separately: -# if coords[0] != coords[-1]: -# coords.append(coords[0]) -# offset_ring1 = ring.parallel_offset( -# offset, side, resolution, join_style, mitre_limit -# ) -# offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( -# offset, side, resolution, join_style, mitre_limit -# ) - -# # Next we need to merge the results: -# if offset_ring1.geom_type == "LineString": -# return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) -# else: -# # We have more than one resulting LineString for offset of -# # the geometry (ring) = offset_ring1. -# # Hence we need to find the LineString which belongs to the -# # offset of element 0 in coords =offset_ring2 -# # in order to add offset_ring2 geometry to it: -# result_list = [] -# thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) -# for offsets in offset_ring1: -# if ( -# abs(offsets.coords[0][0] - coords[0][0]) < thresh -# and abs(offsets.coords[0][1] - coords[0][1]) < thresh -# ): -# result_list.append( -# LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) -# ) -# else: -# result_list.append(LinearRing(offsets)) -# return MultiLineString(result_list) + # """ + # 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 + # """ + + # PROBLEM: Did not work in rare cases since it expects the point order be maintained after offsetting the curve + # (e.g. the first point in the offsetted curve shall belong to the first point in the original curve). However, this + # assumption seems to be not always true that is why this code was replaced by the buffer routine. + + # coords = ring.coords[:] + # # check whether edge at index 0 is concave or convex. Only for + # # concave edges we need to spend additional effort + # dx_seg1 = dy_seg1 = 0 + # if coords[0] != coords[-1]: + # dx_seg1 = coords[0][0] - coords[-1][0] + # dy_seg1 = coords[0][1] - coords[-1][1] + # else: + # dx_seg1 = coords[0][0] - coords[-2][0] + # dy_seg1 = coords[0][1] - coords[-2][1] + # dx_seg2 = coords[1][0] - coords[0][0] + # dy_seg2 = coords[1][1] - coords[0][1] + # # use cross product: + # crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2 + # sidesign = 1 + # if side == "left": + # sidesign = -1 + + # # We do not need to take care of the joint n-0 since we + # # offset along a concave edge: + # if sidesign * offset * crossvalue <= 0: + # return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit) + + # # We offset along a convex edge so we offset the joint n-0 separately: + # if coords[0] != coords[-1]: + # coords.append(coords[0]) + # offset_ring1 = ring.parallel_offset( + # offset, side, resolution, join_style, mitre_limit + # ) + # offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset( + # offset, side, resolution, join_style, mitre_limit + # ) + + # # Next we need to merge the results: + # if offset_ring1.geom_type == "LineString": + # return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1]) + # else: + # # We have more than one resulting LineString for offset of + # # the geometry (ring) = offset_ring1. + # # Hence we need to find the LineString which belongs to the + # # offset of element 0 in coords =offset_ring2 + # # in order to add offset_ring2 geometry to it: + # result_list = [] + # thresh = constants.offset_factor_for_adjacent_geometry * abs(offset) + # for offsets in offset_ring1: + # if ( + # abs(offsets.coords[0][0] - coords[0][0]) < thresh + # and abs(offsets.coords[0][1] - coords[0][1]) < thresh + # ): + # result_list.append( + # LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1]) + # ) + # else: + # result_list.append(LinearRing(offsets)) + # return MultiLineString(result_list) def take_only_valid_linear_rings(rings): @@ -250,7 +251,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, for j in range(len(current_holes)): inner = offset_linear_ring( current_holes[j].val, - -offset, #take negative offset for holes + -offset, # take negative offset for holes resolution=5, join_style=join_style, mitre_limit=10, @@ -327,7 +328,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, if previous_hole.parent is None: previous_hole.parent = current_poly - #print(RenderTree(root)) + # print(RenderTree(root)) make_tree_uniform_ccw(root) if strategy == StitchingStrategy.CLOSEST_POINT: diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 95143bce..13a480e3 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -1,16 +1,18 @@ -from shapely.geometry.polygon import LineString, LinearRing -from shapely.geometry import Point, MultiPoint -from shapely.ops import nearest_points +import math from collections import namedtuple -from depq import DEPQ -import trimesh + import numpy as np -from scipy import spatial -import math +import trimesh from anytree import PreOrderIter -from ..stitches import sample_linestring -from ..stitches import point_transfer +from depq import DEPQ +from scipy import spatial +from shapely.geometry import MultiPoint, Point +from shapely.geometry.polygon import LineString, LinearRing +from shapely.ops import nearest_points + from ..stitches import constants +from ..stitches import point_transfer +from ..stitches import sample_linestring nearest_neighbor_tuple = namedtuple( "nearest_neighbor_tuple", @@ -889,8 +891,15 @@ def connect_raster_tree_spiral( lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[0], own_coords[1]]) else: lineseg = LineString([result_coords[-2], result_coords[-1], own_coords[1]]) - (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, 0, lineseg.length, stitch_distance, min_stitch_distance, - DEPQ(), abs_offset, offset_by_half, False) + (temp_coords, _) = sample_linestring.raster_line_string_with_priority_points(lineseg, + 0, + lineseg.length, + stitch_distance, + min_stitch_distance, + DEPQ(), + abs_offset, + offset_by_half, + False) if len(temp_coords) == 2: # only start and end point of lineseg was needed result_coords.pop() result_coords_origin.pop() -- cgit v1.3.1 From e2ede5e456d8037552ac9077f2cc34ccdfb52db2 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 28 Apr 2022 23:25:52 -0400 Subject: get rid of "closest point" strategy --- lib/elements/fill_stitch.py | 2 +- .../tangential_fill_stitch_line_creator.py | 10 +- .../tangential_fill_stitch_pattern_creator.py | 243 +-------------------- 3 files changed, 5 insertions(+), 250 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 9233b6cf..3b87ea0c 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -103,7 +103,7 @@ class FillStitch(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index deb87659..46b5a262 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -146,9 +146,8 @@ def make_tree_uniform_ccw(tree): # Used to define which stitching strategy shall be used class StitchingStrategy(IntEnum): - CLOSEST_POINT = 0 - INNER_TO_OUTER = 1 - SPIRAL = 2 + INNER_TO_OUTER = 0 + SPIRAL = 1 def check_and_prepare_tree_for_valid_spiral(tree): @@ -350,10 +349,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, make_tree_uniform_ccw(tree) - if strategy == StitchingStrategy.CLOSEST_POINT: - (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_nearest_neighbor( - tree, 'root', offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) - elif strategy == StitchingStrategy.INNER_TO_OUTER: + if strategy == StitchingStrategy.INNER_TO_OUTER: (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( tree, 'root', offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) elif strategy == StitchingStrategy.SPIRAL: diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 872cee0e..92a508cd 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -54,246 +54,6 @@ def cut(line, distance): return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i]) -def connect_raster_tree_nearest_neighbor( # noqa: C901 - tree, node, used_offset, stitch_distance, min_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 - -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting - offsetted paths might be shorter. - -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_node = tree.nodes[node] - current_coords = current_node.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - # We cut the current item so that its index 0 is closest to close_point - start_distance = current_coords.project(close_point) - if start_distance > 0: - current_coords = cut(current_coords, start_distance) - current_node.val = current_coords - - if not current_node.transferred_point_priority_deque.is_empty(): - new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in current_node.transferred_point_priority_deque: - new_DEPQ.insert( - item, - math.fmod( - priority - start_distance + current_coords.length, - current_coords.length, - ), - ) - current_node.transferred_point_priority_deque = new_DEPQ - - stitching_direction = 1 - # This list should contain a tuple of nearest points between - # the current geometry and the subgeometry, the projected - # distance along the current geometry, and the belonging subtree node - nearest_points_list = [] - - for subnode in tree[node]: - point_parent, point_child = nearest_points(current_coords, tree.nodes[subnode].val) - proj_distance = current_coords.project(point_parent) - nearest_points_list.append( - nearest_neighbor_tuple( - nearest_point_parent=point_parent, - nearest_point_child=point_child, - proj_distance_parent=proj_distance, - child_node=subnode) - ) - nearest_points_list.sort( - reverse=False, key=lambda tup: tup.proj_distance_parent) - - if nearest_points_list: - start_distance = min( - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[0].proj_distance_parent, - ) - end_distance = max( - current_coords.length - - abs_offset * constants.factor_offset_starting_points, - nearest_points_list[-1].proj_distance_parent, - ) - else: - start_distance = abs_offset * constants.factor_offset_starting_points - end_distance = (current_coords.length - abs_offset * constants.factor_offset_starting_points) - - (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( - current_coords, - start_distance, # We add/subtract an offset to not sample - # the same point again (avoid double - # points for start and end) - end_distance, - stitch_distance, - min_stitch_distance, - current_node.transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - assert len(own_coords) == len(own_coords_origin) - own_coords_origin[0] = sample_linestring.PointSource.ENTER_LEAVING_POINT - own_coords_origin[-1] = sample_linestring.PointSource.ENTER_LEAVING_POINT - current_node.stitching_direction = stitching_direction - current_node.already_rastered = True - - # Next we need to transfer our rastered points to siblings and childs - to_transfer_point_list = [] - to_transfer_point_list_origin = [] - for k in range(1, len(own_coords) - 1): - # Do not take the first and the last since they are ENTER_LEAVING_POINT - # points for sure - - if (not offset_by_half and own_coords_origin[k] == sample_linestring.PointSource.EDGE_NEEDED): - continue - if (own_coords_origin[k] == sample_linestring.PointSource.ENTER_LEAVING_POINT or - own_coords_origin[k] == sample_linestring.PointSource.FORBIDDEN_POINT): - continue - to_transfer_point_list.append(Point(own_coords[k])) - point_origin = own_coords_origin[k] - to_transfer_point_list_origin.append(point_origin) - - # Since the projection is only in ccw direction towards inner we need - # to use "-used_offset" for stitching_direction==-1 - point_transfer.transfer_points_to_surrounding( - tree, - node, - stitching_direction * used_offset, - offset_by_half, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=False, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - # We transfer also to the overnext child to get a more straight - # arrangement of points perpendicular to the stitching lines - if offset_by_half: - point_transfer.transfer_points_to_surrounding( - tree, - node, - stitching_direction * used_offset, - False, - to_transfer_point_list, - to_transfer_point_list_origin, - overnext_neighbor=True, - transfer_forbidden_points=False, - transfer_to_parent=False, - transfer_to_sibling=True, - transfer_to_child=True, - ) - - if not nearest_points_list: - # If there is no child (inner geometry) we can simply take - # our own rastered coords as result - result_coords = own_coords - result_coords_origin = own_coords_origin - else: - # There are childs so we need to merge their coordinates + - # with our own rastered coords - - # To create a closed ring - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) - - # own_coords does not start with current_coords but has an offset - # (see call of raster_line_string_with_priority_points) - total_distance = start_distance - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [ - sample_linestring.PointSource.ENTER_LEAVING_POINT] - for i in range(1, len(own_coords)): - next_distance = math.sqrt( - (own_coords[i][0] - own_coords[i - 1][0]) ** 2 - + (own_coords[i][1] - own_coords[i - 1][1]) ** 2 - ) - while ( - cur_item < len(nearest_points_list) - and total_distance + next_distance + constants.eps - > nearest_points_list[cur_item].proj_distance_parent - ): - - item = nearest_points_list[cur_item] - (child_coords, child_coords_origin) = connect_raster_tree_nearest_neighbor( - tree, - item.child_node, - used_offset, - stitch_distance, - min_stitch_distance, - item.nearest_point_child, - offset_by_half, - ) - - d = item.nearest_point_parent.distance( - Point(own_coords[i - 1])) - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append(item.nearest_point_parent.coords[0]) - result_coords_origin.append( - sample_linestring.PointSource.ENTER_LEAVING_POINT - ) - # reversing avoids crossing when entering and - # leaving the child segment - result_coords.extend(child_coords[::-1]) - result_coords_origin.extend(child_coords_origin[::-1]) - - # And here we calculate the point for the leaving - d = item.nearest_point_parent.distance(Point(own_coords[i])) - if cur_item < len(nearest_points_list) - 1: - d = min( - d, - abs(nearest_points_list[cur_item+1].proj_distance_parent-item.proj_distance_parent) - ) - - if d > abs_offset * constants.factor_offset_starting_points: - result_coords.append( - current_coords.interpolate( - item.proj_distance_parent - + abs_offset * constants.factor_offset_starting_points - ).coords[0] - ) - result_coords_origin.append(sample_linestring.PointSource.ENTER_LEAVING_POINT) - - cur_item += 1 - if i < len(own_coords) - 1: - if (Point(result_coords[-1]).distance(Point(own_coords[i])) > abs_offset * constants.factor_offset_remove_points): - result_coords.append(own_coords[i]) - result_coords_origin.append(own_coords_origin[i]) - - # Since current_coords and temp are rastered differently - # there accumulate errors regarding the current distance. - # Since a projection of each point in temp would be very time - # consuming we project only every n-th point which resets - # the accumulated error every n-th point. - if i % 20 == 0: - total_distance = current_coords.project(Point(own_coords[i])) - else: - total_distance += next_distance - - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin - - def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): """ Takes a line and calculates the nearest distance along this @@ -753,6 +513,7 @@ def orient_linear_ring(ring): def reorder_linear_ring(ring, start): + # TODO: actually use start? start_index = np.argmin(np.linalg.norm(ring, axis=1)) return np.roll(ring, -start_index, axis=0) @@ -828,8 +589,6 @@ def connect_raster_tree_spiral(tree, used_offset, stitch_distance, min_stitch_di stitches = [Stitch(*point) for point in tree.nodes['root'].val.coords] return running_stitch(stitches, stitch_distance) - # TODO: cut each ring near close_point - starting_point = close_point.coords[0] path = [] for node in nx.dfs_preorder_nodes(tree, 'root'): -- cgit v1.3.1 From 60fb7d0a9efa43d3b58867927ecede6cfdc5ab21 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 14:47:43 -0400 Subject: fix more shapely deprecations --- lib/elements/fill_stitch.py | 2 +- lib/stitches/tangential_fill_stitch_line_creator.py | 8 +++----- lib/stitches/tangential_fill_stitch_pattern_creator.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 3b87ea0c..4157d3fb 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -562,7 +562,7 @@ class FillStitch(EmbroideryElement): def do_tangential_fill(self, last_patch, starting_point): stitch_groups = [] - polygons = list(self.fill_shape) + polygons = self.fill_shape.geoms if not starting_point: starting_point = (0, 0) for poly in polygons: diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 042bdde0..7f8b3bea 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -29,7 +29,7 @@ def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): return result.exterior else: result_list = [] - for poly in result: + for poly in result.geoms: result_list.append(poly.exterior) return MultiLineString(result_list) @@ -41,10 +41,8 @@ def take_only_valid_linear_rings(rings): """ 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] - ): + for ring in rings.geoms: + 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]) diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 0ee1c031..20f7a651 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -250,8 +250,8 @@ def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): # orders of magnitude faster because we're not building and querying a KDTree. num_points = int(20 * ring1.length / max_stitch_length) - ring1_resampled = trimesh.path.traversal.resample_path(ring1, count=num_points) - ring2_resampled = trimesh.path.traversal.resample_path(ring2, count=num_points) + ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points) + ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points) if start is not None: ring1_resampled = reorder_linear_ring(ring1_resampled, start) -- cgit v1.3.1 From 76ab3197317f258ede1bd98195535f33b856b3fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 15:00:52 -0400 Subject: add avoid_self_Crossing option --- lib/elements/fill_stitch.py | 11 +- .../tangential_fill_stitch_line_creator.py | 6 +- .../tangential_fill_stitch_pattern_creator.py | 10 +- lib/svg/tags.py | 111 +++++++++++---------- 4 files changed, 77 insertions(+), 61 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 4157d3fb..bc022ab3 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -118,6 +118,11 @@ class FillStitch(EmbroideryElement): def interlaced(self): return self.get_boolean_param('interlaced', True) + @property + @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=2) + def avoid_self_crossing(self): + return self.get_boolean_param('avoid_self_crossing', False) + @property @param('angle', _('Angle of lines of stitches'), @@ -569,12 +574,14 @@ class FillStitch(EmbroideryElement): connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( poly, -self.row_spacing, - self.join_style+1, + self.join_style + 1, self.max_stitch_length, min(self.min_stitch_length, self.max_stitch_length), self.interlaced, self.tangential_strategy, - shgeo.Point(starting_point)) + shgeo.Point(starting_point), + self.avoid_self_crossing + ) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( color=self.color, diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 7f8b3bea..61598b58 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -115,7 +115,8 @@ def check_and_prepare_tree_for_valid_spiral(tree): return process_node('root') -def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 +def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point, # noqa: C901 + avoid_self_crossing): """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. @@ -139,6 +140,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, In contrast to the other two options, "SPIRAL" does not end at the starting point but at the innermost point -starting_point: Defines the starting point for the stitching + -avoid_self_crossing: don't let the path cross itself when using the Inner to Outer strategy Output: -List of point coordinate tuples -Tag (origin) of each point to analyze why a point was placed @@ -277,7 +279,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, if strategy == StitchingStrategy.INNER_TO_OUTER: connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( - tree, 'root', abs(offset), stitch_distance, min_stitch_distance, starting_point, offset_by_half) + tree, 'root', abs(offset), stitch_distance, min_stitch_distance, starting_point, offset_by_half, avoid_self_crossing) path = [Stitch(*point) for point in connected_line.coords] return running_stitch(path, stitch_distance), "whatever" elif strategy == StitchingStrategy.SPIRAL: diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 20f7a651..553241f8 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -110,7 +110,7 @@ def create_nearest_points_list( @debug.time def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, min_stitch_distance, starting_point, - offset_by_half): # noqa: C901 + offset_by_half, avoid_self_crossing, forward=True): """ Takes the offset curves organized as a tree, connects and samples them. Strategy: A connection from parent to child is made as fast as possible to @@ -137,6 +137,9 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, current_node = tree.nodes[node] current_ring = current_node.val + if not forward and avoid_self_crossing: + current_ring = reverse_line_string(current_ring) + # reorder the coordinates of this ring so that it starts with # a point nearest the starting_point start_distance = current_ring.project(starting_point) @@ -157,7 +160,8 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, if not nearest_points_list: # We have no children, so we're at the center of a spiral. Reversing # the ring gives a nicer visual appearance. - current_ring = reverse_line_string(current_ring) + # current_ring = reverse_line_string(current_ring) + pass else: # This is a recursive algorithm. We'll stitch along this ring, pausing # to jump to each child ring in turn and sew it before continuing on @@ -184,6 +188,8 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, min_stitch_distance, child_connection.nearest_point_child, offset_by_half, + avoid_self_crossing, + not forward ) result_coords.extend(child_path.coords) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 37eb5752..3f412c2e 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -43,60 +43,61 @@ SVG_OBJECT_TAGS = (SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG, SVG_RECT_TAG) INKSTITCH_ATTRIBS = {} inkstitch_attribs = [ - 'ties', - 'force_lock_stitches', - # clone - 'clone', - # polyline - 'polyline', - # fill - 'angle', - 'auto_fill', - 'fill_method', - 'tangential_strategy', - 'join_style', - 'interlaced', - 'expand_mm', - 'fill_underlay', - 'fill_underlay_angle', - 'fill_underlay_inset_mm', - 'fill_underlay_max_stitch_length_mm', - 'fill_underlay_row_spacing_mm', - 'fill_underlay_skip_last', - 'max_stitch_length_mm', - 'min_stitch_length_mm', - 'row_spacing_mm', - 'end_row_spacing_mm', - 'skip_last', - 'staggers', - 'underlay_underpath', - 'underpath', - 'flip', - 'expand_mm', - # stroke - 'manual_stitch', - 'bean_stitch_repeats', - 'repeats', - 'running_stitch_length_mm', - # satin column - 'satin_column', - 'running_stitch_length_mm', - 'center_walk_underlay', - 'center_walk_underlay_stitch_length_mm', - 'contour_underlay', - 'contour_underlay_stitch_length_mm', - 'contour_underlay_inset_mm', - 'zigzag_underlay', - 'zigzag_spacing_mm', - 'zigzag_underlay_inset_mm', - 'zigzag_underlay_spacing_mm', - 'zigzag_underlay_max_stitch_length_mm', - 'e_stitch', - 'pull_compensation_mm', - 'stroke_first', - # Legacy - 'trim_after', - 'stop_after' - ] + 'ties', + 'force_lock_stitches', + # clone + 'clone', + # polyline + 'polyline', + # fill + 'angle', + 'auto_fill', + 'fill_method', + 'tangential_strategy', + 'join_style', + 'interlaced', + 'avoid_self_crossing', + 'expand_mm', + 'fill_underlay', + 'fill_underlay_angle', + 'fill_underlay_inset_mm', + 'fill_underlay_max_stitch_length_mm', + 'fill_underlay_row_spacing_mm', + 'fill_underlay_skip_last', + 'max_stitch_length_mm', + 'min_stitch_length_mm', + 'row_spacing_mm', + 'end_row_spacing_mm', + 'skip_last', + 'staggers', + 'underlay_underpath', + 'underpath', + 'flip', + 'expand_mm', + # stroke + 'manual_stitch', + 'bean_stitch_repeats', + 'repeats', + 'running_stitch_length_mm', + # satin column + 'satin_column', + 'running_stitch_length_mm', + 'center_walk_underlay', + 'center_walk_underlay_stitch_length_mm', + 'contour_underlay', + 'contour_underlay_stitch_length_mm', + 'contour_underlay_inset_mm', + 'zigzag_underlay', + 'zigzag_spacing_mm', + 'zigzag_underlay_inset_mm', + 'zigzag_underlay_spacing_mm', + 'zigzag_underlay_max_stitch_length_mm', + 'e_stitch', + 'pull_compensation_mm', + 'stroke_first', + # Legacy + 'trim_after', + 'stop_after' +] for attrib in inkstitch_attribs: INKSTITCH_ATTRIBS[attrib] = inkex.addNS(attrib, 'inkstitch') -- cgit v1.3.1 From 68ee0eea8733d613543a28627bd21a4481da8b46 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 23:48:46 -0400 Subject: add clockwise option --- lib/elements/fill_stitch.py | 8 +++++- .../tangential_fill_stitch_line_creator.py | 29 +++++++++++++++------- .../tangential_fill_stitch_pattern_creator.py | 12 --------- lib/svg/tags.py | 1 + 4 files changed, 28 insertions(+), 22 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index bc022ab3..2f8687a1 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -123,6 +123,11 @@ class FillStitch(EmbroideryElement): def avoid_self_crossing(self): return self.get_boolean_param('avoid_self_crossing', False) + @property + @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + def clockwise(self): + return self.get_boolean_param('clockwise', True) + @property @param('angle', _('Angle of lines of stitches'), @@ -580,7 +585,8 @@ class FillStitch(EmbroideryElement): self.interlaced, self.tangential_strategy, shgeo.Point(starting_point), - self.avoid_self_crossing + self.avoid_self_crossing, + self.clockwise ) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 61598b58..78213384 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -8,12 +8,12 @@ from shapely.geometry.polygon import LinearRing from shapely.geometry.polygon import orient from shapely.ops import polygonize +from .running_stitch import running_stitch +from ..stitch_plan import Stitch from ..stitches import constants from ..stitches import tangential_fill_stitch_pattern_creator -from ..stitch_plan import Stitch from ..utils import DotDict - -from .running_stitch import running_stitch +from ..utils.geometry import reverse_line_string class Tree(nx.DiGraph): @@ -59,15 +59,26 @@ def take_only_valid_linear_rings(rings): return LinearRing() -def make_tree_uniform_ccw(tree): +def orient_linear_ring(ring, clockwise=True): + # Unfortunately for us, Inkscape SVGs have an inverted Y coordinate. + # Normally we don't have to care about that, but in this very specific + # case, the meaning of is_ccw is flipped. It actually tests whether + # a ring is clockwise. That makes this logic super-confusing. + if ring.is_ccw != clockwise: + return reverse_line_string(ring) + else: + return ring + + +def make_tree_uniform(tree, clockwise=True): """ 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 nx.dfs_preorder_nodes(tree, 'root'): - if tree.nodes[node].type == "hole": - tree.nodes[node].val = LinearRing(reversed(tree.nodes[node].val.coords)) + + for node in tree.nodes.values(): + node.val = orient_linear_ring(node.val, clockwise) # Used to define which stitching strategy shall be used @@ -116,7 +127,7 @@ def check_and_prepare_tree_for_valid_spiral(tree): def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point, # noqa: C901 - avoid_self_crossing): + avoid_self_crossing, clockwise): """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. @@ -275,7 +286,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, tree.nodes[previous_hole].parent = current_poly tree.add_edge(current_poly, previous_hole) - make_tree_uniform_ccw(tree) + make_tree_uniform(tree, clockwise) if strategy == StitchingStrategy.INNER_TO_OUTER: connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 9b59fa07..2556d58c 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -24,7 +24,6 @@ nearest_neighbor_tuple = namedtuple( ) -@debug.time def get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): """ Find the first point along travel_line that is within threshold of next_line. @@ -108,7 +107,6 @@ def create_nearest_points_list( return children_nearest_points -@debug.time def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half, avoid_self_crossing, forward=True): """ @@ -213,13 +211,6 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, return LineString(result_coords) -def orient_linear_ring(ring): - if not ring.is_ccw: - return LinearRing(reversed(ring.coords)) - else: - return ring - - def reorder_linear_ring(ring, start): distances = ring - start start_index = np.argmin(np.linalg.norm(distances, axis=1)) @@ -245,9 +236,6 @@ def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): Return value: Path interpolated between two LinearRings, as a LineString. """ - ring1 = orient_linear_ring(ring1) - ring2 = orient_linear_ring(ring2) - # Resample the two LinearRings so that they are the same number of points # long. Then take the corresponding points in each ring and interpolate # between them, gradually going more toward ring2. diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 3f412c2e..02340aa5 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -57,6 +57,7 @@ inkstitch_attribs = [ 'join_style', 'interlaced', 'avoid_self_crossing', + 'clockwise', 'expand_mm', 'fill_underlay', 'fill_underlay_angle', -- cgit v1.3.1 From 5a69fa3e9c582a3bc21660342cea35837ae1eb9a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 14:34:21 -0400 Subject: add double (fermat) spiral --- lib/elements/fill_stitch.py | 2 +- .../tangential_fill_stitch_line_creator.py | 14 +++-- .../tangential_fill_stitch_pattern_creator.py | 66 +++++++++++++++++----- 3 files changed, 64 insertions(+), 18 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 2f8687a1..70d4f356 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -103,7 +103,7 @@ class FillStitch(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 78213384..69bb13f2 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -84,7 +84,8 @@ def make_tree_uniform(tree, clockwise=True): # Used to define which stitching strategy shall be used class StitchingStrategy(IntEnum): INNER_TO_OUTER = 0 - SPIRAL = 1 + SINGLE_SPIRAL = 1 + DOUBLE_SPIRAL = 2 def check_and_prepare_tree_for_valid_spiral(tree): @@ -158,7 +159,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, at this position """ - if strategy == StitchingStrategy.SPIRAL and len(poly.interiors) > 1: + if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1: raise ValueError( "Single spiral geometry must not have more than one hole!") @@ -293,10 +294,15 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, tree, 'root', abs(offset), stitch_distance, min_stitch_distance, starting_point, offset_by_half, avoid_self_crossing) path = [Stitch(*point) for point in connected_line.coords] return running_stitch(path, stitch_distance), "whatever" - elif strategy == StitchingStrategy.SPIRAL: + elif strategy == StitchingStrategy.SINGLE_SPIRAL: if not check_and_prepare_tree_for_valid_spiral(tree): raise ValueError("Geometry cannot be filled with one spiral!") - (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_spiral( + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral( + tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) + elif strategy == StitchingStrategy.DOUBLE_SPIRAL: + if not check_and_prepare_tree_for_valid_spiral(tree): + raise ValueError("Geometry cannot be filled with a double spiral!") + (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral( tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) else: raise ValueError("Invalid stitching stratety!") diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 2556d58c..edc6e0af 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -1,5 +1,5 @@ -import math from collections import namedtuple +from itertools import chain import networkx as nx import numpy as np import trimesh @@ -259,7 +259,7 @@ def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): return result.simplify(constants.simplification_threshold, False) -def connect_raster_tree_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 +def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them as a spiral. It expects that each node in the tree has max. one child @@ -281,21 +281,61 @@ def connect_raster_tree_spiral(tree, used_offset, stitch_distance, min_stitch_di placed at this position """ - if not tree['root']: # if node has no children - stitches = [Stitch(*point) for point in tree.nodes['root'].val.coords] - return running_stitch(stitches, stitch_distance) + starting_point = close_point.coords[0] + + rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] + + path = make_spiral(rings, stitch_distance, starting_point) + path = [Stitch(*stitch) for stitch in path] + + return running_stitch(path, stitch_distance), None + + +def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 + """ + Takes the offsetted curves organized as tree, connects and samples them as a spiral. + It expects that each node in the tree has max. one child + Input: + -tree: contains the offsetted curves in a hierarchical organized + data structure. + -used_offset: used offset when the offsetted curves were generated + -stitch_distance: maximum allowed distance between two points + after sampling + -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting + offsetted paths might be shorter. + -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. + Return values: + -All offsetted curves connected to one spiral and sampled with + points obeying stitch_distance and offset_by_half + -Tag (origin) of each point to analyze why a point was + placed at this position + """ starting_point = close_point.coords[0] - path = [] - for node in nx.dfs_preorder_nodes(tree, 'root'): - if tree[node]: - ring1 = tree.nodes[node].val - child = list(tree.successors(node))[0] - ring2 = tree.nodes[child].val - spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) - path.extend(spiral_part.coords) + rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] + path = make_fermat_spiral(rings, stitch_distance, starting_point) path = [Stitch(*stitch) for stitch in path] return running_stitch(path, stitch_distance), None + + +def make_fermat_spiral(rings, stitch_distance, starting_point): + forward = make_spiral(rings[::2], stitch_distance, starting_point) + back = make_spiral(rings[1::2], stitch_distance, starting_point) + back.reverse() + + return chain(forward, back) + + +def make_spiral(rings, stitch_distance, starting_point): + path = [] + + for ring1, ring2 in zip(rings[:-1], rings[1:]): + spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) + path.extend(spiral_part.coords) + + return path -- cgit v1.3.1 From aeeaf72338e2d7645309725be641d552a3c56190 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 16:58:55 -0400 Subject: wip --- lib/elements/fill_stitch.py | 14 +- lib/stitches/auto_fill.py | 12 +- .../tangential_fill_stitch_line_creator.py | 253 +++++++++------------ .../tangential_fill_stitch_pattern_creator.py | 14 +- lib/utils/geometry.py | 31 ++- 5 files changed, 154 insertions(+), 170 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 70d4f356..5e795f45 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -576,19 +576,17 @@ class FillStitch(EmbroideryElement): if not starting_point: starting_point = (0, 0) for poly in polygons: - connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( + connected_line = tangential_fill_stitch_line_creator.tangential_fill( poly, - -self.row_spacing, - self.join_style + 1, - self.max_stitch_length, - min(self.min_stitch_length, self.max_stitch_length), - self.interlaced, self.tangential_strategy, + self.row_spacing, + self.max_stitch_length, + self.join_style + 1, + self.clockwise, shgeo.Point(starting_point), self.avoid_self_crossing, - self.clockwise ) - path = [InkstitchPoint(*p) for p in connectedLine] + path = [InkstitchPoint(*p) for p in connected_line] stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 35412e93..630178c4 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -16,8 +16,7 @@ from shapely.strtree import STRtree from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM -from ..utils.geometry import Point as InkstitchPoint -from ..utils.geometry import line_string_to_point_list +from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list, ensure_multi_line_string from .fill import intersect_region_with_grating, stitch_row from .running_stitch import running_stitch @@ -396,15 +395,6 @@ def travel_grating(shape, angle, row_spacing): return shgeo.MultiLineString(list(segments)) -def ensure_multi_line_string(thing): - """Given either a MultiLineString or a single LineString, return a MultiLineString""" - - if isinstance(thing, shgeo.LineString): - return shgeo.MultiLineString([thing]) - else: - return thing - - def build_travel_edges(shape, fill_angle): r"""Given a graph, compute the interior travel edges. diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 69bb13f2..1c10c397 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -1,10 +1,7 @@ from enum import IntEnum import networkx as nx -from depq import DEPQ -from shapely.geometry import MultiLineString, Polygon -from shapely.geometry import MultiPolygon -from shapely.geometry.polygon import LinearRing +from shapely.geometry import Polygon, MultiPolygon, GeometryCollection from shapely.geometry.polygon import orient from shapely.ops import polygonize @@ -13,7 +10,7 @@ from ..stitch_plan import Stitch from ..stitches import constants from ..stitches import tangential_fill_stitch_pattern_creator from ..utils import DotDict -from ..utils.geometry import reverse_line_string +from ..utils.geometry import reverse_line_string, ensure_geometry_collection, ensure_multi_polygon class Tree(nx.DiGraph): @@ -23,15 +20,13 @@ class Tree(nx.DiGraph): def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): - result = Polygon(ring).buffer(offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) + result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) + result = ensure_multi_polygon(result) - if result.geom_type == 'Polygon': - return result.exterior - else: - result_list = [] - for poly in result.geoms: - result_list.append(poly.exterior) - return MultiLineString(result_list) + rings = GeometryCollection([poly.exterior for poly in result.geoms]) + rings = rings.simplify(constants.simplification_threshold, False) + + return take_only_valid_linear_rings(rings) def take_only_valid_linear_rings(rings): @@ -39,24 +34,14 @@ def take_only_valid_linear_rings(rings): Removes all geometries which do not form a "valid" LinearRing (meaning a ring which does not form a straight line) """ - if rings.geom_type == "MultiLineString": - new_list = [] - for ring in rings.geoms: - if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): - new_list.append(ring) - if len(new_list) == 1: - return LinearRing(new_list[0]) - else: - return MultiLineString(new_list) - elif rings.geom_type == "LineString" or rings.geom_type == "LinearRing": - if len(rings.coords) <= 2: - return LinearRing() - elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]: - return LinearRing() - else: - return rings - else: - return LinearRing() + + valid_rings = [] + + for ring in ensure_geometry_collection(rings).geoms: + if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): + valid_rings.append(ring) + + return GeometryCollection(valid_rings) def orient_linear_ring(ring, clockwise=True): @@ -127,8 +112,7 @@ def check_and_prepare_tree_for_valid_spiral(tree): return process_node('root') -def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point, # noqa: C901 - avoid_self_crossing, clockwise): +def offset_poly(poly, offset, join_style, clockwise): """ Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets. @@ -159,21 +143,9 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, at this position """ - if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1: - raise ValueError( - "Single spiral geometry must not have more than one hole!") - ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify( - constants.simplification_threshold, False) tree = Tree() - tree.add_node('root', - type='node', - parent=None, - val=ordered_poly.exterior, - already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), - ) + tree.add_node('root', type='node', parent=None, val=ordered_poly.exterior) active_polys = ['root'] active_holes = [[]] @@ -181,103 +153,26 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, node_num = 0 for hole in ordered_poly.interiors: - tree.add_node(node_num, - type="hole", - val=hole, - already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), - ) + tree.add_node(node_num, type="hole", val=hole) active_holes[0].append(node_num) node_num += 1 while len(active_polys) > 0: current_poly = active_polys.pop() current_holes = active_holes.pop() - poly_inners = [] + outer, inners = offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style) - outer = offset_linear_ring( - tree.nodes[current_poly].val, - offset, - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - outer = outer.simplify(constants.simplification_threshold, False) - outer = take_only_valid_linear_rings(outer) - - for hole in current_holes: - inner = offset_linear_ring( - tree.nodes[hole].val, - -offset, # take negative offset for holes - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - inner = inner.simplify(constants.simplification_threshold, False) - inner = take_only_valid_linear_rings(inner) - if not inner.is_empty: - poly_inners.append(Polygon(inner)) if not outer.is_empty: - if len(poly_inners) == 0: - if outer.geom_type == "LineString" or outer.geom_type == "LinearRing": - result = Polygon(outer) - else: - result = MultiPolygon(polygonize(outer)) - else: - if outer.geom_type == "LineString" or outer.geom_type == "LinearRing": - result = Polygon(outer).difference( - MultiPolygon(poly_inners)) - else: - result = MultiPolygon(polygonize(outer)).difference( - MultiPolygon(poly_inners)) - - if not result.is_empty and result.area > offset * offset / 10: - if result.geom_type == "Polygon": - result_list = [result] - else: - result_list = list(result.geoms) - - for polygon in result_list: - polygon = orient(polygon, -1) - - if polygon.area < offset * offset / 10: - continue - - polygon = polygon.simplify( - constants.simplification_threshold, False - ) - poly_coords = polygon.exterior - poly_coords = take_only_valid_linear_rings(poly_coords) - if poly_coords.is_empty: - continue - - node = node_num - node_num += 1 - tree.add_node(node, - type='node', - parent=current_poly, - val=poly_coords, - already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), - ) - tree.add_edge(current_poly, node) - active_polys.append(node) - hole_node_list = [] - for hole in polygon.interiors: - hole_node = node_num - node_num += 1 - tree.add_node(hole_node, - type="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(tree.nodes[previous_hole].val)): - tree.nodes[previous_hole].parent = hole_node - tree.add_edge(hole_node, previous_hole) - hole_node_list.append(hole_node) - active_holes.append(hole_node_list) + polygons = match_polygons_and_holes(outer, inners) + + if not polygons.is_empty: + for polygon in polygons.geoms: + new_polygon, new_holes = convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes) + + if new_polygon: + active_polys.append(new_polygon) + active_holes.append(new_holes) + for previous_hole in current_holes: # If the previous holes are not # contained in the new holes they @@ -289,22 +184,96 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, make_tree_uniform(tree, clockwise) + return tree + + +def offset_polygon_and_holes(tree, poly, holes, offset, join_style): + outer = offset_linear_ring( + tree.nodes[poly].val, + offset, + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + + inners = [] + for hole in holes: + inner = offset_linear_ring( + tree.nodes[hole].val, + -offset, # take negative offset for holes + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + if not inner.is_empty: + inners.append(Polygon(inner.geoms[0])) + + return outer, inners + + +def match_polygons_and_holes(outer, inners): + result = MultiPolygon(polygonize(outer)) + if len(inners) > 0: + result = ensure_geometry_collection(result.difference(MultiPolygon(inners))) + + return result + + +def convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): + polygon = orient(polygon, -1) + + if polygon.area < 0.1: + return None, None + + polygon = polygon.simplify(constants.simplification_threshold, False) + valid_rings = take_only_valid_linear_rings(polygon.exterior) + + try: + exterior = valid_rings.geoms[0] + except IndexError: + return None, None + + node = id(polygon) # just needs to be unique + + tree.add_node(node, type='node', parent=parent_polygon, val=exterior) + tree.add_edge(parent_polygon, node) + + hole_node_list = [] + for hole in polygon.interiors: + hole_node = id(hole) + tree.add_node(hole_node, type="hole", val=hole) + for previous_hole in child_holes: + if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)): + tree.nodes[previous_hole].parent = hole_node + tree.add_edge(hole_node, previous_hole) + hole_node_list.append(hole_node) + + return node, hole_node_list + + +def tangential_fill(poly, strategy, offset, stitch_distance, join_style, clockwise, starting_point, avoid_self_crossing): + if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1: + raise ValueError( + "Single spiral geometry must not have more than one hole!") + + tree = offset_poly(poly, offset, join_style, clockwise) + if strategy == StitchingStrategy.INNER_TO_OUTER: connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( - tree, 'root', abs(offset), stitch_distance, min_stitch_distance, starting_point, offset_by_half, avoid_self_crossing) + tree, 'root', offset, stitch_distance, starting_point, avoid_self_crossing) path = [Stitch(*point) for point in connected_line.coords] - return running_stitch(path, stitch_distance), "whatever" + return running_stitch(path, stitch_distance) elif strategy == StitchingStrategy.SINGLE_SPIRAL: if not check_and_prepare_tree_for_valid_spiral(tree): raise ValueError("Geometry cannot be filled with one spiral!") - (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral( - tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) + connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral( + tree, offset, stitch_distance, starting_point) elif strategy == StitchingStrategy.DOUBLE_SPIRAL: if not check_and_prepare_tree_for_valid_spiral(tree): raise ValueError("Geometry cannot be filled with a double spiral!") - (connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral( - tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) + connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral( + tree, offset, stitch_distance, starting_point) else: raise ValueError("Invalid stitching stratety!") - return connected_line, connected_line_origin + return connected_line diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index edc6e0af..a19c0a0a 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -107,8 +107,8 @@ def create_nearest_points_list( return children_nearest_points -def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, min_stitch_distance, starting_point, - offset_by_half, avoid_self_crossing, forward=True): +def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, starting_point, + avoid_self_crossing, forward=True): """ Takes the offset curves organized as a tree, connects and samples them. Strategy: A connection from parent to child is made as fast as possible to @@ -183,9 +183,7 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, child_connection.child_node, offset, stitch_distance, - min_stitch_distance, child_connection.nearest_point_child, - offset_by_half, avoid_self_crossing, not forward ) @@ -259,7 +257,7 @@ def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): return result.simplify(constants.simplification_threshold, False) -def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 +def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them as a spiral. It expects that each node in the tree has max. one child @@ -288,10 +286,10 @@ def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, min_st path = make_spiral(rings, stitch_distance, starting_point) path = [Stitch(*stitch) for stitch in path] - return running_stitch(path, stitch_distance), None + return running_stitch(path, stitch_distance) -def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901 +def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901 """ Takes the offsetted curves organized as tree, connects and samples them as a spiral. It expects that each node in the tree has max. one child @@ -320,7 +318,7 @@ def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, min_st path = make_fermat_spiral(rings, stitch_distance, starting_point) path = [Stitch(*stitch) for stitch in path] - return running_stitch(path, stitch_distance), None + return running_stitch(path, stitch_distance) def make_fermat_spiral(rings, stitch_distance, starting_point): diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index ed1e2c0e..86205f02 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -5,7 +5,7 @@ import math -from shapely.geometry import LineString, LinearRing +from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, GeometryCollection from shapely.geometry import Point as ShapelyPoint @@ -66,6 +66,35 @@ def reverse_line_string(line_string): return LineString(line_string.coords[::-1]) +def ensure_multi_line_string(thing): + """Given either a MultiLineString or a single LineString, return a MultiLineString""" + + if isinstance(thing, LineString): + return MultiLineString([thing]) + else: + return thing + + +def ensure_geometry_collection(thing): + """Given either some kind of geometry or a GeometryCollection, return a GeometryCollection""" + + if isinstance(thing, (MultiLineString, MultiPolygon)): + return GeometryCollection(thing.geoms) + elif isinstance(thing, GeometryCollection): + return thing + else: + return GeometryCollection([thing]) + + +def ensure_multi_polygon(thing): + """Given either a MultiPolygon or a single Polygon, return a MultiPolygon""" + + if isinstance(thing, Polygon): + return MultiPolygon([thing]) + else: + return thing + + def cut_path(points, length): """Return a subsection of at the start of the path that is length units long. -- cgit v1.3.1 From 330c6be78786b85ed2528cf2788e529cfda714fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 21:08:48 -0400 Subject: refactor, tidy, and C901 fixes --- lib/elements/fill_stitch.py | 51 +- lib/stitches/guided_fill.py | 16 +- lib/stitches/tangential_fill.py | 538 +++++++++++++++++++++ .../tangential_fill_stitch_line_creator.py | 279 ----------- .../tangential_fill_stitch_pattern_creator.py | 339 ------------- requirements.txt | 1 - 6 files changed, 574 insertions(+), 650 deletions(-) create mode 100644 lib/stitches/tangential_fill.py delete mode 100644 lib/stitches/tangential_fill_stitch_line_creator.py delete mode 100644 lib/stitches/tangential_fill_stitch_pattern_creator.py (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 5e795f45..d7b859b5 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,10 +15,9 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill, guided_fill +from ..stitches import tangential_fill, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning @@ -571,26 +570,40 @@ class FillStitch(EmbroideryElement): return [stitch_group] def do_tangential_fill(self, last_patch, starting_point): - stitch_groups = [] - polygons = self.fill_shape.geoms if not starting_point: starting_point = (0, 0) - for poly in polygons: - connected_line = tangential_fill_stitch_line_creator.tangential_fill( - poly, - self.tangential_strategy, - self.row_spacing, - self.max_stitch_length, - self.join_style + 1, - self.clockwise, - shgeo.Point(starting_point), - self.avoid_self_crossing, - ) - path = [InkstitchPoint(*p) for p in connected_line] + starting_point = shgeo.Point(starting_point) + + stitch_groups = [] + for polygon in self.fill_shape.geoms: + tree = tangential_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) + + stitches = [] + if self.tangential_strategy == 0: + stitches = tangential_fill.inner_to_outer( + tree, + self.row_spacing, + self.max_stitch_length, + starting_point, + self.avoid_self_crossing + ) + elif self.tangential_strategy == 1: + stitches = tangential_fill.single_spiral( + tree, + self.max_stitch_length, + starting_point + ) + elif self.tangential_strategy == 2: + stitches = tangential_fill.double_spiral( + tree, + self.max_stitch_length, + starting_point + ) + stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), - stitches=path) + stitches=stitches) stitch_groups.append(stitch_group) return stitch_groups @@ -611,13 +624,11 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, - min(self.min_stitch_length, self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, ending_point, - self.underpath, - self.interlaced)) + self.underpath)) return [stitch_group] @cache diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 40728c53..9694a546 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -11,19 +11,16 @@ from ..stitch_plan import Stitch from ..utils.geometry import Point as InkstitchPoint, reverse_line_string -@debug.time def guided_fill(shape, guideline, angle, row_spacing, max_stitch_length, - min_stitch_length, running_stitch_length, skip_last, starting_point, ending_point=None, - underpath=True, - offset_by_half=True): + underpath=True): try: segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing) fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point) @@ -36,15 +33,12 @@ def guided_fill(shape, 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, min_stitch_length, running_stitch_length, skip_last, offset_by_half) + result = path_to_stitches(path, travel_graph, fill_stitch_graph, max_stitch_length, running_stitch_length, skip_last) return result -@debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, min_stitch_length, - running_stitch_length, skip_last, offset_by_half): +def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, skip_last): path = collapse_sequential_outline_edges(path) stitches = [] @@ -62,7 +56,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, path_geometry = reverse_line_string(path_geometry) point_list = [Stitch(*point) for point in path_geometry.coords] - new_stitches = running_stitch(point_list, max_stitch_length) + new_stitches = running_stitch(point_list, stitch_length) # need to tag stitches @@ -124,7 +118,7 @@ def repair_non_simple_lines(line): counter += 1 if repaired.geom_type != 'LineString': raise ValueError( - _("Guide line (or offsetted instance) is self crossing!")) + _("Guide line (or offset copy) is self crossing!")) else: return repaired diff --git a/lib/stitches/tangential_fill.py b/lib/stitches/tangential_fill.py new file mode 100644 index 00000000..3cc3335f --- /dev/null +++ b/lib/stitches/tangential_fill.py @@ -0,0 +1,538 @@ +from collections import namedtuple +from itertools import chain + +import networkx as nx +import numpy as np +import trimesh +from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, MultiLineString, Point +from shapely.geometry.polygon import orient +from shapely.ops import nearest_points +from shapely.ops import polygonize + +from .running_stitch import running_stitch +from ..i18n import _ +from ..stitch_plan import Stitch +from ..stitches import constants +from ..utils import DotDict +from ..utils.geometry import cut, reverse_line_string, roll_linear_ring +from ..utils.geometry import ensure_geometry_collection, ensure_multi_polygon + + +class Tree(nx.DiGraph): + # This lets us do tree.nodes['somenode'].parent instead of the default + # tree.nodes['somenode']['parent']. + node_attr_dict_factory = DotDict + + def __init__(self, *args, **kwargs): + self.__node_num = 0 + super().__init__(**kwargs) + + def generate_node_name(self): + node = self.__node_num + self.__node_num += 1 + + return node + + +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) + + +def _offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): + result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) + result = ensure_multi_polygon(result) + + rings = GeometryCollection([poly.exterior for poly in result.geoms]) + rings = rings.simplify(constants.simplification_threshold, False) + + return _take_only_valid_linear_rings(rings) + + +def _take_only_valid_linear_rings(rings): + """ + Removes all geometries which do not form a "valid" LinearRing. + + A "valid" ring is one that does not form a straight line. + """ + + valid_rings = [] + + for ring in ensure_geometry_collection(rings).geoms: + if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): + valid_rings.append(ring) + + return GeometryCollection(valid_rings) + + +def _orient_linear_ring(ring, clockwise=True): + # Unfortunately for us, Inkscape SVGs have an inverted Y coordinate. + # Normally we don't have to care about that, but in this very specific + # case, the meaning of is_ccw is flipped. It actually tests whether + # a ring is clockwise. That makes this logic super-confusing. + if ring.is_ccw != clockwise: + return reverse_line_string(ring) + else: + return ring + + +def _orient_tree(tree, clockwise=True): + """ + Orient all linear rings in the tree. + + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree uniform (having all the same ordering + direction) + """ + + for node in tree.nodes.values(): + node.val = _orient_linear_ring(node.val, clockwise) + + +def offset_polygon(polygon, offset, join_style, clockwise): + """ + Convert a polygon to a tree of isocontours. + + An isocontour is an offset version of the polygon's boundary. For example, + the isocontours of a circle are a set of concentric circles inside the + circle. + + This function takes a polygon (which may have holes) as input and creates + isocontours until the polygon is filled completely. The isocontours are + returned as a Tree, with a parent-child relationship indicating that the + parent isocontour contains the child isocontour. + + Arguments: + polygon - The shapely Polygon which may have holes + offset - The spacing between isocontours + join_style - Join style used when offsetting the Polygon border to create + isocontours. Can be round, mitered or bevel, as defined by + shapely: + https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE + clockwise - If True, isocontour points are in clockwise order; if False, counter-clockwise. + + Return Value: + Tree - see above + """ + + ordered_polygon = orient(polygon, -1) + tree = Tree() + tree.add_node('root', type='node', parent=None, val=ordered_polygon.exterior) + active_polygons = ['root'] + active_holes = [[]] + + for hole in ordered_polygon.interiors: + hole_node = tree.generate_node_name() + tree.add_node(hole_node, type="hole", val=hole) + active_holes[0].append(hole_node) + + while len(active_polygons) > 0: + current_poly = active_polygons.pop() + current_holes = active_holes.pop() + + outer, inners = _offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style) + polygons = _match_polygons_and_holes(outer, inners) + + for polygon in polygons.geoms: + new_polygon, new_holes = _convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes) + + if new_polygon is not None: + active_polygons.append(new_polygon) + active_holes.append(new_holes) + + 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 not tree.nodes[previous_hole].parent: + tree.nodes[previous_hole].parent = current_poly + tree.add_edge(current_poly, previous_hole) + + _orient_tree(tree, clockwise) + return tree + + +def _offset_polygon_and_holes(tree, poly, holes, offset, join_style): + outer = _offset_linear_ring( + tree.nodes[poly].val, + offset, + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + + inners = [] + for hole in holes: + inner = _offset_linear_ring( + tree.nodes[hole].val, + -offset, # take negative offset for holes + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + if not inner.is_empty: + inners.append(Polygon(inner.geoms[0])) + + return outer, inners + + +def _match_polygons_and_holes(outer, inners): + result = MultiPolygon(polygonize(outer.geoms)) + if len(inners) > 0: + result = ensure_geometry_collection(result.difference(MultiPolygon(inners))) + + return result + + +def _convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): + polygon = orient(polygon, -1) + + if polygon.area < 0.1: + return None, None + + polygon = polygon.simplify(constants.simplification_threshold, False) + valid_rings = _take_only_valid_linear_rings(polygon.exterior) + + try: + exterior = valid_rings.geoms[0] + except IndexError: + return None, None + + node = tree.generate_node_name() + tree.add_node(node, type='node', parent=parent_polygon, val=exterior) + tree.add_edge(parent_polygon, node) + + hole_nodes = [] + for hole in polygon.interiors: + hole_node = tree.generate_node_name() + tree.add_node(hole_node, type="hole", val=hole) + for previous_hole in child_holes: + if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)): + tree.nodes[previous_hole].parent = hole_node + tree.add_edge(hole_node, previous_hole) + hole_nodes.append(hole_node) + + return node, hole_nodes + + +def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): + """ + Find the first point along travel_line that is within threshold of 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 + threshold - The distance between travel_line and next_line needs + to below threshold to be a valid point for entering + + Return value: + tuple or None + - the tuple structure is: + (point in travel_line, point in next_line) + - None is returned if there is no point that satisfies the threshold. + """ + + # We'll buffer next_line and find the intersection with travel_line. + # Then we'll return the very first point in the intersection, + # matched with a corresponding point on next_line. Fortunately for + # us, intersection of a Polygon with a LineString yields pieces of + # the LineString in the same order as the input LineString. + threshold_area = next_line.buffer(threshold) + portion_within_threshold = travel_line.intersection(threshold_area) + + if portion_within_threshold.is_empty: + return None + else: + if isinstance(portion_within_threshold, MultiLineString): + portion_within_threshold = portion_within_threshold.geoms[0] + + parent_point = Point(portion_within_threshold.coords[0]) + return nearest_points(parent_point, next_line) + + +def _create_nearest_points_list( + travel_line, tree, children, threshold, threshold_hard): + """Determine the best place to enter each of parent's children + + Arguments: + travel_line - The "parent" line for which the distance should + be minimized to enter each child + children - children of travel_line that need to be entered + threshold - The distance between travel_line and a child should + to be below threshold to be a valid point for entering + threshold_hard - As a last resort, we can accept an entry point + that is this far way + + Return value: + list of nearest_neighbor_tuple - indicating where to enter each + respective child + """ + + children_nearest_points = [] + + for child in children: + result = _get_nearest_points_closer_than_thresh(travel_line, tree.nodes[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, tree.nodes[child].val, threshold_hard) + + proj = travel_line.project(result[0]) + children_nearest_points.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + return children_nearest_points + + +def _find_path_inner_to_outer(tree, node, offset, starting_point, + avoid_self_crossing, forward=True): + """Find a stitch path for this ring and its children. + + 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. + + This function calls itself recursively to find a stitch path for each child + (and its children). + + Arguments: + tree - a Tree of isocontours (as returned by offset_polygon) + offset - offset that was passed to offset_polygon + starting_point - starting point for stitching + avoid_self_crossing - if True, tries to generate a path that does not + cross itself. + forward - if True, this ring will be stitched in its natural direction + (used internally by avoid_self_crossing) + + Return value: + LineString -- the stitching path + """ + + current_node = tree.nodes[node] + current_ring = current_node.val + + if not forward and avoid_self_crossing: + current_ring = reverse_line_string(current_ring) + + # reorder the coordinates of this ring so that it starts with + # a point nearest the starting_point + start_distance = current_ring.project(starting_point) + current_ring = roll_linear_ring(current_ring, start_distance) + current_node.val = current_ring + + # Find where along this ring to connect to each child. + nearest_points_list = _create_nearest_points_list( + current_ring, + tree, + tree[node], + constants.offset_factor_for_adjacent_geometry * offset, + 2.05 * offset + ) + nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) + + result_coords = [] + if not nearest_points_list: + # We have no children, so we're at the center of a spiral. Reversing + # the ring gives a nicer visual appearance. + # current_ring = reverse_line_string(current_ring) + pass + else: + # This is a recursive algorithm. We'll stitch along this ring, pausing + # to jump to each child ring in turn and sew it before continuing on + # this ring. We'll end back where we started. + + result_coords.append(current_ring.coords[0]) + distance_so_far = 0 + for child_connection in nearest_points_list: + # Cut this ring into pieces before and after where this child will connect. + before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far) + distance_so_far += child_connection.proj_distance_parent + + # Stitch the part leading up to this child. + if before is not None: + result_coords.extend(before.coords) + + # Stitch this child. The child will start and end in the same + # place, which should be close to our current location. + child_path = _find_path_inner_to_outer( + tree, + child_connection.child_node, + offset, + child_connection.nearest_point_child, + avoid_self_crossing, + not forward + ) + result_coords.extend(child_path.coords) + + # Skip ahead a little bit on this ring before resuming. This + # gives a nice spiral pattern, where we spiral out from the + # innermost child. + if after is not None: + skip, after = cut(after, offset) + distance_so_far += offset + + current_ring = after + + if current_ring is not None: + # skip a little at the end so we don't end exactly where we started. + remaining_length = current_ring.length + if remaining_length > offset: + current_ring, skip = cut(current_ring, current_ring.length - offset) + + result_coords.extend(current_ring.coords) + + return LineString(result_coords) + + +def inner_to_outer(tree, offset, stitch_length, starting_point, avoid_self_crossing): + """Fill a shape with spirals, from innermost to outermost.""" + + stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing) + points = [Stitch(*point) for point in stitch_path.coords] + stitches = running_stitch(points, stitch_length) + + return stitches + + +def _reorder_linear_ring(ring, start): + distances = ring - start + start_index = np.argmin(np.linalg.norm(distances, axis=1)) + return np.roll(ring, -start_index, axis=0) + + +def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): + """ + Interpolate between two LinearRings + + Creates a path from start_point on ring1 and around the rings, ending at a + nearby point on ring2. The path will smoothly transition from ring1 to + ring2 as it travels around the rings. + + Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py + + Arguments: + ring1 -- LinearRing start point will lie on + ring2 -- LinearRing end point will lie on + max_stitch_length -- maximum stitch length (used to calculate resampling accuracy) + start -- Point on ring1 to start at, as a tuple + + Return value: Path interpolated between two LinearRings, as a LineString. + """ + + # Resample the two LinearRings so that they are the same number of points + # long. Then take the corresponding points in each ring and interpolate + # between them, gradually going more toward ring2. + # + # This is a little less accurate than the method in interpolate(), but several + # orders of magnitude faster because we're not building and querying a KDTree. + + num_points = int(20 * ring1.length / max_stitch_length) + ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points) + ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points) + + if start is not None: + ring1_resampled = _reorder_linear_ring(ring1_resampled, start) + ring2_resampled = _reorder_linear_ring(ring2_resampled, start) + + weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1)) + points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights) + result = LineString(points) + + # TODO: remove when rastering is cheaper + return result.simplify(constants.simplification_threshold, False) + + +def _check_and_prepare_tree_for_valid_spiral(tree): + """Check whether spiral fill is possible, and tweak if necessary. + + Takes a tree consisting of isocontours. If a parent has more than one child + we cannot create a spiral. However, to make the routine more robust, we + allow more than one child if only one of the children has own children. The + other children are removed in this routine then. If the routine returns true, + the tree will have been cleaned up from unwanted children. + + If even with these weaker constraints, a spiral is not possible, False is + returned. + """ + + def process_node(node): + children = set(tree[node]) + + if len(children) == 0: + return True + elif len(children) == 1: + child = children.pop() + return process_node(child) + else: + children_with_children = {child for child in children if tree[child]} + if len(children_with_children) > 1: + # Node has multiple children with children, so a perfect spiral is not possible. + # This False value will be returned all the way up the stack. + return False + elif len(children_with_children) == 1: + children_without_children = children - children_with_children + child = children_with_children.pop() + tree.remove_nodes_from(children_without_children) + return process_node(child) + else: + # None of the children has its own children, so we'll just take the longest. + longest = max(children, key=lambda child: tree[child]['val'].length) + shorter_children = children - {longest} + tree.remove_nodes_from(shorter_children) + return process_node(longest) + + return process_node('root') + + +def single_spiral(tree, stitch_length, starting_point): + """Fill a shape with a single spiral going from outside to center.""" + return _spiral_fill(tree, stitch_length, starting_point, _make_spiral) + + +def double_spiral(tree, stitch_length, starting_point): + """Fill a shape with a double spiral going from outside to center and back to outside. """ + return _spiral_fill(tree, stitch_length, starting_point, _make_fermat_spiral) + + +def _spiral_fill(tree, stitch_length, close_point, spiral_maker): + if not _check_and_prepare_tree_for_valid_spiral(tree): + raise ValueError(_("Shape cannot be filled with single or double spiral!")) + + starting_point = close_point.coords[0] + rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] + path = spiral_maker(rings, stitch_length, starting_point) + path = [Stitch(*stitch) for stitch in path] + + return running_stitch(path, stitch_length) + + +def _make_fermat_spiral(rings, stitch_length, starting_point): + forward = _make_spiral(rings[::2], stitch_length, starting_point) + back = _make_spiral(rings[1::2], stitch_length, starting_point) + back.reverse() + + return chain(forward, back) + + +def _make_spiral(rings, stitch_length, starting_point): + path = [] + + for ring1, ring2 in zip(rings[:-1], rings[1:]): + spiral_part = _interpolate_linear_rings(ring1, ring2, stitch_length, starting_point) + path.extend(spiral_part.coords) + + return path diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py deleted file mode 100644 index 1c10c397..00000000 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ /dev/null @@ -1,279 +0,0 @@ -from enum import IntEnum - -import networkx as nx -from shapely.geometry import Polygon, MultiPolygon, GeometryCollection -from shapely.geometry.polygon import orient -from shapely.ops import polygonize - -from .running_stitch import running_stitch -from ..stitch_plan import Stitch -from ..stitches import constants -from ..stitches import tangential_fill_stitch_pattern_creator -from ..utils import DotDict -from ..utils.geometry import reverse_line_string, ensure_geometry_collection, ensure_multi_polygon - - -class Tree(nx.DiGraph): - # This lets us do tree.nodes['somenode'].parent instead of the default - # tree.nodes['somenode']['parent']. - node_attr_dict_factory = DotDict - - -def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): - result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) - result = ensure_multi_polygon(result) - - rings = GeometryCollection([poly.exterior for poly in result.geoms]) - rings = rings.simplify(constants.simplification_threshold, False) - - return take_only_valid_linear_rings(rings) - - -def take_only_valid_linear_rings(rings): - """ - Removes all geometries which do not form a "valid" LinearRing - (meaning a ring which does not form a straight line) - """ - - valid_rings = [] - - for ring in ensure_geometry_collection(rings).geoms: - if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): - valid_rings.append(ring) - - return GeometryCollection(valid_rings) - - -def orient_linear_ring(ring, clockwise=True): - # Unfortunately for us, Inkscape SVGs have an inverted Y coordinate. - # Normally we don't have to care about that, but in this very specific - # case, the meaning of is_ccw is flipped. It actually tests whether - # a ring is clockwise. That makes this logic super-confusing. - if ring.is_ccw != clockwise: - return reverse_line_string(ring) - else: - return ring - - -def make_tree_uniform(tree, clockwise=True): - """ - 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 tree.nodes.values(): - node.val = orient_linear_ring(node.val, clockwise) - - -# Used to define which stitching strategy shall be used -class StitchingStrategy(IntEnum): - INNER_TO_OUTER = 0 - SINGLE_SPIRAL = 1 - DOUBLE_SPIRAL = 2 - - -def check_and_prepare_tree_for_valid_spiral(tree): - """ - Takes a tree consisting of offsetted curves. If a parent has more than one child we - cannot create a spiral. However, to make the routine more robust, we allow more than - one child if only one of the childs has own childs. The other childs are removed in this - routine then. If the routine returns true, the tree will have been cleaned up from unwanted - childs. If the routine returns false even under the mentioned weaker conditions the - tree cannot be connected by one spiral. - """ - - def process_node(node): - children = set(tree[node]) - - if len(children) == 0: - return True - elif len(children) == 1: - child = children.pop() - return process_node(child) - else: - children_with_children = {child for child in children if tree[child]} - if len(children_with_children) > 1: - # Node has multiple children with children, so a perfect spiral is not possible. - # This False value will be returned all the way up the stack. - return False - elif len(children_with_children) == 1: - children_without_children = children - children_with_children - child = children_with_children.pop() - tree.remove_nodes_from(children_without_children) - return process_node(child) - else: - # None of the children has its own children, so we'll just take the longest. - longest = max(children, key=lambda child: tree[child]['val'].length) - shorter_children = children - {longest} - tree.remove_nodes_from(shorter_children) - return process_node(longest) - - return process_node('root') - - -def offset_poly(poly, offset, join_style, clockwise): - """ - 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 - -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting - offsetted paths might be shorter. - -offset_by_half: True if the points shall be interlaced - -strategy: According to StitchingStrategy enum class you can select between - different strategies for the connection between parent and childs. In - addition it offers the option "SPIRAL" which creates a real spiral towards inner. - In contrast to the other two options, "SPIRAL" does not end at the starting point - but at the innermost point - -starting_point: Defines the starting point for the stitching - -avoid_self_crossing: don't let the path cross itself when using the Inner to Outer strategy - 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) - tree = Tree() - tree.add_node('root', type='node', parent=None, val=ordered_poly.exterior) - active_polys = ['root'] - active_holes = [[]] - - # We don't care about the names of the nodes, we just need them to be unique. - node_num = 0 - - for hole in ordered_poly.interiors: - tree.add_node(node_num, type="hole", val=hole) - active_holes[0].append(node_num) - node_num += 1 - - while len(active_polys) > 0: - current_poly = active_polys.pop() - current_holes = active_holes.pop() - outer, inners = offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style) - - if not outer.is_empty: - polygons = match_polygons_and_holes(outer, inners) - - if not polygons.is_empty: - for polygon in polygons.geoms: - new_polygon, new_holes = convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes) - - if new_polygon: - active_polys.append(new_polygon) - active_holes.append(new_holes) - - 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 not tree.nodes[previous_hole].parent: - tree.nodes[previous_hole].parent = current_poly - tree.add_edge(current_poly, previous_hole) - - make_tree_uniform(tree, clockwise) - - return tree - - -def offset_polygon_and_holes(tree, poly, holes, offset, join_style): - outer = offset_linear_ring( - tree.nodes[poly].val, - offset, - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - - inners = [] - for hole in holes: - inner = offset_linear_ring( - tree.nodes[hole].val, - -offset, # take negative offset for holes - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - if not inner.is_empty: - inners.append(Polygon(inner.geoms[0])) - - return outer, inners - - -def match_polygons_and_holes(outer, inners): - result = MultiPolygon(polygonize(outer)) - if len(inners) > 0: - result = ensure_geometry_collection(result.difference(MultiPolygon(inners))) - - return result - - -def convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): - polygon = orient(polygon, -1) - - if polygon.area < 0.1: - return None, None - - polygon = polygon.simplify(constants.simplification_threshold, False) - valid_rings = take_only_valid_linear_rings(polygon.exterior) - - try: - exterior = valid_rings.geoms[0] - except IndexError: - return None, None - - node = id(polygon) # just needs to be unique - - tree.add_node(node, type='node', parent=parent_polygon, val=exterior) - tree.add_edge(parent_polygon, node) - - hole_node_list = [] - for hole in polygon.interiors: - hole_node = id(hole) - tree.add_node(hole_node, type="hole", val=hole) - for previous_hole in child_holes: - if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)): - tree.nodes[previous_hole].parent = hole_node - tree.add_edge(hole_node, previous_hole) - hole_node_list.append(hole_node) - - return node, hole_node_list - - -def tangential_fill(poly, strategy, offset, stitch_distance, join_style, clockwise, starting_point, avoid_self_crossing): - if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1: - raise ValueError( - "Single spiral geometry must not have more than one hole!") - - tree = offset_poly(poly, offset, join_style, clockwise) - - if strategy == StitchingStrategy.INNER_TO_OUTER: - connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer( - tree, 'root', offset, stitch_distance, starting_point, avoid_self_crossing) - path = [Stitch(*point) for point in connected_line.coords] - return running_stitch(path, stitch_distance) - elif strategy == StitchingStrategy.SINGLE_SPIRAL: - if not check_and_prepare_tree_for_valid_spiral(tree): - raise ValueError("Geometry cannot be filled with one spiral!") - connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral( - tree, offset, stitch_distance, starting_point) - elif strategy == StitchingStrategy.DOUBLE_SPIRAL: - if not check_and_prepare_tree_for_valid_spiral(tree): - raise ValueError("Geometry cannot be filled with a double spiral!") - connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral( - tree, offset, stitch_distance, starting_point) - else: - raise ValueError("Invalid stitching stratety!") - - return connected_line diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py deleted file mode 100644 index a19c0a0a..00000000 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ /dev/null @@ -1,339 +0,0 @@ -from collections import namedtuple -from itertools import chain -import networkx as nx -import numpy as np -import trimesh -from shapely.geometry import Point, LineString, LinearRing, MultiLineString -from shapely.ops import nearest_points - -from .running_stitch import running_stitch - -from ..debug import debug -from ..stitches import constants -from ..stitch_plan import Stitch -from ..utils.geometry import cut, roll_linear_ring, reverse_line_string - -nearest_neighbor_tuple = namedtuple( - "nearest_neighbor_tuple", - [ - "nearest_point_parent", - "nearest_point_child", - "proj_distance_parent", - "child_node", - ], -) - - -def get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): - """ - Find the first point along travel_line that is within threshold of 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 - -threshold: The distance between travel_line and next_line needs - to below threshold to be a valid point for entering - - Output: - -tuple or None - - the tuple structure is: - (nearest point in travel_line, nearest point in next_line) - - None is returned if there is no point that satisfies the threshold. - """ - - # We'll buffer next_line and find the intersection with travel_line. - # Then we'll return the very first point in the intersection, - # matched with a corresponding point on next_line. Fortunately for - # us, intersection of a Polygon with a LineString yields pieces of - # the LineString in the same order as the input LineString. - threshold_area = next_line.buffer(threshold) - portion_within_threshold = travel_line.intersection(threshold_area) - - if portion_within_threshold.is_empty: - return None - else: - if isinstance(portion_within_threshold, MultiLineString): - portion_within_threshold = portion_within_threshold.geoms[0] - - parent_point = Point(portion_within_threshold.coords[0]) - return nearest_points(parent_point, next_line) - - -def create_nearest_points_list( - travel_line, tree, children_list, threshold, threshold_hard): - """ - 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) - """ - - children_nearest_points = [] - - for child in children_list: - result = get_nearest_points_closer_than_thresh(travel_line, tree.nodes[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, tree.nodes[child].val, threshold_hard) - - proj = travel_line.project(result[0]) - children_nearest_points.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - return children_nearest_points - - -def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, starting_point, - avoid_self_crossing, forward=True): - """ - Takes the offset curves organized as a 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 - -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting - offsetted paths might be shorter. - -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_node = tree.nodes[node] - current_ring = current_node.val - - if not forward and avoid_self_crossing: - current_ring = reverse_line_string(current_ring) - - # reorder the coordinates of this ring so that it starts with - # a point nearest the starting_point - start_distance = current_ring.project(starting_point) - current_ring = roll_linear_ring(current_ring, start_distance) - current_node.val = current_ring - - # Find where along this ring to connect to each child. - nearest_points_list = create_nearest_points_list( - current_ring, - tree, - tree[node], - constants.offset_factor_for_adjacent_geometry * offset, - 2.05 * offset - ) - nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) - - result_coords = [] - if not nearest_points_list: - # We have no children, so we're at the center of a spiral. Reversing - # the ring gives a nicer visual appearance. - # current_ring = reverse_line_string(current_ring) - pass - else: - # This is a recursive algorithm. We'll stitch along this ring, pausing - # to jump to each child ring in turn and sew it before continuing on - # this ring. We'll end back where we started. - - result_coords.append(current_ring.coords[0]) - distance_so_far = 0 - for child_connection in nearest_points_list: - # Cut this ring into pieces before and after where this child will connect. - before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far) - distance_so_far += child_connection.proj_distance_parent - - # Stitch the part leading up to this child. - if before is not None: - result_coords.extend(before.coords) - - # Stitch this child. The child will start and end in the same - # place, which should be close to our current location. - child_path = connect_raster_tree_from_inner_to_outer( - tree, - child_connection.child_node, - offset, - stitch_distance, - child_connection.nearest_point_child, - avoid_self_crossing, - not forward - ) - result_coords.extend(child_path.coords) - - # Skip ahead a little bit on this ring before resuming. This - # gives a nice spiral pattern, where we spiral out from the - # innermost child. - if after is not None: - skip, after = cut(after, offset) - distance_so_far += offset - - current_ring = after - - if current_ring is not None: - # skip a little at the end so we don't end exactly where we started. - remaining_length = current_ring.length - if remaining_length > offset: - current_ring, skip = cut(current_ring, current_ring.length - offset) - - result_coords.extend(current_ring.coords) - - return LineString(result_coords) - - -def reorder_linear_ring(ring, start): - distances = ring - start - start_index = np.argmin(np.linalg.norm(distances, axis=1)) - return np.roll(ring, -start_index, axis=0) - - -def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): - """ - Interpolate between two LinearRings - - Creates a path from start_point on ring1 and around the rings, ending at a - nearby point on ring2. The path will smoothly transition from ring1 to - ring2 as it travels around the rings. - - Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py - - Arguments: - ring1 -- LinearRing start point will lie on - ring2 -- LinearRing end point will lie on - max_stitch_length -- maximum stitch length (used to calculate resampling accuracy) - start -- Point on ring1 to start at, as a tuple - - Return value: Path interpolated between two LinearRings, as a LineString. - """ - - # Resample the two LinearRings so that they are the same number of points - # long. Then take the corresponding points in each ring and interpolate - # between them, gradually going more toward ring2. - # - # This is a little less accurate than the method in interpolate(), but several - # orders of magnitude faster because we're not building and querying a KDTree. - - num_points = int(20 * ring1.length / max_stitch_length) - ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points) - ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points) - - if start is not None: - ring1_resampled = reorder_linear_ring(ring1_resampled, start) - ring2_resampled = reorder_linear_ring(ring2_resampled, start) - - weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1)) - points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights) - result = LineString(points) - - # TODO: remove when rastering is cheaper - return result.simplify(constants.simplification_threshold, False) - - -def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901 - """ - Takes the offsetted curves organized as tree, connects and samples them as a spiral. - It expects that each node in the tree has max. one child - Input: - -tree: contains the offsetted curves in a hierarchical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting - offsetted paths might be shorter. - -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. - Return values: - -All offsetted curves connected to one spiral and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - starting_point = close_point.coords[0] - - rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] - - path = make_spiral(rings, stitch_distance, starting_point) - path = [Stitch(*stitch) for stitch in path] - - return running_stitch(path, stitch_distance) - - -def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901 - """ - Takes the offsetted curves organized as tree, connects and samples them as a spiral. - It expects that each node in the tree has max. one child - Input: - -tree: contains the offsetted curves in a hierarchical organized - data structure. - -used_offset: used offset when the offsetted curves were generated - -stitch_distance: maximum allowed distance between two points - after sampling - -min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting - offsetted paths might be shorter. - -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. - Return values: - -All offsetted curves connected to one spiral and sampled with - points obeying stitch_distance and offset_by_half - -Tag (origin) of each point to analyze why a point was - placed at this position - """ - - starting_point = close_point.coords[0] - - rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] - - path = make_fermat_spiral(rings, stitch_distance, starting_point) - path = [Stitch(*stitch) for stitch in path] - - return running_stitch(path, stitch_distance) - - -def make_fermat_spiral(rings, stitch_distance, starting_point): - forward = make_spiral(rings[::2], stitch_distance, starting_point) - back = make_spiral(rings[1::2], stitch_distance, starting_point) - back.reverse() - - return chain(forward, back) - - -def make_spiral(rings, stitch_distance, starting_point): - path = [] - - for ring1, ring2 in zip(rings[:-1], rings[1:]): - spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) - path.extend(spiral_part.coords) - - return path diff --git a/requirements.txt b/requirements.txt index 585b1dac..e0b07b0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,6 @@ stringcase tinycss2 flask fonttools -depq trimesh scipy==1.7.3 -- cgit v1.3.1 From 48d0a0250e2787a3351137172924d1c4d277f002 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Wed, 4 May 2022 18:27:12 +0200 Subject: undo build changes for depq, update clone --- lib/elements/clone.py | 2 +- lib/elements/utils.py | 7 ++++--- lib/stitches/guided_fill.py | 1 - requirements.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 3f133471..303c1c2f 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -70,7 +70,7 @@ class Clone(EmbroideryElement): def clone_to_element(self, node): from .utils import node_to_elements - return node_to_elements(node) + return node_to_elements(node, True) def to_stitch_groups(self, last_patch=None): patches = [] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 561188aa..dafde759 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -19,11 +19,12 @@ from .stroke import Stroke from .text import TextObject -def node_to_elements(node): # noqa: C901 +def node_to_elements(node, clone_to_element=False): # noqa: C901 if node.tag == SVG_POLYLINE_TAG: return [Polyline(node)] - elif is_clone(node): + elif is_clone(node) and not clone_to_element: + # clone_to_element: get an actual embroiderable element once a clone has been defined as a clone return [Clone(node)] elif node.tag == SVG_PATH_TAG and not node.get('d', ''): @@ -32,7 +33,7 @@ def node_to_elements(node): # noqa: C901 elif has_marker(node): return [MarkerObject(node)] - elif node.tag in EMBROIDERABLE_TAGS: + elif node.tag in EMBROIDERABLE_TAGS or is_clone(node): element = EmbroideryElement(node) if element.get_boolean_param("satin_column") and element.get_style("stroke"): diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 9694a546..e4918e1d 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -5,7 +5,6 @@ from .auto_fill import (build_fill_stitch_graph, build_travel_graph, collapse_sequential_outline_edges, fallback, find_stitch_path, graph_is_valid, travel) from .running_stitch import running_stitch -from ..debug import debug from ..i18n import _ from ..stitch_plan import Stitch from ..utils.geometry import Point as InkstitchPoint, reverse_line_string diff --git a/requirements.txt b/requirements.txt index e0b07b0b..4ff2cc0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ tinycss2 flask fonttools trimesh -scipy==1.7.3 +scipy pywinutils; sys.platform == 'win32' pywin32; sys.platform == 'win32' -- cgit v1.3.1 From e65aaebbcab1ca6fbcf99d9f3665af423e02c2f5 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Wed, 4 May 2022 20:04:39 +0200 Subject: rebase corrections --- lib/elements/clone.py | 6 ------ lib/extensions/base.py | 15 ++++++++++++--- lib/stitches/fill.py | 1 - lib/svg/tags.py | 10 ++++++++-- 4 files changed, 20 insertions(+), 12 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 303c1c2f..d9185012 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,13 +5,7 @@ from math import atan, degrees -<<<<<<< HEAD -from ..commands import is_command, is_command_symbol -======= -import inkex - from ..commands import is_command_symbol ->>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 949f947e..cf94714c 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -8,10 +8,12 @@ import os import re from collections.abc import MutableMapping -import inkex from lxml import etree +from lxml.etree import Comment from stringcase import snakecase +import inkex + from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone @@ -19,7 +21,8 @@ from ..i18n import _ from ..marker import has_marker from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, + SVG_GROUP_TAG, SVG_MASK_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -129,6 +132,10 @@ class InkstitchExtension(inkex.Effect): def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901 nodes = [] + + if node.tag == Comment: + return [] + element = EmbroideryElement(node) if element.has_command('ignore_object'): @@ -141,7 +148,9 @@ class InkstitchExtension(inkex.Effect): if (node.tag in EMBROIDERABLE_TAGS or node.tag == SVG_GROUP_TAG) and element.get_style('display', 'inline') is None: return [] - if node.tag == SVG_DEFS_TAG: + # defs, masks and clippaths can contain embroiderable elements + # but should never be rendered directly. + if node.tag in [SVG_DEFS_TAG, SVG_MASK_TAG, SVG_CLIPPATH_TAG]: return [] # command connectors with a fill color set, will glitch into the elements list diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index a09b93b1..94df3f77 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -11,7 +11,6 @@ from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache -from ..stitch_plan import Stitch def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 02340aa5..0c5ffd3d 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -3,14 +3,16 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import inkex from lxml import etree +import inkex + etree.register_namespace("inkstitch", "http://inkstitch.org/namespace") inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') +SVG_POLYGON_TAG = inkex.addNS('polygon', 'svg') SVG_RECT_TAG = inkex.addNS('rect', 'svg') SVG_ELLIPSE_TAG = inkex.addNS('ellipse', 'svg') SVG_CIRCLE_TAG = inkex.addNS('circle', 'svg') @@ -22,12 +24,15 @@ SVG_LINK_TAG = inkex.addNS('a', 'svg') SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg') SVG_USE_TAG = inkex.addNS('use', 'svg') SVG_IMAGE_TAG = inkex.addNS('image', 'svg') +SVG_CLIPPATH_TAG = inkex.addNS('clipPath', 'svg') +SVG_MASK_TAG = inkex.addNS('mask', 'svg') INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') CONNECTION_START = inkex.addNS('connection-start', 'inkscape') CONNECTION_END = inkex.addNS('connection-end', 'inkscape') CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') +INKSCAPE_DOCUMENT_UNITS = inkex.addNS('document-units', 'inkscape') XLINK_HREF = inkex.addNS('href', 'xlink') @@ -37,7 +42,8 @@ SODIPODI_ROLE = inkex.addNS('role', 'sodipodi') INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch') -EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG) +EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_POLYGON_TAG, + SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG) NOT_EMBROIDERABLE_TAGS = (SVG_IMAGE_TAG, SVG_TEXT_TAG) SVG_OBJECT_TAGS = (SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG, SVG_RECT_TAG) -- cgit v1.3.1 From a275d49a24dc91b734c6dbee1e29157bfd0d59d5 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 5 May 2022 22:53:31 -0400 Subject: tangential->contour, fix legacy, remove unused params --- lib/elements/element.py | 4 +- lib/elements/fill_stitch.py | 76 ++---- lib/stitches/auto_fill.py | 6 +- lib/stitches/contour_fill.py | 536 +++++++++++++++++++++++++++++++++++++++ lib/stitches/fill.py | 3 +- lib/stitches/tangential_fill.py | 537 ---------------------------------------- lib/svg/tags.py | 4 +- 7 files changed, 572 insertions(+), 594 deletions(-) create mode 100644 lib/stitches/contour_fill.py delete mode 100644 lib/stitches/tangential_fill.py (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index ee4eadbb..3f5c6f4a 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -208,7 +208,7 @@ class EmbroideryElement(object): # L10N options to allow lock stitch before and after objects options=[_("Both"), _("Before"), _("After"), _("Neither")], default=0, - sort_index=4) + sort_index=10) @cache def ties(self): return self.get_int_param("ties", 0) @@ -220,7 +220,7 @@ class EmbroideryElement(object): 'even if the distance to the next object is shorter than defined by the collapse length value in the Ink/Stitch preferences.'), type='boolean', default=False, - sort_index=5) + sort_index=10) @cache def force_lock_stitches(self): return self.get_boolean_param('force_lock_stitches', False) diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index d7b859b5..c1bba7b8 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,7 +15,7 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill, auto_fill, legacy_fill, guided_fill +from ..stitches import contour_fill, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import cache, version @@ -96,34 +96,29 @@ class FillStitch(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) + options=[_("Auto Fill"), _("Contour Fill"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=2) - def tangential_strategy(self): - return self.get_int_param('tangential_strategy', 1) + @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=1, + options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=3) + def contour_strategy(self): + return self.get_int_param('contour_strategy', 1) @property @param('join_style', _('Join Style'), type='dropdown', default=0, - options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=4) def join_style(self): return self.get_int_param('join_style', 0) @property - @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) - def interlaced(self): - return self.get_boolean_param('interlaced', True) - - @property - @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=2) + @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=5) def avoid_self_crossing(self): return self.get_boolean_param('avoid_self_crossing', False) @property - @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1)], sort_index=5) def clockwise(self): return self.get_boolean_param('clockwise', True) @@ -133,7 +128,7 @@ class FillStitch(EmbroideryElement): tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', - sort_index=4, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 3)], default=0) @cache @@ -152,7 +147,7 @@ class FillStitch(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, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], default=False) @@ -166,7 +161,7 @@ class FillStitch(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, + sort_index=7, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], default=False) @@ -178,7 +173,7 @@ class FillStitch(EmbroideryElement): _('Spacing between rows'), tooltip=_('Distance between rows of stitches.'), unit='mm', - sort_index=4, + sort_index=6, type='float', default=0.25) def row_spacing(self): @@ -194,32 +189,18 @@ class FillStitch(EmbroideryElement): 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=6, type='float', default=3.0) def max_stitch_length(self): return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - @property - @param('min_stitch_length_mm', - _('Minimum fill stitch length'), - tooltip=_( - 'The minimum length of a stitches in a row. Larger values might introduce deviations from the desired path.' - 'Shorter stitch may be used at the start or end of a row.'), - unit='mm', - sort_index=4, - select_items=[('fill_method', 1), ('fill_method', 2)], - type='float', - default=0.0) - def min_stitch_length(self): - return self.get_float_param("min_stitch_length_mm", 0.0) - @property @param('staggers', _('Stagger rows this many times before repeating'), tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', - sort_index=4, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 3)], default=4) def staggers(self): @@ -339,7 +320,7 @@ class FillStitch(EmbroideryElement): type='float', default=1.5, select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=4) + sort_index=6) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -505,14 +486,11 @@ class FillStitch(EmbroideryElement): underlay_stitch_groups, start = self.do_underlay(start) stitch_groups.extend(underlay_stitch_groups) if self.fill_method == 0: - stitch_groups.extend( - self.do_auto_fill(last_patch, start, end)) + stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) if self.fill_method == 1: - stitch_groups.extend( - self.do_tangential_fill(last_patch, start)) + stitch_groups.extend(self.do_contour_fill(last_patch, start)) elif self.fill_method == 2: - stitch_groups.extend( - self.do_guided_fill(last_patch, start, end)) + stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) except Exception: self.fatal_fill_error() @@ -569,32 +547,32 @@ class FillStitch(EmbroideryElement): self.underpath)) return [stitch_group] - def do_tangential_fill(self, last_patch, starting_point): + def do_contour_fill(self, last_patch, starting_point): if not starting_point: starting_point = (0, 0) starting_point = shgeo.Point(starting_point) stitch_groups = [] for polygon in self.fill_shape.geoms: - tree = tangential_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) + tree = contour_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) stitches = [] - if self.tangential_strategy == 0: - stitches = tangential_fill.inner_to_outer( + if self.contour_strategy == 0: + stitches = contour_fill.inner_to_outer( tree, self.row_spacing, self.max_stitch_length, starting_point, self.avoid_self_crossing ) - elif self.tangential_strategy == 1: - stitches = tangential_fill.single_spiral( + elif self.contour_strategy == 1: + stitches = contour_fill.single_spiral( tree, self.max_stitch_length, starting_point ) - elif self.tangential_strategy == 2: - stitches = tangential_fill.double_spiral( + elif self.contour_strategy == 2: + stitches = contour_fill.double_spiral( tree, self.max_stitch_length, starting_point diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 630178c4..b3b9434f 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -59,7 +59,8 @@ def auto_fill(shape, ending_point=None, underpath=True): try: - segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + segments = [segment for row in rows for segment in row] fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point) except ValueError: # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node @@ -390,7 +391,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): def travel_grating(shape, angle, row_spacing): - segments = intersect_region_with_grating(shape, angle, row_spacing) + rows = intersect_region_with_grating(shape, angle, row_spacing) + segments = [segment for row in rows for segment in row] return shgeo.MultiLineString(list(segments)) diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py new file mode 100644 index 00000000..916285d8 --- /dev/null +++ b/lib/stitches/contour_fill.py @@ -0,0 +1,536 @@ +from collections import namedtuple +from itertools import chain + +import networkx as nx +import numpy as np +import trimesh +from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, MultiLineString, Point +from shapely.geometry.polygon import orient +from shapely.ops import nearest_points +from shapely.ops import polygonize + +from .running_stitch import running_stitch +from ..i18n import _ +from ..stitch_plan import Stitch +from ..stitches import constants +from ..utils import DotDict +from ..utils.geometry import cut, reverse_line_string, roll_linear_ring +from ..utils.geometry import ensure_geometry_collection, ensure_multi_polygon + + +class Tree(nx.DiGraph): + # This lets us do tree.nodes['somenode'].parent instead of the default + # tree.nodes['somenode']['parent']. + node_attr_dict_factory = DotDict + + def __init__(self, *args, **kwargs): + self.__node_num = 0 + super().__init__(**kwargs) + + def generate_node_name(self): + node = self.__node_num + self.__node_num += 1 + + return node + + +nearest_neighbor_tuple = namedtuple( + "nearest_neighbor_tuple", + [ + "nearest_point_parent", + "nearest_point_child", + "proj_distance_parent", + "child_node", + ], +) + + +def _offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): + result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) + result = ensure_multi_polygon(result) + + rings = GeometryCollection([poly.exterior for poly in result.geoms]) + rings = rings.simplify(constants.simplification_threshold, False) + + return _take_only_valid_linear_rings(rings) + + +def _take_only_valid_linear_rings(rings): + """ + Removes all geometries which do not form a "valid" LinearRing. + + A "valid" ring is one that does not form a straight line. + """ + + valid_rings = [] + + for ring in ensure_geometry_collection(rings).geoms: + if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): + valid_rings.append(ring) + + return GeometryCollection(valid_rings) + + +def _orient_linear_ring(ring, clockwise=True): + # Unfortunately for us, Inkscape SVGs have an inverted Y coordinate. + # Normally we don't have to care about that, but in this very specific + # case, the meaning of is_ccw is flipped. It actually tests whether + # a ring is clockwise. That makes this logic super-confusing. + if ring.is_ccw != clockwise: + return reverse_line_string(ring) + else: + return ring + + +def _orient_tree(tree, clockwise=True): + """ + Orient all linear rings in the tree. + + Since naturally holes have the opposite point ordering than non-holes we + make all lines within the tree uniform (having all the same ordering + direction) + """ + + for node in tree.nodes.values(): + node.val = _orient_linear_ring(node.val, clockwise) + + +def offset_polygon(polygon, offset, join_style, clockwise): + """ + Convert a polygon to a tree of isocontours. + + An isocontour is an offset version of the polygon's boundary. For example, + the isocontours of a circle are a set of concentric circles inside the + circle. + + This function takes a polygon (which may have holes) as input and creates + isocontours until the polygon is filled completely. The isocontours are + returned as a Tree, with a parent-child relationship indicating that the + parent isocontour contains the child isocontour. + + Arguments: + polygon - The shapely Polygon which may have holes + offset - The spacing between isocontours + join_style - Join style used when offsetting the Polygon border to create + isocontours. Can be round, mitered or bevel, as defined by + shapely: + https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE + clockwise - If True, isocontour points are in clockwise order; if False, counter-clockwise. + + Return Value: + Tree - see above + """ + + ordered_polygon = orient(polygon, -1) + tree = Tree() + tree.add_node('root', type='node', parent=None, val=ordered_polygon.exterior) + active_polygons = ['root'] + active_holes = [[]] + + for hole in ordered_polygon.interiors: + hole_node = tree.generate_node_name() + tree.add_node(hole_node, type="hole", val=hole) + active_holes[0].append(hole_node) + + while len(active_polygons) > 0: + current_poly = active_polygons.pop() + current_holes = active_holes.pop() + + outer, inners = _offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style) + polygons = _match_polygons_and_holes(outer, inners) + + for polygon in polygons.geoms: + new_polygon, new_holes = _convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes) + + if new_polygon is not None: + active_polygons.append(new_polygon) + active_holes.append(new_holes) + + 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 not tree.nodes[previous_hole].parent: + tree.nodes[previous_hole].parent = current_poly + tree.add_edge(current_poly, previous_hole) + + _orient_tree(tree, clockwise) + return tree + + +def _offset_polygon_and_holes(tree, poly, holes, offset, join_style): + outer = _offset_linear_ring( + tree.nodes[poly].val, + offset, + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + + inners = [] + for hole in holes: + inner = _offset_linear_ring( + tree.nodes[hole].val, + -offset, # take negative offset for holes + resolution=5, + join_style=join_style, + mitre_limit=10, + ) + if not inner.is_empty: + inners.append(Polygon(inner.geoms[0])) + + return outer, inners + + +def _match_polygons_and_holes(outer, inners): + result = MultiPolygon(polygonize(outer.geoms)) + if len(inners) > 0: + result = ensure_geometry_collection(result.difference(MultiPolygon(inners))) + + return result + + +def _convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): + polygon = orient(polygon, -1) + + if polygon.area < 0.1: + return None, None + + polygon = polygon.simplify(constants.simplification_threshold, False) + valid_rings = _take_only_valid_linear_rings(polygon.exterior) + + try: + exterior = valid_rings.geoms[0] + except IndexError: + return None, None + + node = tree.generate_node_name() + tree.add_node(node, type='node', parent=parent_polygon, val=exterior) + tree.add_edge(parent_polygon, node) + + hole_nodes = [] + for hole in polygon.interiors: + hole_node = tree.generate_node_name() + tree.add_node(hole_node, type="hole", val=hole) + for previous_hole in child_holes: + if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)): + tree.nodes[previous_hole].parent = hole_node + tree.add_edge(hole_node, previous_hole) + hole_nodes.append(hole_node) + + return node, hole_nodes + + +def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): + """ + Find the first point along travel_line that is within threshold of 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 + threshold - The distance between travel_line and next_line needs + to below threshold to be a valid point for entering + + Return value: + tuple or None + - the tuple structure is: + (point in travel_line, point in next_line) + - None is returned if there is no point that satisfies the threshold. + """ + + # We'll buffer next_line and find the intersection with travel_line. + # Then we'll return the very first point in the intersection, + # matched with a corresponding point on next_line. Fortunately for + # us, intersection of a Polygon with a LineString yields pieces of + # the LineString in the same order as the input LineString. + threshold_area = next_line.buffer(threshold) + portion_within_threshold = travel_line.intersection(threshold_area) + + if portion_within_threshold.is_empty: + return None + else: + # Projecting with 0 lets us avoid distinguishing between LineString and + # MultiLineString. + parent_point = Point(portion_within_threshold.interpolate(0)) + return nearest_points(parent_point, next_line) + + +def _create_nearest_points_list( + travel_line, tree, children, threshold, threshold_hard): + """Determine the best place to enter each of parent's children + + Arguments: + travel_line - The "parent" line for which the distance should + be minimized to enter each child + children - children of travel_line that need to be entered + threshold - The distance between travel_line and a child should + to be below threshold to be a valid point for entering + threshold_hard - As a last resort, we can accept an entry point + that is this far way + + Return value: + list of nearest_neighbor_tuple - indicating where to enter each + respective child + """ + + children_nearest_points = [] + + for child in children: + result = _get_nearest_points_closer_than_thresh(travel_line, tree.nodes[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, tree.nodes[child].val, threshold_hard) + + proj = travel_line.project(result[0]) + children_nearest_points.append( + nearest_neighbor_tuple( + nearest_point_parent=result[0], + nearest_point_child=result[1], + proj_distance_parent=proj, + child_node=child, + ) + ) + + return children_nearest_points + + +def _find_path_inner_to_outer(tree, node, offset, starting_point, + avoid_self_crossing, forward=True): + """Find a stitch path for this ring and its children. + + 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. + + This function calls itself recursively to find a stitch path for each child + (and its children). + + Arguments: + tree - a Tree of isocontours (as returned by offset_polygon) + offset - offset that was passed to offset_polygon + starting_point - starting point for stitching + avoid_self_crossing - if True, tries to generate a path that does not + cross itself. + forward - if True, this ring will be stitched in its natural direction + (used internally by avoid_self_crossing) + + Return value: + LineString -- the stitching path + """ + + current_node = tree.nodes[node] + current_ring = current_node.val + + if not forward and avoid_self_crossing: + current_ring = reverse_line_string(current_ring) + + # reorder the coordinates of this ring so that it starts with + # a point nearest the starting_point + start_distance = current_ring.project(starting_point) + current_ring = roll_linear_ring(current_ring, start_distance) + current_node.val = current_ring + + # Find where along this ring to connect to each child. + nearest_points_list = _create_nearest_points_list( + current_ring, + tree, + tree[node], + constants.offset_factor_for_adjacent_geometry * offset, + 2.05 * offset + ) + nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) + + result_coords = [] + if not nearest_points_list: + # We have no children, so we're at the center of a spiral. Reversing + # the innermost ring gives a nicer visual appearance. + if not avoid_self_crossing: + current_ring = reverse_line_string(current_ring) + else: + # This is a recursive algorithm. We'll stitch along this ring, pausing + # to jump to each child ring in turn and sew it before continuing on + # this ring. We'll end back where we started. + + result_coords.append(current_ring.coords[0]) + distance_so_far = 0 + for child_connection in nearest_points_list: + # Cut this ring into pieces before and after where this child will connect. + before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far) + distance_so_far = child_connection.proj_distance_parent + + # Stitch the part leading up to this child. + if before is not None: + result_coords.extend(before.coords) + + # Stitch this child. The child will start and end in the same + # place, which should be close to our current location. + child_path = _find_path_inner_to_outer( + tree, + child_connection.child_node, + offset, + child_connection.nearest_point_child, + avoid_self_crossing, + not forward + ) + result_coords.extend(child_path.coords) + + # Skip ahead a little bit on this ring before resuming. This + # gives a nice spiral pattern, where we spiral out from the + # innermost child. + if after is not None: + skip, after = cut(after, offset) + distance_so_far += offset + + current_ring = after + + if current_ring is not None: + # skip a little at the end so we don't end exactly where we started. + remaining_length = current_ring.length + if remaining_length > offset: + current_ring, skip = cut(current_ring, current_ring.length - offset) + + result_coords.extend(current_ring.coords) + + return LineString(result_coords) + + +def inner_to_outer(tree, offset, stitch_length, starting_point, avoid_self_crossing): + """Fill a shape with spirals, from innermost to outermost.""" + + stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing) + points = [Stitch(*point) for point in stitch_path.coords] + stitches = running_stitch(points, stitch_length) + + return stitches + + +def _reorder_linear_ring(ring, start): + distances = ring - start + start_index = np.argmin(np.linalg.norm(distances, axis=1)) + return np.roll(ring, -start_index, axis=0) + + +def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): + """ + Interpolate between two LinearRings + + Creates a path from start_point on ring1 and around the rings, ending at a + nearby point on ring2. The path will smoothly transition from ring1 to + ring2 as it travels around the rings. + + Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py + + Arguments: + ring1 -- LinearRing start point will lie on + ring2 -- LinearRing end point will lie on + max_stitch_length -- maximum stitch length (used to calculate resampling accuracy) + start -- Point on ring1 to start at, as a tuple + + Return value: Path interpolated between two LinearRings, as a LineString. + """ + + # Resample the two LinearRings so that they are the same number of points + # long. Then take the corresponding points in each ring and interpolate + # between them, gradually going more toward ring2. + # + # This is a little less accurate than the method in interpolate(), but several + # orders of magnitude faster because we're not building and querying a KDTree. + + num_points = int(20 * ring1.length / max_stitch_length) + ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points) + ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points) + + if start is not None: + ring1_resampled = _reorder_linear_ring(ring1_resampled, start) + ring2_resampled = _reorder_linear_ring(ring2_resampled, start) + + weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1)) + points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights) + result = LineString(points) + + return result.simplify(constants.simplification_threshold, False) + + +def _check_and_prepare_tree_for_valid_spiral(tree): + """Check whether spiral fill is possible, and tweak if necessary. + + Takes a tree consisting of isocontours. If a parent has more than one child + we cannot create a spiral. However, to make the routine more robust, we + allow more than one child if only one of the children has own children. The + other children are removed in this routine then. If the routine returns true, + the tree will have been cleaned up from unwanted children. + + If even with these weaker constraints, a spiral is not possible, False is + returned. + """ + + def process_node(node): + children = set(tree[node]) + + if len(children) == 0: + return True + elif len(children) == 1: + child = children.pop() + return process_node(child) + else: + children_with_children = {child for child in children if tree[child]} + if len(children_with_children) > 1: + # Node has multiple children with children, so a perfect spiral is not possible. + # This False value will be returned all the way up the stack. + return False + elif len(children_with_children) == 1: + children_without_children = children - children_with_children + child = children_with_children.pop() + tree.remove_nodes_from(children_without_children) + return process_node(child) + else: + # None of the children has its own children, so we'll just take the longest. + longest = max(children, key=lambda child: tree[child]['val'].length) + shorter_children = children - {longest} + tree.remove_nodes_from(shorter_children) + return process_node(longest) + + return process_node('root') + + +def single_spiral(tree, stitch_length, starting_point): + """Fill a shape with a single spiral going from outside to center.""" + return _spiral_fill(tree, stitch_length, starting_point, _make_spiral) + + +def double_spiral(tree, stitch_length, starting_point): + """Fill a shape with a double spiral going from outside to center and back to outside. """ + return _spiral_fill(tree, stitch_length, starting_point, _make_fermat_spiral) + + +def _spiral_fill(tree, stitch_length, close_point, spiral_maker): + if not _check_and_prepare_tree_for_valid_spiral(tree): + raise ValueError(_("Shape cannot be filled with single or double spiral!")) + + starting_point = close_point.coords[0] + rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] + path = spiral_maker(rings, stitch_length, starting_point) + path = [Stitch(*stitch) for stitch in path] + + return running_stitch(path, stitch_length) + + +def _make_fermat_spiral(rings, stitch_length, starting_point): + forward = _make_spiral(rings[::2], stitch_length, starting_point) + back = _make_spiral(rings[1::2], stitch_length, starting_point) + back.reverse() + + return chain(forward, back) + + +def _make_spiral(rings, stitch_length, starting_point): + path = [] + + for ring1, ring2 in zip(rings[:-1], rings[1:]): + spiral_part = _interpolate_linear_rings(ring1, ring2, stitch_length, starting_point) + path.extend(spiral_part.coords) + + return path diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 94df3f77..d5a983f9 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -162,7 +162,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non runs.reverse() runs = [tuple(reversed(run)) for run in runs] - yield from runs + yield runs if end_row_spacing: current_row_y += row_spacing + \ @@ -225,6 +225,7 @@ def pull_runs(rows, shape, row_spacing): # print >>sys.stderr, "\n".join(str(len(row)) for row in rows) + rows = list(rows) runs = [] count = 0 while (len(rows) > 0): diff --git a/lib/stitches/tangential_fill.py b/lib/stitches/tangential_fill.py deleted file mode 100644 index 833f9db3..00000000 --- a/lib/stitches/tangential_fill.py +++ /dev/null @@ -1,537 +0,0 @@ -from collections import namedtuple -from itertools import chain - -import networkx as nx -import numpy as np -import trimesh -from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, MultiLineString, Point -from shapely.geometry.polygon import orient -from shapely.ops import nearest_points -from shapely.ops import polygonize - -from .running_stitch import running_stitch -from ..i18n import _ -from ..stitch_plan import Stitch -from ..stitches import constants -from ..utils import DotDict -from ..utils.geometry import cut, reverse_line_string, roll_linear_ring -from ..utils.geometry import ensure_geometry_collection, ensure_multi_polygon - - -class Tree(nx.DiGraph): - # This lets us do tree.nodes['somenode'].parent instead of the default - # tree.nodes['somenode']['parent']. - node_attr_dict_factory = DotDict - - def __init__(self, *args, **kwargs): - self.__node_num = 0 - super().__init__(**kwargs) - - def generate_node_name(self): - node = self.__node_num - self.__node_num += 1 - - return node - - -nearest_neighbor_tuple = namedtuple( - "nearest_neighbor_tuple", - [ - "nearest_point_parent", - "nearest_point_child", - "proj_distance_parent", - "child_node", - ], -) - - -def _offset_linear_ring(ring, offset, resolution, join_style, mitre_limit): - result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True) - result = ensure_multi_polygon(result) - - rings = GeometryCollection([poly.exterior for poly in result.geoms]) - rings = rings.simplify(constants.simplification_threshold, False) - - return _take_only_valid_linear_rings(rings) - - -def _take_only_valid_linear_rings(rings): - """ - Removes all geometries which do not form a "valid" LinearRing. - - A "valid" ring is one that does not form a straight line. - """ - - valid_rings = [] - - for ring in ensure_geometry_collection(rings).geoms: - if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]): - valid_rings.append(ring) - - return GeometryCollection(valid_rings) - - -def _orient_linear_ring(ring, clockwise=True): - # Unfortunately for us, Inkscape SVGs have an inverted Y coordinate. - # Normally we don't have to care about that, but in this very specific - # case, the meaning of is_ccw is flipped. It actually tests whether - # a ring is clockwise. That makes this logic super-confusing. - if ring.is_ccw != clockwise: - return reverse_line_string(ring) - else: - return ring - - -def _orient_tree(tree, clockwise=True): - """ - Orient all linear rings in the tree. - - Since naturally holes have the opposite point ordering than non-holes we - make all lines within the tree uniform (having all the same ordering - direction) - """ - - for node in tree.nodes.values(): - node.val = _orient_linear_ring(node.val, clockwise) - - -def offset_polygon(polygon, offset, join_style, clockwise): - """ - Convert a polygon to a tree of isocontours. - - An isocontour is an offset version of the polygon's boundary. For example, - the isocontours of a circle are a set of concentric circles inside the - circle. - - This function takes a polygon (which may have holes) as input and creates - isocontours until the polygon is filled completely. The isocontours are - returned as a Tree, with a parent-child relationship indicating that the - parent isocontour contains the child isocontour. - - Arguments: - polygon - The shapely Polygon which may have holes - offset - The spacing between isocontours - join_style - Join style used when offsetting the Polygon border to create - isocontours. Can be round, mitered or bevel, as defined by - shapely: - https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE - clockwise - If True, isocontour points are in clockwise order; if False, counter-clockwise. - - Return Value: - Tree - see above - """ - - ordered_polygon = orient(polygon, -1) - tree = Tree() - tree.add_node('root', type='node', parent=None, val=ordered_polygon.exterior) - active_polygons = ['root'] - active_holes = [[]] - - for hole in ordered_polygon.interiors: - hole_node = tree.generate_node_name() - tree.add_node(hole_node, type="hole", val=hole) - active_holes[0].append(hole_node) - - while len(active_polygons) > 0: - current_poly = active_polygons.pop() - current_holes = active_holes.pop() - - outer, inners = _offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style) - polygons = _match_polygons_and_holes(outer, inners) - - for polygon in polygons.geoms: - new_polygon, new_holes = _convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes) - - if new_polygon is not None: - active_polygons.append(new_polygon) - active_holes.append(new_holes) - - 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 not tree.nodes[previous_hole].parent: - tree.nodes[previous_hole].parent = current_poly - tree.add_edge(current_poly, previous_hole) - - _orient_tree(tree, clockwise) - return tree - - -def _offset_polygon_and_holes(tree, poly, holes, offset, join_style): - outer = _offset_linear_ring( - tree.nodes[poly].val, - offset, - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - - inners = [] - for hole in holes: - inner = _offset_linear_ring( - tree.nodes[hole].val, - -offset, # take negative offset for holes - resolution=5, - join_style=join_style, - mitre_limit=10, - ) - if not inner.is_empty: - inners.append(Polygon(inner.geoms[0])) - - return outer, inners - - -def _match_polygons_and_holes(outer, inners): - result = MultiPolygon(polygonize(outer.geoms)) - if len(inners) > 0: - result = ensure_geometry_collection(result.difference(MultiPolygon(inners))) - - return result - - -def _convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): - polygon = orient(polygon, -1) - - if polygon.area < 0.1: - return None, None - - polygon = polygon.simplify(constants.simplification_threshold, False) - valid_rings = _take_only_valid_linear_rings(polygon.exterior) - - try: - exterior = valid_rings.geoms[0] - except IndexError: - return None, None - - node = tree.generate_node_name() - tree.add_node(node, type='node', parent=parent_polygon, val=exterior) - tree.add_edge(parent_polygon, node) - - hole_nodes = [] - for hole in polygon.interiors: - hole_node = tree.generate_node_name() - tree.add_node(hole_node, type="hole", val=hole) - for previous_hole in child_holes: - if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)): - tree.nodes[previous_hole].parent = hole_node - tree.add_edge(hole_node, previous_hole) - hole_nodes.append(hole_node) - - return node, hole_nodes - - -def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): - """ - Find the first point along travel_line that is within threshold of 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 - threshold - The distance between travel_line and next_line needs - to below threshold to be a valid point for entering - - Return value: - tuple or None - - the tuple structure is: - (point in travel_line, point in next_line) - - None is returned if there is no point that satisfies the threshold. - """ - - # We'll buffer next_line and find the intersection with travel_line. - # Then we'll return the very first point in the intersection, - # matched with a corresponding point on next_line. Fortunately for - # us, intersection of a Polygon with a LineString yields pieces of - # the LineString in the same order as the input LineString. - threshold_area = next_line.buffer(threshold) - portion_within_threshold = travel_line.intersection(threshold_area) - - if portion_within_threshold.is_empty: - return None - else: - # Projecting with 0 lets us avoid distinguishing between LineString and - # MultiLineString. - parent_point = Point(portion_within_threshold.interpolate(0)) - return nearest_points(parent_point, next_line) - - -def _create_nearest_points_list( - travel_line, tree, children, threshold, threshold_hard): - """Determine the best place to enter each of parent's children - - Arguments: - travel_line - The "parent" line for which the distance should - be minimized to enter each child - children - children of travel_line that need to be entered - threshold - The distance between travel_line and a child should - to be below threshold to be a valid point for entering - threshold_hard - As a last resort, we can accept an entry point - that is this far way - - Return value: - list of nearest_neighbor_tuple - indicating where to enter each - respective child - """ - - children_nearest_points = [] - - for child in children: - result = _get_nearest_points_closer_than_thresh(travel_line, tree.nodes[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, tree.nodes[child].val, threshold_hard) - - proj = travel_line.project(result[0]) - children_nearest_points.append( - nearest_neighbor_tuple( - nearest_point_parent=result[0], - nearest_point_child=result[1], - proj_distance_parent=proj, - child_node=child, - ) - ) - - return children_nearest_points - - -def _find_path_inner_to_outer(tree, node, offset, starting_point, - avoid_self_crossing, forward=True): - """Find a stitch path for this ring and its children. - - 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. - - This function calls itself recursively to find a stitch path for each child - (and its children). - - Arguments: - tree - a Tree of isocontours (as returned by offset_polygon) - offset - offset that was passed to offset_polygon - starting_point - starting point for stitching - avoid_self_crossing - if True, tries to generate a path that does not - cross itself. - forward - if True, this ring will be stitched in its natural direction - (used internally by avoid_self_crossing) - - Return value: - LineString -- the stitching path - """ - - current_node = tree.nodes[node] - current_ring = current_node.val - - if not forward and avoid_self_crossing: - current_ring = reverse_line_string(current_ring) - - # reorder the coordinates of this ring so that it starts with - # a point nearest the starting_point - start_distance = current_ring.project(starting_point) - current_ring = roll_linear_ring(current_ring, start_distance) - current_node.val = current_ring - - # Find where along this ring to connect to each child. - nearest_points_list = _create_nearest_points_list( - current_ring, - tree, - tree[node], - constants.offset_factor_for_adjacent_geometry * offset, - 2.05 * offset - ) - nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) - - result_coords = [] - if not nearest_points_list: - # We have no children, so we're at the center of a spiral. Reversing - # the innermost ring gives a nicer visual appearance. - if not avoid_self_crossing: - current_ring = reverse_line_string(current_ring) - else: - # This is a recursive algorithm. We'll stitch along this ring, pausing - # to jump to each child ring in turn and sew it before continuing on - # this ring. We'll end back where we started. - - result_coords.append(current_ring.coords[0]) - distance_so_far = 0 - for child_connection in nearest_points_list: - # Cut this ring into pieces before and after where this child will connect. - before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far) - distance_so_far = child_connection.proj_distance_parent - - # Stitch the part leading up to this child. - if before is not None: - result_coords.extend(before.coords) - - # Stitch this child. The child will start and end in the same - # place, which should be close to our current location. - child_path = _find_path_inner_to_outer( - tree, - child_connection.child_node, - offset, - child_connection.nearest_point_child, - avoid_self_crossing, - not forward - ) - result_coords.extend(child_path.coords) - - # Skip ahead a little bit on this ring before resuming. This - # gives a nice spiral pattern, where we spiral out from the - # innermost child. - if after is not None: - skip, after = cut(after, offset) - distance_so_far += offset - - current_ring = after - - if current_ring is not None: - # skip a little at the end so we don't end exactly where we started. - remaining_length = current_ring.length - if remaining_length > offset: - current_ring, skip = cut(current_ring, current_ring.length - offset) - - result_coords.extend(current_ring.coords) - - return LineString(result_coords) - - -def inner_to_outer(tree, offset, stitch_length, starting_point, avoid_self_crossing): - """Fill a shape with spirals, from innermost to outermost.""" - - stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing) - points = [Stitch(*point) for point in stitch_path.coords] - stitches = running_stitch(points, stitch_length) - - return stitches - - -def _reorder_linear_ring(ring, start): - distances = ring - start - start_index = np.argmin(np.linalg.norm(distances, axis=1)) - return np.roll(ring, -start_index, axis=0) - - -def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): - """ - Interpolate between two LinearRings - - Creates a path from start_point on ring1 and around the rings, ending at a - nearby point on ring2. The path will smoothly transition from ring1 to - ring2 as it travels around the rings. - - Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py - - Arguments: - ring1 -- LinearRing start point will lie on - ring2 -- LinearRing end point will lie on - max_stitch_length -- maximum stitch length (used to calculate resampling accuracy) - start -- Point on ring1 to start at, as a tuple - - Return value: Path interpolated between two LinearRings, as a LineString. - """ - - # Resample the two LinearRings so that they are the same number of points - # long. Then take the corresponding points in each ring and interpolate - # between them, gradually going more toward ring2. - # - # This is a little less accurate than the method in interpolate(), but several - # orders of magnitude faster because we're not building and querying a KDTree. - - num_points = int(20 * ring1.length / max_stitch_length) - ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points) - ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points) - - if start is not None: - ring1_resampled = _reorder_linear_ring(ring1_resampled, start) - ring2_resampled = _reorder_linear_ring(ring2_resampled, start) - - weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1)) - points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights) - result = LineString(points) - - # TODO: remove when rastering is cheaper - return result.simplify(constants.simplification_threshold, False) - - -def _check_and_prepare_tree_for_valid_spiral(tree): - """Check whether spiral fill is possible, and tweak if necessary. - - Takes a tree consisting of isocontours. If a parent has more than one child - we cannot create a spiral. However, to make the routine more robust, we - allow more than one child if only one of the children has own children. The - other children are removed in this routine then. If the routine returns true, - the tree will have been cleaned up from unwanted children. - - If even with these weaker constraints, a spiral is not possible, False is - returned. - """ - - def process_node(node): - children = set(tree[node]) - - if len(children) == 0: - return True - elif len(children) == 1: - child = children.pop() - return process_node(child) - else: - children_with_children = {child for child in children if tree[child]} - if len(children_with_children) > 1: - # Node has multiple children with children, so a perfect spiral is not possible. - # This False value will be returned all the way up the stack. - return False - elif len(children_with_children) == 1: - children_without_children = children - children_with_children - child = children_with_children.pop() - tree.remove_nodes_from(children_without_children) - return process_node(child) - else: - # None of the children has its own children, so we'll just take the longest. - longest = max(children, key=lambda child: tree[child]['val'].length) - shorter_children = children - {longest} - tree.remove_nodes_from(shorter_children) - return process_node(longest) - - return process_node('root') - - -def single_spiral(tree, stitch_length, starting_point): - """Fill a shape with a single spiral going from outside to center.""" - return _spiral_fill(tree, stitch_length, starting_point, _make_spiral) - - -def double_spiral(tree, stitch_length, starting_point): - """Fill a shape with a double spiral going from outside to center and back to outside. """ - return _spiral_fill(tree, stitch_length, starting_point, _make_fermat_spiral) - - -def _spiral_fill(tree, stitch_length, close_point, spiral_maker): - if not _check_and_prepare_tree_for_valid_spiral(tree): - raise ValueError(_("Shape cannot be filled with single or double spiral!")) - - starting_point = close_point.coords[0] - rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')] - path = spiral_maker(rings, stitch_length, starting_point) - path = [Stitch(*stitch) for stitch in path] - - return running_stitch(path, stitch_length) - - -def _make_fermat_spiral(rings, stitch_length, starting_point): - forward = _make_spiral(rings[::2], stitch_length, starting_point) - back = _make_spiral(rings[1::2], stitch_length, starting_point) - back.reverse() - - return chain(forward, back) - - -def _make_spiral(rings, stitch_length, starting_point): - path = [] - - for ring1, ring2 in zip(rings[:-1], rings[1:]): - spiral_part = _interpolate_linear_rings(ring1, ring2, stitch_length, starting_point) - path.extend(spiral_part.coords) - - return path diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 0c5ffd3d..d78ba678 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -59,9 +59,8 @@ inkstitch_attribs = [ 'angle', 'auto_fill', 'fill_method', - 'tangential_strategy', + 'contour_strategy', 'join_style', - 'interlaced', 'avoid_self_crossing', 'clockwise', 'expand_mm', @@ -72,7 +71,6 @@ inkstitch_attribs = [ 'fill_underlay_row_spacing_mm', 'fill_underlay_skip_last', 'max_stitch_length_mm', - 'min_stitch_length_mm', 'row_spacing_mm', 'end_row_spacing_mm', 'skip_last', -- cgit v1.3.1 From b30fce85dbdb4097bb9e01c3d68a77e0c50dd80a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 7 May 2022 16:20:15 -0400 Subject: undo aggressive line wrapping --- lib/elements/element.py | 21 +++++++------------- lib/elements/fill_stitch.py | 3 +-- lib/stitches/auto_fill.py | 48 +++++++++++++++------------------------------ lib/stitches/fill.py | 22 +++++++-------------- 4 files changed, 31 insertions(+), 63 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 3f5c6f4a..3648760b 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -166,8 +166,7 @@ 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. @@ -201,8 +200,7 @@ 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 @@ -260,8 +258,7 @@ 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() @@ -276,8 +273,7 @@ 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 @@ -327,8 +323,7 @@ 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() @@ -341,10 +336,8 @@ 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/fill_stitch.py b/lib/elements/fill_stitch.py index c1bba7b8..58629085 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -213,8 +213,7 @@ class FillStitch(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 diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 1d72e710..65b1e06d 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -69,10 +69,8 @@ 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) @@ -181,8 +179,7 @@ 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) @@ -195,8 +192,7 @@ 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): @@ -215,11 +211,9 @@ 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 @@ -303,8 +297,7 @@ 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. @@ -356,8 +349,7 @@ 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 @@ -435,12 +427,9 @@ 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") @@ -451,12 +440,10 @@ def build_travel_edges(shape, fill_angle): for ls in mls.geoms 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.geoms, vertical_edges.geoms) @@ -518,8 +505,7 @@ 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) @@ -548,8 +534,7 @@ 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,7 +624,6 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, 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/fill.py b/lib/stitches/fill.py index d5a983f9..46352d4f 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -14,8 +14,7 @@ from ..utils import cache 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) @@ -75,8 +74,7 @@ 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: @@ -85,8 +83,7 @@ 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: @@ -119,8 +116,7 @@ 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 @@ -155,8 +151,7 @@ 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() @@ -165,9 +160,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non yield 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 @@ -182,8 +175,7 @@ def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length if (swap): (beg, end) = (end, beg) - stitch_row(stitches, beg, end, angle, row_spacing, - max_stitch_length, staggers, skip_last) + stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last) swap = not swap -- cgit v1.3.1 From d0fc0e13261f5f37247570793dea726786df5456 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 7 May 2022 23:14:55 -0400 Subject: default to 'inner to outer' --- lib/elements/fill_stitch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 58629085..c9d609b4 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -101,10 +101,10 @@ class FillStitch(EmbroideryElement): return self.get_int_param('fill_method', 0) @property - @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=1, + @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=0, options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=3) def contour_strategy(self): - return self.get_int_param('contour_strategy', 1) + return self.get_int_param('contour_strategy', 0) @property @param('join_style', _('Join Style'), type='dropdown', default=0, -- cgit v1.3.1 From 87f328ec851aa418772d6b459cf6797248deb98f Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Tue, 17 May 2022 17:33:10 +0200 Subject: * flip option only for legacyfill * rename autofill underlay to fill underlay * remove unused dependency --- lib/elements/fill_stitch.py | 17 ++++++++--------- lib/stitches/contour_fill.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index c9d609b4..f1a75e2f 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -162,8 +162,7 @@ class FillStitch(EmbroideryElement): 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', sort_index=7, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], + select_items=[('fill_method', 3)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -324,7 +323,7 @@ class FillStitch(EmbroideryElement): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) + @param('fill_underlay', _('Underlay'), type='toggle', group=_('Fill Underlay'), default=True) def fill_underlay(self): return self.get_boolean_param("fill_underlay", default=True) @@ -333,7 +332,7 @@ class FillStitch(EmbroideryElement): _('Fill angle'), tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float') @cache def fill_underlay_angle(self): @@ -356,7 +355,7 @@ class FillStitch(EmbroideryElement): _('Row spacing'), tooltip=_('default: 3x fill row spacing'), unit='mm', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float') @cache def fill_underlay_row_spacing(self): @@ -367,7 +366,7 @@ class FillStitch(EmbroideryElement): _('Max stitch length'), tooltip=_('default: equal to fill max stitch length'), unit='mm', - group=_('AutoFill Underlay'), type='float') + group=_('Fill Underlay'), type='float') @cache def fill_underlay_max_stitch_length(self): return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length @@ -377,7 +376,7 @@ class FillStitch(EmbroideryElement): _('Inset'), tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float', default=0) def fill_underlay_inset(self): @@ -389,7 +388,7 @@ class FillStitch(EmbroideryElement): _('Skip last stitch in each row'), tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='boolean', default=False) def fill_underlay_skip_last(self): @@ -427,7 +426,7 @@ class FillStitch(EmbroideryElement): tooltip=_('Travel inside the shape when moving from section to section. Underpath ' 'stitches avoid traveling in the direction of the row angle so that they ' 'are not visible. This gives them a jagged appearance.'), - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='boolean', default=True) def underlay_underpath(self): diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py index 2466a04b..c42cc6f2 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -4,7 +4,7 @@ from itertools import chain import networkx as nx import numpy as np import trimesh -from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, MultiLineString, Point +from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, Point from shapely.geometry.polygon import orient from shapely.ops import nearest_points from shapely.ops import polygonize -- cgit v1.3.1 From 47123198760f8740acda0799d3b22f14b3f69550 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Thu, 19 May 2022 20:03:37 +0200 Subject: avoid simple satin division by zero error --- lib/elements/stroke.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 307c78b8..c9c0f795 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -167,6 +167,10 @@ class Stroke(EmbroideryElement): for i in range(len(patch) - 1): start = patch.stitches[i] end = patch.stitches[i + 1] + # sometimes the stitch results into zero length which cause a division by zero error + # ignoring this leads to a slightly bad result, but that is better than no output + if (end - start).length() == 0: + continue segment_direction = (end - start).unit() zigzag_direction = segment_direction.rotate_left() -- cgit v1.3.1