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 +- lib/extensions/__init__.py | 2 + lib/extensions/base.py | 12 +- lib/extensions/cleanup.py | 4 +- lib/extensions/params.py | 81 ++++- lib/extensions/selection_to_guide_line.py | 67 ++++ lib/patterns.py | 7 +- lib/stitches/ConnectAndSamplePattern.py | 477 ++++++++++++++++++++++++++++ lib/stitches/DebuggingMethods.py | 155 +++++++++ lib/stitches/LineStringSampling.py | 502 ++++++++++++++++++++++++++++++ lib/stitches/PointTransfer.py | 467 +++++++++++++++++++++++++++ lib/stitches/StitchPattern.py | 223 +++++++++++++ lib/stitches/auto_fill.py | 98 ++++-- lib/stitches/constants.py | 41 +++ lib/stitches/fill.py | 64 +++- lib/svg/tags.py | 11 +- 21 files changed, 2422 insertions(+), 301 deletions(-) delete mode 100644 lib/elements/fill.py create mode 100644 lib/extensions/selection_to_guide_line.py create mode 100644 lib/stitches/ConnectAndSamplePattern.py create mode 100644 lib/stitches/DebuggingMethods.py create mode 100644 lib/stitches/LineStringSampling.py create mode 100644 lib/stitches/PointTransfer.py create mode 100644 lib/stitches/StitchPattern.py create mode 100644 lib/stitches/constants.py (limited to 'lib') 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)) diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index b6e0d1d1..933720c9 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -39,6 +39,7 @@ from .print_pdf import Print from .remove_embroidery_settings import RemoveEmbroiderySettings from .reorder import Reorder from .selection_to_pattern import SelectionToPattern +from .selection_to_guide_line import SelectionToGuideLine from .simulator import Simulator from .stitch_plan_preview import StitchPlanPreview from .zip import Zip @@ -52,6 +53,7 @@ __all__ = extensions = [StitchPlanPreview, Zip, Flip, SelectionToPattern, + SelectionToGuideLine, ObjectCommands, ObjectCommandsToggleVisibility, LayerCommands, diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 75a07c5a..56385458 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -10,7 +10,6 @@ from collections.abc import MutableMapping import inkex from lxml import etree -from lxml.etree import Comment from stringcase import snakecase from ..commands import is_command, layer_commands @@ -20,8 +19,7 @@ from ..i18n import _ from ..patterns import is_pattern from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, - SVG_GROUP_TAG, SVG_MASK_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -131,10 +129,6 @@ class InkstitchExtension(inkex.Effect): def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901 nodes = [] - - if node.tag == Comment: - return [] - element = EmbroideryElement(node) if element.has_command('ignore_object'): @@ -147,9 +141,7 @@ class InkstitchExtension(inkex.Effect): if (node.tag in EMBROIDERABLE_TAGS or node.tag == SVG_GROUP_TAG) and element.get_style('display', 'inline') is None: return [] - # defs, masks and clippaths can contain embroiderable elements - # but should never be rendered directly. - if node.tag in [SVG_DEFS_TAG, SVG_MASK_TAG, SVG_CLIPPATH_TAG]: + if node.tag == SVG_DEFS_TAG: return [] # command connectors with a fill color set, will glitch into the elements list diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index a38818b8..ae95041b 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, errormsg -from ..elements import Fill, Stroke +from ..elements import AutoFill, Stroke from ..i18n import _ from .base import InkstitchExtension @@ -38,7 +38,7 @@ class Cleanup(InkstitchExtension): return for element in self.elements: - if (isinstance(element, Fill) and self.rm_fill and element.shape.area < self.fill_threshold): + if (isinstance(element, AutoFill) and self.rm_fill and element.shape.area < self.fill_threshold): element.node.getparent().remove(element.node) count += 1 if (isinstance(element, Stroke) and self.rm_stroke and diff --git a/lib/extensions/params.py b/lib/extensions/params.py index c96b9691..8021d5d7 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,15 +7,15 @@ import os import sys -from collections import defaultdict +from collections import defaultdict,namedtuple from copy import copy -from itertools import groupby +from itertools import groupby,zip_longest import wx from wx.lib.scrolledpanel import ScrolledPanel from ..commands import is_command, is_command_symbol -from ..elements import (AutoFill, Clone, EmbroideryElement, Fill, Polyline, +from ..elements import (AutoFill, Clone, EmbroideryElement, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..gui import PresetsPanel, SimulatorPreview, WarningPanel @@ -25,6 +25,14 @@ from ..utils import get_resource_dir from .base import InkstitchExtension +#ChoiceWidgets = namedtuple("ChoiceWidgets", "param widget last_initialized_choice") + + + +def grouper(iterable_obj, count, fillvalue=None): + args = [iter(iterable_obj)] * count + return zip_longest(*args, fillvalue=fillvalue) + class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) @@ -38,6 +46,8 @@ class ParamsTab(ScrolledPanel): self.dependent_tabs = [] self.parent_tab = None self.param_inputs = {} + self.choice_widgets = defaultdict(list) + self.dict_of_choices = {} self.paired_tab = None self.disable_notify_pair = False @@ -113,6 +123,19 @@ class ParamsTab(ScrolledPanel): if event: event.Skip() + def update_choice_state(self, event=None): + input = event.GetEventObject() + selection = input.GetSelection() + + param = self.inputs_to_params[input] + + self.update_choice_widgets((param, selection)) + self.settings_grid.Layout() + self.Layout() + + if event: + event.Skip() + def pair_changed(self, value): # print self.name, "pair_changed", value new_value = not value @@ -245,7 +268,30 @@ class ParamsTab(ScrolledPanel): # end wxGlade pass - def __do_layout(self): + #choice tuple is None or contains ("choice widget param name", "actual selection") + def update_choice_widgets(self, choice_tuple = None): + if choice_tuple == None: #update all choices + for choice in self.dict_of_choices.values(): + self.update_choice_widgets((choice["param"].name, choice["widget"].GetSelection())) + else: + choice = self.dict_of_choices[choice_tuple[0]] + last_selection = choice["last_initialized_choice"] + current_selection = choice["widget"].GetSelection() + if last_selection != -1 and last_selection != current_selection: #Hide the old widgets + for widget in self.choice_widgets[(choice["param"].name, last_selection)]: + widget.Hide() + #self.settings_grid.Detach(widget) + + #choice_index = self.settings_grid.GetChildren().index(self.settings_grid.GetItem(choice["widget"])) #TODO: is there a better way to get the index in the sizer? + for widgets in grouper(self.choice_widgets[choice_tuple], 4): + widgets[0].Show(True) + widgets[1].Show(True) + widgets[2].Show(True) + widgets[3].Show(True) + choice["last_initialized_choice"] = current_selection + + def __do_layout(self, only_settings_grid=False): + # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) @@ -266,14 +312,20 @@ class ParamsTab(ScrolledPanel): box.Add(toggle_sizer, proportion=0, flag=wx.BOTTOM, border=10) for param in self.params: - self.settings_grid.Add(self.create_change_indicator(param.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - + col1 = self.create_change_indicator(param.name) description = wx.StaticText(self, label=param.description) description.SetToolTip(param.tooltip) + + if param.select_items != None: + col1.Hide() + description.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([col1, description]) + #else: + self.settings_grid.Add(col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) if param.type == 'boolean': - if len(param.values) > 1: input = wx.CheckBox(self, style=wx.CHK_3STATE) input.Set3StateValue(wx.CHK_UNDETERMINED) @@ -287,6 +339,8 @@ class ParamsTab(ScrolledPanel): input = wx.Choice(self, wx.ID_ANY, choices=param.options) input.SetSelection(int(param.values[0])) input.Bind(wx.EVT_CHOICE, self.changed) + input.Bind(wx.EVT_CHOICE, self.update_choice_state) + self.dict_of_choices[param.name] = {"param": param, "widget": input, "last_initialized_choice": 1} elif len(param.values) > 1: input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(str(value) for value in param.values), style=wx.CB_DROPDOWN) input.Bind(wx.EVT_COMBOBOX, self.changed) @@ -298,13 +352,22 @@ class ParamsTab(ScrolledPanel): self.param_inputs[param.name] = input + col4 = wx.StaticText(self, label=param.unit or "") + + if param.select_items != None: + input.Hide() + col4.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([input, col4]) + #else: self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) - self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) self.SetSizer(box) + self.update_choice_widgets() self.Layout() @@ -521,7 +584,7 @@ class Params(InkstitchExtension): else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": classes.append(AutoFill) - classes.append(Fill) + #classes.append(Fill) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py new file mode 100644 index 00000000..85a44bb1 --- /dev/null +++ b/lib/extensions/selection_to_guide_line.py @@ -0,0 +1,67 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex +from lxml import etree + +from ..i18n import _ +from ..svg.tags import SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_DEFS_TAG +from .base import InkstitchExtension + + +class SelectionToGuideLine(InkstitchExtension): + + def effect(self): + if not self.get_elements(): + return + + if not self.svg.selected: + inkex.errormsg(_("Please select one object to be marked as a guide line.")) + return + + if len(self.get_nodes())!=1: + inkex.errormsg(_("Please select only one object to be marked as a guide line.")) + return + + for guide_line in self.get_nodes(): + if guide_line.tag in (SVG_PATH_TAG, SVG_POLYLINE_TAG): + self.set_marker(guide_line) + + def set_marker(self, node): + xpath = ".//marker[@id='inkstitch-guide-line-marker']" + guide_line_marker = self.document.xpath(xpath) + + if not guide_line_marker: + # get or create def element + defs = self.document.find(SVG_DEFS_TAG) + if defs is None: + defs = etree.SubElement(self.document, SVG_DEFS_TAG) + + # insert marker + marker = """ + + + + + """ # noqa: E501 + defs.append(etree.fromstring(marker)) + + # attach marker to node + style = node.get('style') or '' + style = style.split(";") + style = [i for i in style if not i.startswith('marker-start')] + style.append('marker-start:url(#inkstitch-guide-line-marker)') + node.set('style', ";".join(style)) diff --git a/lib/patterns.py b/lib/patterns.py index da22f21b..b4b60522 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) + patterns = get_patterns(node,"#inkstitch-pattern-marker") _apply_fill_patterns(patterns['fill_patterns'], patches) _apply_stroke_patterns(patterns['stroke_patterns'], patches) @@ -64,13 +64,14 @@ def _apply_fill_patterns(patterns, patches): patch.stitches = patch_points -def _get_patterns(node): +def get_patterns(node, marker_id): 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(#inkstitch-pattern-marker)')]" + 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: diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py new file mode 100644 index 00000000..21a56cd6 --- /dev/null +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -0,0 +1,477 @@ +from shapely.geometry.polygon import LineString, LinearRing +from shapely.geometry import Point, MultiPoint, linestring +from shapely.ops import nearest_points, polygonize +from collections import namedtuple +from depq import DEPQ +import math +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']) + + +# 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): + 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]) + + +#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): + + 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 + #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 + 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) + + 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) + 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)) + 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 + 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): + 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,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 + 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) + + 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 + current_item_index = 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 (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: + 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 + 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): + 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 + +#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)) + + 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 + + +#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): + 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 == 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) + 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) + 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)) + + if preferred_direction == 1: + 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)) + 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): + angles = LineStringSampling.calculate_line_angles(line_segment) + if angles[1] < abs_offset*constants.limiting_angle_straight: + if line_segment.length < max_stich_distance: + return None + else: + return line_segment.interpolate(line_segment.length-max_stich_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): + + 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 != 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) + 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) + 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, 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 + 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,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 + 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) + + 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 + + current_item_index = 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): + 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 + 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") + 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: + 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): + 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 diff --git a/lib/stitches/DebuggingMethods.py b/lib/stitches/DebuggingMethods.py new file mode 100644 index 00000000..d0f65576 --- /dev/null +++ b/lib/stitches/DebuggingMethods.py @@ -0,0 +1,155 @@ + +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 numpy as np +import matplotlib.collections as mcoll +import matplotlib.path as mpath + +# 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 new file mode 100644 index 00000000..434c6bbf --- /dev/null +++ b/lib/stitches/LineStringSampling.py @@ -0,0 +1,502 @@ +from sys import path +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 + +#Used to tag the origin of a rastered point +class PointSource(IntEnum): + #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 + + +# 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 +def calculate_line_angles(line): + 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 + +#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): + 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 + + 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: + deque_points = deque_points[::-1] #Since points with highest priority (=distance along line) are first (descending sorted) + + # 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) + 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]) 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 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* + (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 + 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. +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: + 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) + +# Ordering in priority queue: +# (point, LineStringSampling.PointSource), priority) + aligned_line = LineString(linecoords) #might be different from line for stitching_direction=-1 + + 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 + angles[0] = 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 + 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: + # 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 + 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)): + item = merged_point_list.pop() + 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 + 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): + 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]-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 + 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 new file mode 100644 index 00000000..998282a3 --- /dev/null +++ b/lib/stitches/PointTransfer.py @@ -0,0 +1,467 @@ +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']) + +#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: + 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 + + +#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) + 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 child.already_rastered == False: + 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: + child_list.append(subchild) + + if transfer_to_sibling: + for sibling in siblings_tuple: + if sibling.already_rastered == False: + 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: + neighbor_list.append(subchild) + + if transfer_to_parent and treenode.parent != None: + if treenode.parent.already_rastered == False: + 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 != None: + if treenode.parent.parent.already_rastered == False: + 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==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 == 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==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 == 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)) + +#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: + 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 + + +#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): + + 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) + + 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==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 == 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 new file mode 100644 index 00000000..d0a3f7aa --- /dev/null +++ b/lib/stitches/StitchPattern.py @@ -0,0 +1,223 @@ +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 shapely.geometry.polygon import orient +from depq import DEPQ +from enum import IntEnum +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): + 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) + + +# 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'): + 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 + + +#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): + 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 + +# 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): + 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: + #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))) + + # counter = 0 + while len(active_polys) > 0: # and counter < 20: + # counter += 1 + # print("New iter") + 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 = 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) + 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) + # print("New result_list: ", len(result_list)) + 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 + # 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)) + 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 == 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) + else: + print("Invalid strategy!") + assert(0) + + return connected_line, connected_line_origin diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 160d927e..71cfd80f 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -12,14 +12,17 @@ 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 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, stitch_row +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_graph class PathEdge(object): @@ -49,6 +52,7 @@ class PathEdge(object): @debug.time def auto_fill(shape, + line, angle, row_spacing, end_row_spacing, @@ -58,10 +62,13 @@ def auto_fill(shape, skip_last, starting_point, ending_point=None, - underpath=True): + underpath=True, + offset_by_half=True): + #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, 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) @@ -72,7 +79,7 @@ def auto_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, staggers, skip_last) + max_stitch_length, running_stitch_length, staggers, skip_last,line!=None,offset_by_half) return result @@ -106,7 +113,7 @@ def project(shape, coords, outline_index): @debug.time -def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None): +def build_fill_stitch_graph(shape, line, 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 @@ -141,18 +148,34 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting debug.add_layer("auto-fill fill stitch") - # 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] + if line == 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] graph = networkx.MultiGraph() - # 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=[]) + + 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) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -325,7 +348,8 @@ def get_segments(graph): segments = [] for start, end, key, data in graph.edges(keys=True, data=True): if key == 'segment': - segments.append(shgeo.LineString((start, end))) + segments.append(data["geometry"]) + #segments.append(shgeo.LineString((start, end))) return segments @@ -363,7 +387,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, end = segment.coords + 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. @@ -614,9 +639,28 @@ 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) + + + stitches.append(Stitch(*stitched_line[0], tags=('fill_row_start',))) + 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',))) + 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): +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 = [] @@ -627,7 +671,23 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, for edge in path: if edge.is_segment(): - stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) + 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', [])) else: stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py new file mode 100644 index 00000000..63746310 --- /dev/null +++ b/lib/stitches/constants.py @@ -0,0 +1,41 @@ +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 + +# 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 +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_factor = 1.5 + +# Used to handle numerical inaccuracies during comparisons +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. + +factor_offset_remove_points=0.5 #if points are closer than abs_offset*factor_offset_remove_points one of it is removed + +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. + +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) + + +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 + +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. + +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 diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 21e35d83..4e1669e9 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -6,12 +6,11 @@ import math import shapely - -from ..stitch_plan import Stitch +from shapely.geometry.linestring import LineString 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): rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip) @@ -89,6 +88,65 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge 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): + 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 + + 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 + + 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 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 + #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 + 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 diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 8b6f02a4..f3118661 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -11,7 +11,6 @@ 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') @@ -23,15 +22,12 @@ 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') @@ -41,8 +37,7 @@ SODIPODI_ROLE = inkex.addNS('role', 'sodipodi') INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch') -EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_POLYGON_TAG, - SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG) +EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_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) @@ -57,6 +52,10 @@ inkstitch_attribs = [ # fill 'angle', 'auto_fill', + 'fill_method', + 'tangential_strategy', + 'join_style', + 'interlaced', 'expand_mm', 'fill_underlay', 'fill_underlay_angle', -- cgit v1.2.3 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') 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.2.3 From 1a1939b5daf421116791b5ae45434cb1aba2ea38 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 30 Oct 2021 21:28:40 +0200 Subject: Bug fixing + refactoring --- lib/stitches/ConnectAndSamplePattern.py | 9 +- lib/stitches/LineStringSampling.py | 273 +++++--------------------------- lib/stitches/StitchPattern.py | 16 +- lib/stitches/auto_fill.py | 10 +- 4 files changed, 62 insertions(+), 246 deletions(-) (limited to 'lib') diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 9b3572d9..e8f1def5 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -139,9 +139,10 @@ def connect_raster_tree_nearest_neighbor( # points for start and end) end_distance, stitch_distance, - stitching_direction, 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 @@ -548,9 +549,10 @@ def connect_raster_tree_from_inner_to_outer( # and end) end_offset, stitch_distance, - stitching_direction, tree.transferred_point_priority_deque, abs_offset, + offset_by_half, + False ) else: ( @@ -566,9 +568,10 @@ def connect_raster_tree_from_inner_to_outer( # and end) current_coords.length - end_offset, stitch_distance, - stitching_direction, tree.transferred_point_priority_deque, abs_offset, + offset_by_half, + False ) current_coords.coords = current_coords.coords[::-1] diff --git a/lib/stitches/LineStringSampling.py b/lib/stitches/LineStringSampling.py index 07106515..bd20f55c 100644 --- a/lib/stitches/LineStringSampling.py +++ b/lib/stitches/LineStringSampling.py @@ -7,10 +7,11 @@ from enum import IntEnum from ..stitches import constants from ..stitches import PointTransfer -# Used to tag the origin of a rastered point - 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 @@ -40,9 +41,12 @@ class PointSource(IntEnum): 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 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]) @@ -65,31 +69,31 @@ def calculate_line_angles(line): 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 -# 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, 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] - 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 @@ -114,6 +118,7 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, # 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) @@ -130,212 +135,11 @@ def raster_line_string_with_priority_points(line, start_distance, end_distance, path_coords.coords = path_coords.coords[:-1] angles = calculate_line_angles(path_coords) - - current_distance = start_distance - - # Next we merge the line points and the projected (deque) points into one list - merged_point_list = [] - dq_iter = 0 - for point, angle in zip(path_coords.coords, angles): - # if abs(point[0]-40.4) < 0.2 and abs(point[1]-2.3)< 0.2: - # print("GEFUNDEN") - current_distance = start_distance+path_coords.project(Point(point)) - while dq_iter < len(deque_points) and deque_points[dq_iter][1] < current_distance: - # We want to avoid setting points at soft edges close to forbidden points - 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)): - item = merged_point_list.pop() - 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 - 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): - 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 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 * - (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 - 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. - - -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: - 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] - -# Ordering in priority queue: -# (point, LineStringSampling.PointSource), priority) - # 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 angles[0] = 1.1*constants.limiting_angle angles[-1] = 1.1*constants.limiting_angle - current_distance = 0.0 + current_distance = start_distance # Next we merge the line points and the projected (deque) points into one list merged_point_list = [] @@ -396,13 +200,13 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit 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: + # 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]-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]-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( @@ -476,9 +280,10 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit return_point_list = [] # [result_list[0][0].point.coords[0]] return_point_source_list = [] # [result_list[0][0].point_source] - # Currently replacement of forbidden points not satisfying - result_list = replace_forbidden_points( - aligned_line, result_list, forbidden_point_list, abs_offset) + # Note: replacement of forbidden points sometimes not satisfying + if replace_forbidden_points: + result_list = _replace_forbidden_points( + aligned_line, result_list, forbidden_point_list, abs_offset) # Finally we create the final return_point_list and return_point_source_list for i in range(len(result_list)): @@ -504,7 +309,7 @@ def raster_line_string_with_priority_points_graph(line, maxstitch_distance, stit return return_point_list, return_point_source_list -def replace_forbidden_points(line, result_list, forbidden_point_list_indices, abs_offset): +def _replace_forbidden_points(line, result_list, forbidden_point_list_indices, abs_offset): # since we add and remove points in the result_list, we need to adjust the indices stored in forbidden_point_list_indices current_index_shift = 0 for index in forbidden_point_list_indices: diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index ba3e3031..1fc5e389 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -152,7 +152,8 @@ def offset_poly( at this position """ ordered_poly = orient(poly, -1) - ordered_poly = ordered_poly.simplify(constants.simplification_threshold, False) + ordered_poly = ordered_poly.simplify( + constants.simplification_threshold, False) root = AnyNode( id="node", val=ordered_poly.exterior, @@ -168,7 +169,8 @@ def offset_poly( id="hole", val=holes, already_rastered=False, - transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + transferred_point_priority_deque=DEPQ( + iterable=None, maxlen=None), ) ) @@ -182,7 +184,7 @@ def offset_poly( offset, "left", resolution=5, - joint_style=join_style, + join_style=join_style, mitre_limit=10, ) outer = outer.simplify(constants.simplification_threshold, False) @@ -194,7 +196,7 @@ def offset_poly( offset, "left", resolution=5, - joint_style=join_style, + join_style=join_style, mitre_limit=10, ) inner = inner.simplify(constants.simplification_threshold, False) @@ -209,9 +211,11 @@ def offset_poly( result = MultiPolygon(polygonize(outer)) else: if outer.geom_type == "LineString": - result = Polygon(outer).difference(MultiPolygon(poly_inners)) + 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: result_list = [] diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 1331ecb2..a0d9637a 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -21,7 +21,7 @@ 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_graph +from .LineStringSampling import raster_line_string_with_priority_points class PathEdge(object): @@ -666,8 +666,12 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s # 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) + 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)): -- cgit v1.2.3 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') 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.2.3 From e15bce1401af8b80a72aa528714e5667edbd9429 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 19 Nov 2021 19:32:29 +0100 Subject: minor changes --- lib/stitches/PointTransfer.py | 3 +++ lib/stitches/auto_fill.py | 8 +++++++- lib/stitches/fill.py | 31 ++++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/stitches/PointTransfer.py b/lib/stitches/PointTransfer.py index b4c6c004..da73aea0 100644 --- a/lib/stitches/PointTransfer.py +++ b/lib/stitches/PointTransfer.py @@ -409,6 +409,9 @@ def transfer_points_to_surrounding_graph(fill_stitch_graph, current_edge, used_o 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 diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index a0d9637a..95cc9103 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -165,6 +165,10 @@ 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. @@ -674,7 +678,7 @@ def stitch_line(stitches, stitching_direction, geometry, projected_points, max_s 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)): + for i in range(1, len(stitched_line)-1): stitches.append(Stitch(*stitched_line[i], tags=('fill_row'))) if not skip_last: @@ -684,6 +688,8 @@ 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/fill.py b/lib/stitches/fill.py index 55ce09a4..01bfdc20 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -113,6 +113,19 @@ def extend_line(line, minx, maxx, miny, maxy): 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 intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing=None, flip=False): row_spacing = abs(row_spacing) @@ -141,17 +154,12 @@ 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 - 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] + line_offsetted = repair_multiple_parallel_offset_curves( + line_offsetted) if row_spacing < 0: line_offsetted.coords = line_offsetted.coords[::-1] @@ -162,10 +170,15 @@ def intersect_region_with_grating_line(shape, line, row_spacing, end_row_spacing 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( + 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) + # if res.geom_type != 'LineString': + # print("HIER!!") return rows -- cgit v1.2.3 From 8966fa1919df5c2070cebebd080b54a56c7001b1 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 19 Nov 2021 19:42:28 +0100 Subject: minor changes --- lib/stitches/StitchPattern.py | 4 ++-- lib/stitches/auto_fill.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py index 1fc5e389..1edfd452 100644 --- a/lib/stitches/StitchPattern.py +++ b/lib/stitches/StitchPattern.py @@ -123,11 +123,11 @@ def make_tree_uniform_ccw(root): class StitchingStrategy(IntEnum): CLOSEST_POINT = 0 INNER_TO_OUTER = 1 + SPIRAL = 2 def offset_poly( - poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point -): + 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. diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 95cc9103..b63f4be1 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -165,10 +165,10 @@ 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!") + # 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. @@ -688,8 +688,8 @@ 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") + # if stitches[-1].x == stitches[-2].x and stitches[-1].y == stitches[-2].y: + # print("FEHLER") @debug.time -- cgit v1.2.3 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 +++++- 6 files changed, 346 insertions(+), 39 deletions(-) (limited to 'lib') 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) -- cgit v1.2.3 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') 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.2.3 From cc051bcdba88c985a040a508be2ccc8878e09181 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 14 Jan 2022 20:51:38 +0100 Subject: removed double points at spirals --- lib/stitches/ConnectAndSamplePattern.py | 39 ++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/stitches/ConnectAndSamplePattern.py b/lib/stitches/ConnectAndSamplePattern.py index 2c538e5d..33a1ba6d 100644 --- a/lib/stitches/ConnectAndSamplePattern.py +++ b/lib/stitches/ConnectAndSamplePattern.py @@ -443,7 +443,7 @@ def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distan """ 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. + 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) @@ -840,7 +840,7 @@ def connect_raster_tree_spiral( 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 + -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 @@ -917,12 +917,35 @@ def connect_raster_tree_spiral( 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] + # 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 -- cgit v1.2.3 From 8d19cdc59d00836fdf3ef037f31e7130771f84d2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 28 Jan 2022 21:54:20 +0100 Subject: adjusted selection_to_guide_line to match new marker creation way --- lib/extensions/selection_to_guide_line.py | 55 ++++--------------------------- lib/marker.py | 2 +- 2 files changed, 7 insertions(+), 50 deletions(-) (limited to 'lib') diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py index e11cdb4e..a0d32601 100644 --- a/lib/extensions/selection_to_guide_line.py +++ b/lib/extensions/selection_to_guide_line.py @@ -4,10 +4,10 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. import inkex -from lxml import etree from ..i18n import _ -from ..svg.tags import SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_DEFS_TAG +from ..marker import set_marker +from ..svg.tags import EMBROIDERABLE_TAGS from .base import InkstitchExtension @@ -18,52 +18,9 @@ class SelectionToGuideLine(InkstitchExtension): return if not self.svg.selected: - inkex.errormsg( - _("Please select one object to be marked as a guide line.")) + inkex.errormsg(_("Please select at least one object to be marked as a guide line.")) return - if len(self.get_nodes()) != 1: - inkex.errormsg( - _("Please select only one object to be marked as a guide line.")) - return - - for guide_line in self.get_nodes(): - if guide_line.tag in (SVG_PATH_TAG, SVG_POLYLINE_TAG): - self.set_marker(guide_line) - - def set_marker(self, node): - xpath = ".//marker[@id='inkstitch-guide-line-marker']" - guide_line_marker = self.document.xpath(xpath) - - if not guide_line_marker: - # get or create def element - defs = self.document.find(SVG_DEFS_TAG) - if defs is None: - defs = etree.SubElement(self.document, SVG_DEFS_TAG) - - # insert marker - marker = """ - - - - - """ # noqa: E501 - defs.append(etree.fromstring(marker)) - - # attach marker to node - style = node.get('style') or '' - style = style.split(";") - style = [i for i in style if not i.startswith('marker-start')] - style.append('marker-start:url(#inkstitch-guide-line-marker)') - node.set('style', ";".join(style)) + for pattern in self.get_nodes(): + if pattern.tag in EMBROIDERABLE_TAGS: + set_marker(pattern, 'start', 'guide-line') diff --git a/lib/marker.py b/lib/marker.py index 4f262abe..1d9145e7 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -33,5 +33,5 @@ def set_marker(node, position, marker): style = node.get('style') or '' style = style.split(";") style = [i for i in style if not i.startswith('marker-%s' % position)] - style.append('marker-%s:url(#inkstitch-pattern-marker)' % position) + style.append('marker-%s:url(#inkstitch-%s-marker)' % (position, marker)) node.set('style', ";".join(style)) -- cgit v1.2.3 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') 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.2.3 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') 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.2.3 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 +- 12 files changed, 697 insertions(+), 619 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') 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 -- cgit v1.2.3 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') 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.2.3 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') 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.2.3 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') 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.2.3 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') 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.2.3 From f75c812aba081f3d8d3a6fde799cc2e9fbc9b757 Mon Sep 17 00:00:00 2001 From: Andreas Date: Mon, 21 Mar 2022 20:39:06 +0100 Subject: replacing shapely parallel_offset by buffer --- .../tangential_fill_stitch_line_creator.py | 165 +++++++++++---------- 1 file changed, 90 insertions(+), 75 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 4d4377f0..01124478 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -2,7 +2,7 @@ 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 anytree import AnyNode, PreOrderIter, LevelOrderGroupIter, RenderTree from shapely.geometry.polygon import orient from depq import DEPQ from enum import IntEnum @@ -10,79 +10,94 @@ 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 - """ +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) - 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] + if result.geom_type == 'Polygon': + return result.exterior 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)) + for poly in result: + 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) + + def take_only_valid_linear_rings(rings): """ Removes all geometries which do not form a "valid" LinearRing @@ -99,13 +114,15 @@ def take_only_valid_linear_rings(rings): return LinearRing(new_list[0]) else: return MultiLineString(new_list) - else: + 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() def make_tree_uniform_ccw(root): @@ -223,7 +240,6 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, outer = offset_linear_ring( current_poly.val, offset, - "left", resolution=5, join_style=join_style, mitre_limit=10, @@ -234,8 +250,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, - "left", + -offset, #take negative offset for holes resolution=5, join_style=join_style, mitre_limit=10, @@ -246,12 +261,12 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, 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" or outer.geom_type == "LinearRing": result = Polygon(outer) else: result = MultiPolygon(polygonize(outer)) else: - if outer.geom_type == "LineString": + if outer.geom_type == "LineString" or outer.geom_type == "LinearRing": result = Polygon(outer).difference( MultiPolygon(poly_inners)) else: @@ -312,7 +327,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)) make_tree_uniform_ccw(root) if strategy == StitchingStrategy.CLOSEST_POINT: -- cgit v1.2.3 From f4c47a8e226af11852e2e530c9b4a6a6403bff07 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Apr 2022 07:56:58 -0400 Subject: generalize get_segments and process_travel_edges --- lib/stitches/auto_fill.py | 7 ++--- lib/stitches/guided_fill.py | 64 --------------------------------------------- 2 files changed, 4 insertions(+), 67 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 52dc6a81..a8118039 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -155,7 +155,7 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting 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=[]) + graph.add_edge(*segment, key="segment", underpath_edges=[], geometry=shgeo.LineString(segment)) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -334,7 +334,7 @@ def get_segments(graph): segments = [] for start, end, key, data in graph.edges(keys=True, data=True): if key == 'segment': - segments.append(shgeo.LineString((start, end))) + segments.append(data["geometry"]) return segments @@ -373,7 +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, end = segment.coords + 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. diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 6948a086..51b0618f 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -124,70 +124,6 @@ def build_guided_fill_stitch_graph(shape, guideline, row_spacing, starting_point 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, min_stitch_length, row_spacing, skip_last, offset_by_half): if stitching_direction == 1: stitched_line, _ = raster_line_string_with_priority_points( -- cgit v1.2.3 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') 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.2.3 From cbcaa0ac0ef43c4bb9da3aa49abd23c1e0d4b360 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Apr 2022 11:21:59 -0400 Subject: faster method for single spiral ring interpolation --- .../tangential_fill_stitch_pattern_creator.py | 100 ++++++++++----------- 1 file changed, 50 insertions(+), 50 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 13a480e3..084b1d01 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -5,11 +5,11 @@ import numpy as np import trimesh from anytree import PreOrderIter 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 ..debug import debug from ..stitches import constants from ..stitches import point_transfer from ..stitches import sample_linestring @@ -737,61 +737,62 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, 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): +def orient_linear_ring(ring): + if not ring.is_ccw: + debug.log("reversing a ring") + return LinearRing(reversed(ring.coords)) + else: + return ring + + +def reorder_linear_ring(ring, start): + start_index = np.argmin(np.linalg.norm(ring, axis=1)) + return np.roll(ring, -start_index, axis=0) + + +@debug.time +def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): """ 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 + + 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 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] + 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. + # + # 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(ring1, count=num_points) + ring2_resampled = trimesh.path.traversal.resample_path(ring2, count=num_points) - 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) + 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) @@ -811,7 +812,7 @@ def connect_raster_tree_spiral( -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: + 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 @@ -839,8 +840,7 @@ def connect_raster_tree_spiral( ring1 = node.val ring2 = node.children[0].val - part_spiral = interpolate_LinearRings( - ring1, ring2, starting_point) + part_spiral = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) node.val = part_spiral for node in PreOrderIter(tree, stop=lambda n: n.is_leaf): -- cgit v1.2.3 From 6ca1af0c88728bd91f1775d0056a66d9272b3a1b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 21 Apr 2022 23:09:05 -0400 Subject: avoid anytree dependency --- lib/stitches/point_transfer.py | 80 +++---- .../tangential_fill_stitch_line_creator.py | 169 ++++++++------- .../tangential_fill_stitch_pattern_creator.py | 231 +++++++++++---------- lib/utils/dotdict.py | 2 +- 4 files changed, 261 insertions(+), 221 deletions(-) (limited to 'lib') diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py index 553ffbda..c0d519ef 100644 --- a/lib/stitches/point_transfer.py +++ b/lib/stitches/point_transfer.py @@ -15,7 +15,7 @@ projected_point_tuple = namedtuple( def calc_transferred_point(bisectorline, child): """ - Calculates the nearest interserction point of "bisectorline" with the coordinates of child (child.val). + Calculates the nearest intersection 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. """ @@ -39,7 +39,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=[], # noqa: C901 +def transfer_points_to_surrounding(tree, node, 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): """ @@ -64,18 +64,24 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra 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 (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) < 3: return + current_node = tree.nodes[node] + # 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 + childs_tuple = tuple(tree.successors(node)) + if current_node.parent: + siblings_tuple = tuple(child for child in tree[current_node.parent] if child != node) + else: + siblings_tuple = () + # 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 = [] @@ -85,38 +91,39 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra if transfer_to_child: for child in childs_tuple: - if not child.already_rastered: + if not tree.nodes[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) + for grandchild in tree[child]: + if not tree.nodes[grandchild].already_rastered: + child_list.append(grandchild) if transfer_to_sibling: for sibling in siblings_tuple: - if not sibling.already_rastered: + if not tree.nodes[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) + for nibling in tree[sibling]: + if not tree.nodes[nibling].already_rastered: + neighbor_list.append(nibling) - if transfer_to_parent and treenode.parent is not None: - if not treenode.parent.already_rastered: + if transfer_to_parent and current_node.parent is not None: + if not tree.nodes[current_node.parent].already_rastered: if not overnext_neighbor: - neighbor_list.append(treenode.parent) + neighbor_list.append(current_node.parent) if transfer_forbidden_points: - neighbor_list_forbidden.append(treenode.parent) + neighbor_list_forbidden.append(current_node.parent) if overnext_neighbor: - if treenode.parent.parent is not None: - if not treenode.parent.parent.already_rastered: - neighbor_list.append(treenode.parent.parent) + grandparent = tree.nodes[current_node].parent + if grandparent is not None: + if not tree.nodes[grandparent].already_rastered: + neighbor_list.append(grandparent) if not neighbor_list and not child_list: return @@ -130,7 +137,7 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra 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): + if point_source_list: point_source_list.pop() if len(point_list) == 0: return @@ -243,34 +250,35 @@ def transfer_points_to_surrounding(treenode, used_offset, offset_by_half, to_tra originPoint_forbidden_point.coords[0][1]-vecy_forbidden_point)]) for child in child_list: - point, priority = calc_transferred_point(bisectorline_child, child) + current_child = tree.nodes[child] + point, priority = calc_transferred_point(bisectorline_child, current_child) if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple( + current_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) + current_child = tree.nodes[child] + point, priority = calc_transferred_point(bisectorline_forbidden_point_child, current_child) if point is None: continue - child.transferred_point_priority_deque.insert(projected_point_tuple( + current_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) + current_neighbor = tree.nodes[neighbor] + point, priority = calc_transferred_point(bisectorline_neighbor, current_neighbor) if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + current_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) + current_neighbor = tree.nodes[neighbor] + point, priority = calc_transferred_point(bisectorline_forbidden_point_neighbor, current_neighbor) if point is None: continue - neighbor.transferred_point_priority_deque.insert(projected_point_tuple( + current_neighbor.transferred_point_priority_deque.insert(projected_point_tuple( point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) i += 1 diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 6b141611..deb87659 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -1,6 +1,6 @@ from enum import IntEnum -from anytree import AnyNode, LevelOrderGroupIter, PreOrderIter +import networkx as nx from depq import DEPQ from shapely.geometry import MultiLineString, Polygon from shapely.geometry import MultiPolygon @@ -10,6 +10,13 @@ from shapely.ops import polygonize from ..stitches import constants from ..stitches import tangential_fill_stitch_pattern_creator +from ..utils import DotDict + + +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): @@ -126,15 +133,15 @@ def take_only_valid_linear_rings(rings): return LinearRing() -def make_tree_uniform_ccw(root): +def make_tree_uniform_ccw(tree): """ 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] + 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)) # Used to define which stitching strategy shall be used @@ -144,7 +151,7 @@ class StitchingStrategy(IntEnum): SPIRAL = 2 -def check_and_prepare_tree_for_valid_spiral(root): +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 @@ -153,27 +160,34 @@ def check_and_prepare_tree_for_valid_spiral(root): 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: + + 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 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 + 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, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point): # noqa: C901 @@ -213,25 +227,29 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, 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] + 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), + ) + 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), - ) - ) + # 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, + already_rastered=False, + transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None), + ) + active_holes[0].append(node_num) + node_num += 1 while len(active_polys) > 0: current_poly = active_polys.pop() @@ -239,7 +257,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, poly_inners = [] outer = offset_linear_ring( - current_poly.val, + tree.nodes[current_poly].val, offset, resolution=5, join_style=join_style, @@ -248,9 +266,9 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, outer = outer.simplify(constants.simplification_threshold, False) outer = take_only_valid_linear_rings(outer) - for j in range(len(current_holes)): + for hole in current_holes: inner = offset_linear_ring( - current_holes[j].val, + tree.nodes[hole].val, -offset, # take negative offset for holes resolution=5, join_style=join_style, @@ -275,11 +293,10 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, 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) + result_list = list(result.geoms) for polygon in result_list: polygon = orient(polygon, -1) @@ -295,29 +312,31 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, 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 - ), - ) + 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 = AnyNode( - id="hole", - val=hole, - already_rastered=False, - transferred_point_priority_deque=DEPQ( - iterable=None, maxlen=None - ), - ) + 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(previous_hole.val)): - previous_hole.parent = hole_node + 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) for previous_hole in current_holes: @@ -325,23 +344,23 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, # contained in the new holes they # have been merged with the # outer polygon - if previous_hole.parent is None: - previous_hole.parent = current_poly + if tree.nodes[previous_hole].parent is None: + tree.nodes[previous_hole].parent = current_poly + tree.add_edge(current_poly, previous_hole) - # print(RenderTree(root)) - make_tree_uniform_ccw(root) + 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( - root, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) + tree, '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, min_stitch_distance, starting_point, offset_by_half) + tree, '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): + 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( - root, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half) + 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 084b1d01..1b085ed6 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -3,7 +3,7 @@ from collections import namedtuple import numpy as np import trimesh -from anytree import PreOrderIter +import networkx as nx from depq import DEPQ from shapely.geometry import MultiPoint, Point from shapely.geometry.polygon import LineString, LinearRing @@ -54,7 +54,7 @@ def cut(line, distance): def connect_raster_tree_nearest_neighbor( # noqa: C901 - tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): + 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 @@ -77,20 +77,21 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 placed at this position """ - current_coords = tree.val + 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 = tree.val.project(close_point) + start_distance = current_coords.project(close_point) if start_distance > 0: current_coords = cut(current_coords, start_distance) - tree.val = current_coords + current_node.val = current_coords - if not tree.transferred_point_priority_deque.is_empty(): + if not current_node.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: + for item, priority in current_node.transferred_point_priority_deque: new_DEPQ.insert( item, math.fmod( @@ -98,7 +99,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 current_coords.length, ), ) - tree.transferred_point_priority_deque = new_DEPQ + current_node.transferred_point_priority_deque = new_DEPQ stitching_direction = 1 # This list should contain a tuple of nearest points between @@ -106,8 +107,8 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 # 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) + 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( @@ -141,7 +142,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 end_distance, stitch_distance, min_stitch_distance, - tree.transferred_point_priority_deque, + current_node.transferred_point_priority_deque, abs_offset, offset_by_half, False) @@ -149,8 +150,8 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 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 + 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 = [] @@ -172,6 +173,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 # 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, @@ -188,6 +190,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 if offset_by_half: point_transfer.transfer_points_to_surrounding( tree, + node, stitching_direction * used_offset, False, to_transfer_point_list, @@ -232,6 +235,7 @@ def connect_raster_tree_nearest_neighbor( # noqa: C901 item = nearest_points_list[cur_item] (child_coords, child_coords_origin) = connect_raster_tree_nearest_neighbor( + tree, item.child_node, used_offset, stitch_distance, @@ -324,7 +328,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, tree, 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 @@ -357,13 +361,13 @@ def create_nearest_points_list( weight_reversed_order = 0 for child in children_list: result = get_nearest_points_closer_than_thresh( - travel_line, child.val, threshold + 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, child.val, threshold_hard + travel_line, tree.nodes[child].val, threshold_hard ) assert result is not None proj = travel_line.project(result[0]) @@ -378,13 +382,13 @@ def create_nearest_points_list( ) result = get_nearest_points_closer_than_thresh( - travel_line_reversed, child.val, threshold + travel_line_reversed, 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_reversed, child.val, threshold_hard + travel_line_reversed, tree.nodes[child].val, threshold_hard ) assert result is not None proj = travel_line_reversed.project(result[0]) @@ -404,7 +408,7 @@ def create_nearest_points_list( weight_in_order / 2, max(0, weight_in_order - 10 * threshold) ) if weight_in_order == weight_reversed_order: - return (1, result_list_in_order) + return 1, result_list_in_order elif preferred_direction == -1: # Reduce weight_reversed_order to make reversed # stitching more preferred @@ -438,7 +442,8 @@ 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, min_stitch_distance, close_point, offset_by_half): # noqa: C901 +def connect_raster_tree_from_inner_to_outer(tree, node, 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 @@ -462,20 +467,21 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, at this position """ - current_coords = tree.val + current_node = tree.nodes[node] + current_coords = current_node.val abs_offset = abs(used_offset) result_coords = [] result_coords_origin = [] - start_distance = tree.val.project(close_point) + start_distance = current_coords.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 + current_node.val = current_coords - if not tree.transferred_point_priority_deque.is_empty(): + if not current_node.transferred_point_priority_deque.is_empty(): new_DEPQ = DEPQ(iterable=None, maxlen=None) - for item, priority in tree.transferred_point_priority_deque: + for item, priority in current_node.transferred_point_priority_deque: new_DEPQ.insert( item, math.fmod( @@ -483,19 +489,20 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, current_coords.length, ), ) - tree.transferred_point_priority_deque = new_DEPQ + current_node.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 + if current_node.parent is not None: + parent_stitching_direction = tree.nodes[current_node.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, + tree, + tree[node], constants.offset_factor_for_adjacent_geometry * abs_offset, 2.05 * abs_offset, parent_stitching_direction, @@ -528,7 +535,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, end_offset, stitch_distance, min_stitch_distance, - tree.transferred_point_priority_deque, + current_node.transferred_point_priority_deque, abs_offset, offset_by_half, False @@ -545,7 +552,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, current_coords.length - end_offset, stitch_distance, min_stitch_distance, - tree.transferred_point_priority_deque, + current_node.transferred_point_priority_deque, abs_offset, offset_by_half, False @@ -554,8 +561,8 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, assert len(own_coords) == len(own_coords_origin) - tree.stitching_direction = stitching_direction - tree.already_rastered = True + current_node.stitching_direction = stitching_direction + current_node.already_rastered = True to_transfer_point_list = [] to_transfer_point_list_origin = [] @@ -563,7 +570,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, # TODO: maybe do not take the first and the last # since they are ENTER_LEAVING_POINT points for sure if ( - not offset_by_half + 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 @@ -579,6 +586,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, # 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, @@ -595,6 +603,7 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, if offset_by_half: point_transfer.transfer_points_to_surrounding( tree, + node, stitching_direction * used_offset, False, to_transfer_point_list, @@ -642,10 +651,8 @@ def connect_raster_tree_from_inner_to_outer(tree, used_offset, stitch_distance, # 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( + (child_coords, child_coords_origin) = connect_raster_tree_from_inner_to_outer( + tree, item.child_node, used_offset, stitch_distance, @@ -820,14 +827,14 @@ def connect_raster_tree_spiral( """ abs_offset = abs(used_offset) - if tree.is_leaf: + if not tree['root']: # if node has no children return sample_linestring.raster_line_string_with_priority_points( - tree.val, + tree.nodes['root'].val, 0, - tree.val.length, + tree.nodes['root'].val.length, stitch_distance, min_stitch_distance, - tree.transferred_point_priority_deque, + tree.nodes['root'].transferred_point_priority_deque, abs_offset, offset_by_half, False) @@ -836,86 +843,92 @@ def connect_raster_tree_spiral( 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_linear_rings(ring1, ring2, stitch_distance, starting_point) - node.val = part_spiral + 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 + + part_spiral = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) + tree.nodes[node].val = part_spiral + + for node in nx.dfs_preorder_nodes(tree, 'root'): + if tree[node]: + current_node = tree.nodes[node] + (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( + current_node.val, + 0, + current_node.val.length, + stitch_distance, + min_stitch_distance, + tree.nodes[node].transferred_point_priority_deque, + abs_offset, + offset_by_half, + False) - 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, - min_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( + tree, node, -used_offset, - False, + offset_by_half, own_coords, own_coords_origin, - overnext_neighbor=True, + overnext_neighbor=False, 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, - 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() - 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 + # 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, + -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) - # make sure the next section starts where this - # section of the curve ends - starting_point = result_coords[-1] + 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, + 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() + 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/utils/dotdict.py b/lib/utils/dotdict.py index acd575b9..12cf6e79 100644 --- a/lib/utils/dotdict.py +++ b/lib/utils/dotdict.py @@ -15,7 +15,7 @@ class DotDict(dict): def update(self, *args, **kwargs): super(DotDict, self).update(*args, **kwargs) - self.dotdictify() + self._dotdictify() def _dotdictify(self): for k, v in self.items(): -- cgit v1.2.3 From ca5178fb99ffe1af4331bb7d72b21e007fbb01f1 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Apr 2022 22:09:46 -0400 Subject: more efficient angle calculation --- lib/stitches/sample_linestring.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) (limited to 'lib') diff --git a/lib/stitches/sample_linestring.py b/lib/stitches/sample_linestring.py index 85592984..65760717 100644 --- a/lib/stitches/sample_linestring.py +++ b/lib/stitches/sample_linestring.py @@ -1,4 +1,3 @@ -import math from enum import IntEnum import numpy as np @@ -47,20 +46,19 @@ def calculate_line_angles(line): 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 + angles = np.zeros(len(line.coords)) + + # approach from https://stackoverflow.com/a/50772253/4249120 + vectors = np.diff(line.coords, axis=0) + v1 = vectors[:-1] + v2 = vectors[1:] + dot = np.einsum('ij,ij->i', v1, v2) + mag1 = np.linalg.norm(v1, axis=1) + mag2 = np.linalg.norm(v2, axis=1) + cosines = dot / (mag1 * mag2) + angles[1:-1] = np.arccos(np.clip(cosines, -1, 1)) + + return angles def raster_line_string_with_priority_points(line, # noqa: C901 -- cgit v1.2.3 From f4e1bbc7d7697f994add0baa0f2749ed7d02c0d5 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 28 Apr 2022 22:38:33 -0400 Subject: way better corner handling for running stitch --- lib/stitches/running_stitch.py | 96 +++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 49 deletions(-) (limited to 'lib') diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index 2878480c..cb8acf68 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -3,11 +3,15 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +from ..debug import debug +import math from copy import copy +from shapely.geometry import LineString """ Utility functions to produce running stitches. """ +@debug.time def running_stitch(points, stitch_length): """Generate running stitch along a path. @@ -23,56 +27,50 @@ def running_stitch(points, stitch_length): if len(points) < 2: return [] + # simplify will remove as many points as possible while ensuring that the + # resulting path stays within 0.75 pixels (0.2mm) of the original path. + path = LineString(points) + simplified = path.simplify(0.75, preserve_topology=False) + + # save the points that simplify picked and make sure we stitch them + important_points = set(simplified.coords) + important_point_indices = [i for i, point in enumerate(points) if point.as_tuple() in important_points] + output = [] - segment_start = points[0] - last_segment_direction = None - - # This tracks the distance we've traveled along the current segment so - # far. Each time we make a stitch, we add the stitch_length to this - # value. If we fall off the end of the current segment, we carry over - # the remainder to the next segment. - distance = 0.0 - - for segment_end in points[1:]: - segment = segment_end - segment_start - segment_length = segment.length() - - if segment_length == 0: - continue - - segment_direction = segment.unit() - - # corner detection - if last_segment_direction: - cos_angle_between = segment_direction * last_segment_direction - - # This checks whether the corner is sharper than 45 degrees. - if cos_angle_between < 0.5: - # Only add the corner point if it's more than 0.1mm away to - # avoid a double-stitch. - if (segment_start - output[-1]).length() > 0.1: - # add a stitch at the corner - output.append(segment_start) - - # next stitch needs to be stitch_length along this segment - distance = stitch_length - - while distance < segment_length: - output.append(segment_start + distance * segment_direction) - distance += stitch_length - - # prepare for the next segment - segment_start = segment_end - last_segment_direction = segment_direction - distance -= segment_length - - # stitch a single point if the path has a length of zero - if not output: - output.append(segment_start) - - # stitch the last point unless we're already almost there - if (segment_start - output[-1]).length() > 0.1 or len(output) == 0: - output.append(segment_start) + for start, end in zip(important_point_indices[:-1], important_point_indices[1:]): + # consider sections of the original path, each one starting and ending + # with an important point + section = points[start:end + 1] + output.append(section[0]) + + # Now split each section up evenly into stitches, each with a length no + # greater than the specified stitch_length. + section_ls = LineString(section) + section_length = section_ls.length + if section_length > stitch_length: + # a fractional stitch needs to be rounded up, which will make all + # of the stitches shorter + num_stitches = math.ceil(section_length / stitch_length) + actual_stitch_length = section_length / num_stitches + + distance = actual_stitch_length + + segment_start = section[0] + for segment_end in section[1:]: + segment = segment_end - segment_start + segment_length = segment.length() + + if distance < segment_length: + segment_direction = segment.unit() + + while distance < segment_length: + output.append(segment_start + distance * segment_direction) + distance += actual_stitch_length + + distance -= segment_length + segment_start = segment_end + + output.append(points[-1]) return output -- cgit v1.2.3 From f019d396588f538431b7a1c7d41240d5db66930c Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 28 Apr 2022 23:09:10 -0400 Subject: use running_stitch for single spiral --- .../tangential_fill_stitch_pattern_creator.py | 114 +++------------------ 1 file changed, 13 insertions(+), 101 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 1b085ed6..872cee0e 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -1,18 +1,19 @@ import math from collections import namedtuple +import networkx as nx import numpy as np import trimesh -import networkx as nx from depq import DEPQ from shapely.geometry import MultiPoint, Point from shapely.geometry.polygon import LineString, LinearRing from shapely.ops import nearest_points -from ..debug import debug +from .running_stitch import running_stitch from ..stitches import constants from ..stitches import point_transfer from ..stitches import sample_linestring +from ..stitch_plan import Stitch nearest_neighbor_tuple = namedtuple( "nearest_neighbor_tuple", @@ -746,7 +747,6 @@ def connect_raster_tree_from_inner_to_outer(tree, node, used_offset, stitch_dist def orient_linear_ring(ring): if not ring.is_ccw: - debug.log("reversing a ring") return LinearRing(reversed(ring.coords)) else: return ring @@ -757,7 +757,6 @@ def reorder_linear_ring(ring, start): return np.roll(ring, -start_index, axis=0) -@debug.time def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): """ Interpolate between two LinearRings @@ -803,8 +802,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): +def connect_raster_tree_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 @@ -826,109 +824,23 @@ def connect_raster_tree_spiral( placed at this position """ - abs_offset = abs(used_offset) if not tree['root']: # if node has no children - return sample_linestring.raster_line_string_with_priority_points( - tree.nodes['root'].val, - 0, - tree.nodes['root'].val.length, - stitch_distance, - min_stitch_distance, - tree.nodes['root'].transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) + stitches = [Stitch(*point) for point in tree.nodes['root'].val.coords] + return running_stitch(stitches, stitch_distance) + + # TODO: cut each ring near close_point - result_coords = [] - result_coords_origin = [] starting_point = close_point.coords[0] - # iterate to the second last level + 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 - part_spiral = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) - tree.nodes[node].val = part_spiral + spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point) + path.extend(spiral_part.coords) - for node in nx.dfs_preorder_nodes(tree, 'root'): - if tree[node]: - current_node = tree.nodes[node] - (own_coords, own_coords_origin) = sample_linestring.raster_line_string_with_priority_points( - current_node.val, - 0, - current_node.val.length, - stitch_distance, - min_stitch_distance, - tree.nodes[node].transferred_point_priority_deque, - abs_offset, - offset_by_half, - False) - - point_transfer.transfer_points_to_surrounding( - tree, - 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( - tree, - 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, - 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() - 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] + path = [Stitch(*stitch) for stitch in path] - assert len(result_coords) == len(result_coords_origin) - return result_coords, result_coords_origin + return running_stitch(path, stitch_distance), None -- cgit v1.2.3 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') 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.2.3 From ff0aa68876c5b6840a5d7f01ef2af6c848190570 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Apr 2022 14:41:27 -0400 Subject: properly link holes to outers --- lib/stitches/tangential_fill_stitch_line_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 46b5a262..e0bf62e6 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -343,7 +343,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, # contained in the new holes they # have been merged with the # outer polygon - if tree.nodes[previous_hole].parent is None: + if not tree.nodes[previous_hole].parent: tree.nodes[previous_hole].parent = current_poly tree.add_edge(current_poly, previous_hole) -- cgit v1.2.3 From e14fffb879875cae43124a7daae7cebb5bcadddc Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Apr 2022 15:21:13 -0400 Subject: remove redundant cut() function --- .../tangential_fill_stitch_pattern_creator.py | 32 ++-------------------- lib/utils/geometry.py | 29 +++++++++++++++++++- 2 files changed, 31 insertions(+), 30 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 92a508cd..c3ad0047 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -14,6 +14,7 @@ from ..stitches import constants from ..stitches import point_transfer from ..stitches import sample_linestring from ..stitch_plan import Stitch +from ..utils.geometry import cut, reverse_line_string, roll_linear_ring nearest_neighbor_tuple = namedtuple( "nearest_neighbor_tuple", @@ -26,34 +27,6 @@ nearest_neighbor_tuple = namedtuple( ) -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 get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): """ Takes a line and calculates the nearest distance along this @@ -237,7 +210,7 @@ def connect_raster_tree_from_inner_to_outer(tree, node, used_offset, stitch_dist start_distance = current_coords.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) + current_coords = roll_linear_ring(current_coords, start_distance) current_node.val = current_coords if not current_node.transferred_point_priority_deque.is_empty(): @@ -254,6 +227,7 @@ def connect_raster_tree_from_inner_to_outer(tree, node, used_offset, stitch_dist # We try to use always the opposite stitching direction with respect to the # parent to avoid crossings when entering and leaving the child + # LEX: this seems like a lie ^^ parent_stitching_direction = -1 if current_node.parent is not None: parent_stitching_direction = tree.nodes[current_node.parent].stitching_direction diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index bce278ed..ed1e2c0e 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -5,7 +5,7 @@ import math -from shapely.geometry import LineString +from shapely.geometry import LineString, LinearRing from shapely.geometry import Point as ShapelyPoint @@ -39,6 +39,33 @@ def cut(line, distance, normalized=False): LineString([(cp.x, cp.y)] + coords[i:])] +def roll_linear_ring(ring, distance, normalized=False): + """Make a linear ring start at a different point. + + Example: A B C D E F G A -> D E F G A B C + + Same linear ring, different ordering of the coordinates. + """ + + if not isinstance(ring, LinearRing): + # In case they handed us a LineString + ring = LinearRing(ring) + + pieces = cut(LinearRing(ring), distance, normalized=False) + + if None in pieces: + # We cut exactly at the start or end. + return ring + + # The first and last point in a linear ring are duplicated, so we omit one + # copy + return LinearRing(pieces[1].coords[:] + pieces[0].coords[1:]) + + +def reverse_line_string(line_string): + return LineString(line_string.coords[::-1]) + + def cut_path(points, length): """Return a subsection of at the start of the path that is length units long. -- cgit v1.2.3 From 0625f250c6cfab821f241fbe31ee1c7e0e24bfc1 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Apr 2022 20:17:28 -0400 Subject: add missing polygonize() --- lib/stitches/tangential_fill_stitch_line_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index e0bf62e6..466fd6b6 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -288,7 +288,7 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, result = Polygon(outer).difference( MultiPolygon(poly_inners)) else: - result = MultiPolygon(outer).difference( + result = MultiPolygon(polygonize(outer)).difference( MultiPolygon(poly_inners)) if not result.is_empty and result.area > offset * offset / 10: -- cgit v1.2.3 From 899a15b8e160d8248b2d93764b424b99b88645ab Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Apr 2022 20:18:25 -0400 Subject: 4x speedup and simpler code --- .../tangential_fill_stitch_pattern_creator.py | 55 ++++++++++++---------- 1 file changed, 29 insertions(+), 26 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index c3ad0047..35ab957f 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -1,20 +1,20 @@ import math from collections import namedtuple - import networkx as nx import numpy as np import trimesh from depq import DEPQ -from shapely.geometry import MultiPoint, Point -from shapely.geometry.polygon import LineString, LinearRing +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 ..stitches import point_transfer from ..stitches import sample_linestring from ..stitch_plan import Stitch -from ..utils.geometry import cut, reverse_line_string, roll_linear_ring +from ..utils.geometry import roll_linear_ring nearest_neighbor_tuple = namedtuple( "nearest_neighbor_tuple", @@ -27,38 +27,41 @@ nearest_neighbor_tuple = namedtuple( ) -def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh): +@debug.time +def get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): """ - Takes a line and calculates the nearest distance along this - line to enter the next_line + 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 - -thresh: The distance between travel_line and next_line needs - to below thresh to be a valid point for entering + -threshold: The distance between travel_line and next_line needs + to below threshold to be a valid point for entering + Output: - -tuple - the tuple structure is: - (nearest point in travel_line, nearest point in next_line) + -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. """ - 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) + # 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 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: + 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( -- cgit v1.2.3 From eefb3460e31bc39060deafee3128533f08be53f1 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Apr 2022 20:28:23 -0400 Subject: just go one direction --- .../tangential_fill_stitch_pattern_creator.py | 65 +++------------------- 1 file changed, 7 insertions(+), 58 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 35ab957f..8fe29910 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -89,48 +89,17 @@ def create_nearest_points_list( distance along travel_line, belonging child) """ - result_list_in_order = [] - result_list_reversed_order = [] + children_nearest_points = [] - 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, tree.nodes[child].val, threshold - ) + 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 - ) - 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, - ) - ) + # up to 2 * used offset can arise + result = get_nearest_points_closer_than_thresh(travel_line, tree.nodes[child].val, threshold_hard) - result = get_nearest_points_closer_than_thresh( - travel_line_reversed, 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_reversed, tree.nodes[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( + proj = travel_line.project(result[0]) + children_nearest_points.append( nearest_neighbor_tuple( nearest_point_parent=result[0], nearest_point_child=result[1], @@ -139,27 +108,7 @@ def create_nearest_points_list( ) ) - 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) + return (1, children_nearest_points) def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): -- cgit v1.2.3 From fc3d05845a9e04a9dbe69e8ed6a025e3e77e6349 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 1 May 2022 13:16:23 -0400 Subject: simpler, faster inner-to-outer algo --- .../tangential_fill_stitch_line_creator.py | 9 +- .../tangential_fill_stitch_pattern_creator.py | 338 ++++----------------- 2 files changed, 73 insertions(+), 274 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 466fd6b6..416974c5 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -10,8 +10,11 @@ from shapely.ops import polygonize 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 + class Tree(nx.DiGraph): # This lets us do tree.nodes['somenode'].parent instead of the default @@ -350,8 +353,10 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, make_tree_uniform_ccw(tree) 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) + 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) + path = [Stitch(*point) for point in connected_line.coords] + return running_stitch(path, stitch_distance), "whatever" elif strategy == StitchingStrategy.SPIRAL: if not check_and_prepare_tree_for_valid_spiral(tree): raise ValueError("Geometry cannot be filled with one spiral!") diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 8fe29910..4abe498d 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -3,7 +3,6 @@ from collections import namedtuple import networkx as nx import numpy as np import trimesh -from depq import DEPQ from shapely.geometry import Point, LineString, LinearRing, MultiLineString from shapely.ops import nearest_points @@ -11,10 +10,9 @@ from .running_stitch import running_stitch from ..debug import debug from ..stitches import constants -from ..stitches import point_transfer from ..stitches import sample_linestring from ..stitch_plan import Stitch -from ..utils.geometry import roll_linear_ring +from ..utils.geometry import cut, roll_linear_ring, reverse_line_string nearest_neighbor_tuple = namedtuple( "nearest_neighbor_tuple", @@ -65,7 +63,7 @@ def get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): def create_nearest_points_list( - travel_line, tree, children_list, threshold, threshold_hard, preferred_direction=0): + 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 @@ -108,7 +106,7 @@ def create_nearest_points_list( ) ) - return (1, children_nearest_points) + return children_nearest_points def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance): @@ -128,10 +126,11 @@ 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, node, used_offset, stitch_distance, min_stitch_distance, close_point, +@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 """ - Takes the offsetted curves organized as tree, connects and samples them. + 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. @@ -154,281 +153,76 @@ def connect_raster_tree_from_inner_to_outer(tree, node, used_offset, stitch_dist """ current_node = tree.nodes[node] - current_coords = current_node.val - abs_offset = abs(used_offset) - result_coords = [] - result_coords_origin = [] - - start_distance = current_coords.project(close_point) - # We cut the current path so that its index 0 is closest to close_point - if start_distance > 0: - current_coords = roll_linear_ring(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 - - # We try to use always the opposite stitching direction with respect to the - # parent to avoid crossings when entering and leaving the child - # LEX: this seems like a lie ^^ - parent_stitching_direction = -1 - if current_node.parent is not None: - parent_stitching_direction = tree.nodes[current_node.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, + current_ring = current_node.val + + # 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 * 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 initial/end - # point twice (avoid double points for start - # and end) - end_offset, - stitch_distance, - min_stitch_distance, - current_node.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 initial/end point - # twice (avoid double - # points for start - # and end) - current_coords.length - end_offset, - stitch_distance, - min_stitch_distance, - current_node.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) - - current_node.stitching_direction = stitching_direction - current_node.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, - 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, + constants.offset_factor_for_adjacent_geometry * offset, + 2.05 * offset ) + nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) - # 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, - ) - + result_coords = [] 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 + # 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) else: - # There are childs so we need to merge their coordinates - # with our own rastered coords + # 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, + min_stitch_distance, + child_connection.nearest_point_child, + offset_by_half, + ) + result_coords.extend(child_path.coords) - # Create a closed ring for the following code - own_coords.append(own_coords[0]) - own_coords_origin.append(own_coords_origin[0]) + # 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 - # 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_ring = after - cur_item = 0 - result_coords = [own_coords[0]] - result_coords_origin = [own_coords_origin[0]] + 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) - 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( - tree, - item.child_node, - used_offset, - stitch_distance, - min_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 + result_coords.extend(current_ring.coords) + + return LineString(result_coords) def orient_linear_ring(ring): -- cgit v1.2.3 From e6fcf11035d3d953c2b07e6d153a1225f79cb781 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 1 May 2022 16:31:51 -0400 Subject: fix some shapelydeprecations --- lib/stitches/auto_fill.py | 6 +- lib/stitches/guided_fill.py | 6 +- .../tangential_fill_stitch_line_creator.py | 75 ---------------------- 3 files changed, 6 insertions(+), 81 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index a8118039..27328bab 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -90,7 +90,7 @@ def which_outline(shape, coords): # fail sometimes. point = shgeo.Point(*coords) - outlines = list(shape.boundary) + outlines = list(shape.boundary.geoms) outline_indices = list(range(len(outlines))) closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) @@ -104,7 +104,7 @@ def project(shape, coords, outline_index): This returns the distance along the outline at which the point resides. """ - outline = list(shape.boundary)[outline_index] + outline = list(shape.boundary.geoms)[outline_index] return outline.project(shgeo.Point(*coords)) @@ -204,7 +204,7 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): def add_boundary_travel_nodes(graph, shape): - for outline_index, outline in enumerate(shape.boundary): + for outline_index, outline in enumerate(shape.boundary.geoms): prev = None for point in outline.coords: point = shgeo.Point(point) diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index fb122fba..9170d66f 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -12,7 +12,7 @@ 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 +from ..utils.geometry import Point as InkstitchPoint, reverse_line_string @debug.time @@ -276,7 +276,7 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False line_offsetted = repair_non_simple_lines(line_offsetted) if row_spacing < 0: - line_offsetted.coords = line_offsetted.coords[::-1] + line_offsetted = reverse_line_string(line_offsetted) line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) if row_spacing > 0 and not isinstance(res, (GeometryCollection, MultiLineString)): @@ -290,7 +290,7 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False 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 = reverse_line_string(line_offsetted) line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) return rows diff --git a/lib/stitches/tangential_fill_stitch_line_creator.py b/lib/stitches/tangential_fill_stitch_line_creator.py index 416974c5..042bdde0 100644 --- a/lib/stitches/tangential_fill_stitch_line_creator.py +++ b/lib/stitches/tangential_fill_stitch_line_creator.py @@ -33,81 +33,6 @@ 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) - def take_only_valid_linear_rings(rings): """ -- cgit v1.2.3 From bd8cb0d1ff2ce1f17ed8d3a5eaca19f8e8652ad0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 14:38:33 -0400 Subject: use running_stitch instead for guided fill --- lib/stitches/auto_fill.py | 23 +++--- lib/stitches/fill.py | 6 +- lib/stitches/guided_fill.py | 170 +++++++++----------------------------------- lib/svg/path.py | 6 ++ 4 files changed, 48 insertions(+), 157 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 27328bab..35412e93 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -59,9 +59,9 @@ def auto_fill(shape, starting_point, ending_point=None, underpath=True): - fill_stitch_graph = [] try: - fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point, ending_point) + segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) + 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 return fallback(shape, running_stitch_length) @@ -109,7 +109,7 @@ def project(shape, coords, outline_index): @debug.time -def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None): +def build_fill_stitch_graph(shape, segments, 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 @@ -144,10 +144,6 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting debug.add_layer("auto-fill fill stitch") - # 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() # First, add the grating segments as edges. We'll use the coordinates @@ -155,7 +151,7 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting 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=[], geometry=shgeo.LineString(segment)) + graph.add_edge(segment[0], segment[-1], key="segment", underpath_edges=[], geometry=shgeo.LineString(segment)) tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -177,7 +173,7 @@ def insert_node(graph, shape, point): point = tuple(point) outline = which_outline(shape, point) projection = project(shape, point, outline) - projected_point = list(shape.boundary)[outline].interpolate(projection) + projected_point = list(shape.boundary.geoms)[outline].interpolate(projection) node = (projected_point.x, projected_point.y) edges = [] @@ -395,10 +391,9 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): def travel_grating(shape, angle, row_spacing): - rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing) - segments = list(chain(*rows_of_segments)) + segments = intersect_region_with_grating(shape, angle, row_spacing) - return shgeo.MultiLineString(segments) + return shgeo.MultiLineString(list(segments)) def ensure_multi_line_string(thing): @@ -457,7 +452,7 @@ def build_travel_edges(shape, fill_angle): debug.log_line_strings(grating3, "grating3") endpoints = [coord for mls in (grating1, grating2, grating3) - for ls in mls + for ls in mls.geoms for coord in ls.coords] diagonal_edges = ensure_multi_line_string( @@ -467,7 +462,7 @@ def build_travel_edges(shape, fill_angle): vertical_edges = ensure_multi_line_string( snap(grating3.difference(grating1), diagonal_edges, 0.005)) - return endpoints, chain(diagonal_edges, vertical_edges) + return endpoints, chain(diagonal_edges.geoms, vertical_edges.geoms) def nearest_node(nodes, point, attr=None): diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 1fdc6fac..a09b93b1 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -136,8 +136,6 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non # fill regions at the same angle and spacing always line up nicely. start -= (start + normal * center) % row_spacing - rows = [] - current_row_y = start while current_row_y < end: @@ -165,7 +163,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non runs.reverse() runs = [tuple(reversed(run)) for run in runs] - rows.append(runs) + yield from runs if end_row_spacing: current_row_y += row_spacing + \ @@ -174,8 +172,6 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non else: current_row_y += row_spacing - return rows - def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last): stitches = [] diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 9170d66f..40728c53 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -1,14 +1,10 @@ -import networkx -from depq import DEPQ -from shapely.geometry import GeometryCollection, LineString, MultiLineString +from shapely import geometry as shgeo from shapely.ops import linemerge, unary_union -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) -from .point_transfer import transfer_points_to_surrounding_graph -from .sample_linestring import raster_line_string_with_priority_points +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 @@ -28,11 +24,9 @@ def guided_fill(shape, 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) + segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing) + 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 return fallback(shape, running_stitch_length) @@ -48,108 +42,6 @@ def guided_fill(shape, 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 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) - else: - stitched_line, _ = raster_line_string_with_priority_points( - 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): - 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, min_stitch_length, running_stitch_length, skip_last, offset_by_half): @@ -163,23 +55,23 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, 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, 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) - 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) + + if edge[0] != path_geometry.coords[0]: + 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) + + # need to tag stitches + + if skip_last: + del new_stitches[-1] stitches.extend(new_stitches) + + 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)) @@ -195,14 +87,14 @@ def extend_line(line, minx, maxx, miny, maxy): point1 = InkstitchPoint(*line.coords[0]) point2 = InkstitchPoint(*line.coords[1]) - new_starting_point = point1-(point2-point1).unit()*length + 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 + new_ending_point = point4 + (point4 - point3).unit() * length - return LineString([new_starting_point.as_tuple()] + - line.coords[1:-1]+[new_ending_point.as_tuple()]) + return shgeo.LineString([new_starting_point.as_tuple()] + + line.coords[1:-1] + [new_ending_point.as_tuple()]) def repair_multiple_parallel_offset_curves(multi_line): @@ -251,15 +143,15 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False 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)): + while isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)) or (not res.is_empty and len(res.coords) > 1): + if isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)): runs = [line_string.coords for line_string in res.geoms if ( - not line_string.is_empty and len(line_string.coords) > 1)] + 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()) + InkstitchPoint(*seg[0]) - upper_left).length()) if flip: runs.reverse() runs = [tuple(reversed(run)) for run in runs] @@ -279,7 +171,7 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False line_offsetted = reverse_line_string(line_offsetted) line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - if row_spacing > 0 and not isinstance(res, (GeometryCollection, MultiLineString)): + if row_spacing > 0 and not isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)): if (res.is_empty or len(res.coords) == 1): row_spacing = -row_spacing @@ -293,4 +185,6 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False line_offsetted = reverse_line_string(line_offsetted) line_offsetted = line_offsetted.simplify(0.01, False) res = line_offsetted.intersection(shape) - return rows + + for row in rows: + yield from row diff --git a/lib/svg/path.py b/lib/svg/path.py index c33a7a8f..6c2cbe35 100644 --- a/lib/svg/path.py +++ b/lib/svg/path.py @@ -74,6 +74,12 @@ def get_correction_transform(node, child=False): def line_strings_to_csp(line_strings): + try: + # This lets us accept a MultiLineString or a list. + line_strings = line_strings.geoms + except AttributeError: + pass + return point_lists_to_csp(ls.coords for ls in line_strings) -- cgit v1.2.3 From 8a1f70a6cdcbf7c599d9f00837996a0f6734f780 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 14:41:34 -0400 Subject: remove sample_linestring and point_transfer --- lib/stitches/point_transfer.py | 503 --------------------- lib/stitches/sample_linestring.py | 342 -------------- .../tangential_fill_stitch_pattern_creator.py | 18 - 3 files changed, 863 deletions(-) delete mode 100644 lib/stitches/point_transfer.py delete mode 100644 lib/stitches/sample_linestring.py (limited to 'lib') diff --git a/lib/stitches/point_transfer.py b/lib/stitches/point_transfer.py deleted file mode 100644 index c0d519ef..00000000 --- a/lib/stitches/point_transfer.py +++ /dev/null @@ -1,503 +0,0 @@ -import math -from collections import namedtuple - -from shapely.geometry import LineString, LinearRing, MultiPoint, Point -from shapely.ops import nearest_points - -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.""" - -projected_point_tuple = namedtuple( - 'projected_point_tuple', ['point', 'point_source']) - - -def calc_transferred_point(bisectorline, child): - """ - Calculates the nearest intersection 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(tree, node, 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) < 3: - return - - current_node = tree.nodes[node] - - # Get a list of all possible adjacent nodes which will be considered for transferring the points of treenode: - childs_tuple = tuple(tree.successors(node)) - if current_node.parent: - siblings_tuple = tuple(child for child in tree[current_node.parent] if child != node) - else: - siblings_tuple = () - - # 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 tree.nodes[child].already_rastered: - if not overnext_neighbor: - child_list.append(child) - if transfer_forbidden_points: - child_list_forbidden.append(child) - if overnext_neighbor: - for grandchild in tree[child]: - if not tree.nodes[grandchild].already_rastered: - child_list.append(grandchild) - - if transfer_to_sibling: - for sibling in siblings_tuple: - if not tree.nodes[sibling].already_rastered: - if not overnext_neighbor: - neighbor_list.append(sibling) - if transfer_forbidden_points: - neighbor_list_forbidden.append(sibling) - if overnext_neighbor: - for nibling in tree[sibling]: - if not tree.nodes[nibling].already_rastered: - neighbor_list.append(nibling) - - if transfer_to_parent and current_node.parent is not None: - if not tree.nodes[current_node.parent].already_rastered: - if not overnext_neighbor: - neighbor_list.append(current_node.parent) - if transfer_forbidden_points: - neighbor_list_forbidden.append(current_node.parent) - if overnext_neighbor: - grandparent = tree.nodes[current_node].parent - if grandparent is not None: - if not tree.nodes[grandparent].already_rastered: - neighbor_list.append(grandparent) - - 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: - current_child = tree.nodes[child] - point, priority = calc_transferred_point(bisectorline_child, current_child) - if point is None: - continue - current_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: - current_child = tree.nodes[child] - point, priority = calc_transferred_point(bisectorline_forbidden_point_child, current_child) - if point is None: - continue - current_child.transferred_point_priority_deque.insert(projected_point_tuple( - point=point, point_source=sample_linestring.PointSource.FORBIDDEN_POINT), priority) - - for neighbor in neighbor_list: - current_neighbor = tree.nodes[neighbor] - point, priority = calc_transferred_point(bisectorline_neighbor, current_neighbor) - if point is None: - continue - current_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: - current_neighbor = tree.nodes[neighbor] - point, priority = calc_transferred_point(bisectorline_forbidden_point_neighbor, current_neighbor) - if point is None: - continue - current_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 deleted file mode 100644 index 65760717..00000000 --- a/lib/stitches/sample_linestring.py +++ /dev/null @@ -1,342 +0,0 @@ -from enum import IntEnum - -import numpy as np -from shapely.geometry import LineString, Point -from shapely.ops import substring - -from ..stitches import constants, 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)) - - # approach from https://stackoverflow.com/a/50772253/4249120 - vectors = np.diff(line.coords, axis=0) - v1 = vectors[:-1] - v2 = vectors[1:] - dot = np.einsum('ij,ij->i', v1, v2) - mag1 = np.linalg.norm(v1, axis=1) - mag2 = np.linalg.norm(v2, axis=1) - cosines = dot / (mag1 * mag2) - angles[1:-1] = np.arccos(np.clip(cosines, -1, 1)) - - return angles - - -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: - -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 - -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) - 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) < 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) - - 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, 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)): - 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 < 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 - 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): - 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: - 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_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 4abe498d..0ee1c031 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -10,7 +10,6 @@ from .running_stitch import running_stitch from ..debug import debug from ..stitches import constants -from ..stitches import sample_linestring from ..stitch_plan import Stitch from ..utils.geometry import cut, roll_linear_ring, reverse_line_string @@ -109,23 +108,6 @@ def create_nearest_points_list( return children_nearest_points -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] - - @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 -- cgit v1.2.3 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') 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.2.3 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') 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.2.3 From 056a1cc45d3744c44314572e710fcc60b548fd23 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 22:43:38 -0400 Subject: fix start position in single spiral --- lib/stitches/tangential_fill_stitch_pattern_creator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill_stitch_pattern_creator.py b/lib/stitches/tangential_fill_stitch_pattern_creator.py index 553241f8..9b59fa07 100644 --- a/lib/stitches/tangential_fill_stitch_pattern_creator.py +++ b/lib/stitches/tangential_fill_stitch_pattern_creator.py @@ -221,8 +221,8 @@ 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)) + distances = ring - start + start_index = np.argmin(np.linalg.norm(distances, axis=1)) return np.roll(ring, -start_index, axis=0) -- cgit v1.2.3 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') 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.2.3 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') 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.2.3 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') 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.2.3 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 ------------- 5 files changed, 574 insertions(+), 649 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') 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 -- cgit v1.2.3 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 - 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'lib') 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 -- cgit v1.2.3 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') 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.2.3 From ff0e28d3e552234f634867eaa613cdbb3b2e7c3c Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 May 2022 20:56:18 -0400 Subject: bugfix --- lib/stitches/tangential_fill.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/stitches/tangential_fill.py b/lib/stitches/tangential_fill.py index 3cc3335f..833f9db3 100644 --- a/lib/stitches/tangential_fill.py +++ b/lib/stitches/tangential_fill.py @@ -251,10 +251,9 @@ def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): 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]) + # 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) @@ -347,9 +346,9 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, 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 + # 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 @@ -360,7 +359,7 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, 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 + distance_so_far = child_connection.proj_distance_parent # Stitch the part leading up to this child. if before is not None: -- cgit v1.2.3 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') 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.2.3 From a7153c52e2c295ef2a7296ec09bcd7cd5317809d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 6 May 2022 20:42:11 -0400 Subject: best-effort rather than throwing exception --- lib/stitches/contour_fill.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py index 916285d8..a4ee727c 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -507,17 +507,38 @@ def double_spiral(tree, stitch_length, starting_point): 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')] + + rings = _get_spiral_rings(tree) path = spiral_maker(rings, stitch_length, starting_point) path = [Stitch(*stitch) for stitch in path] return running_stitch(path, stitch_length) +def _get_spiral_rings(tree): + rings = [] + + node = 'root' + while True: + rings.append(tree.nodes[node].val) + + children = tree[node] + if len(children) == 0: + break + elif len(children) == 1: + node = list(children)[0] + else: + # We can only really fill a shape with a single spiral if each + # parent has only one child. We'll do our best though, because + # that is probably more helpful to the user than just refusing + # entirely. We'll pick the child that's closest to the center. + parent_center = rings[-1].centroid + node = min(children, key=lambda child: parent_center.distance(tree.nodes[child].val.centroid)) + + return rings + + 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) -- cgit v1.2.3 From 672bded1259589d609d1a6656df5537c5da20569 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 6 May 2022 21:03:56 -0400 Subject: shapely geoms fixes --- lib/extensions/break_apart.py | 2 +- lib/stitches/auto_fill.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py index 5bfd88a4..581e49bc 100644 --- a/lib/extensions/break_apart.py +++ b/lib/extensions/break_apart.py @@ -83,7 +83,7 @@ class BreakApart(InkstitchExtension): if diff.geom_type == 'MultiPolygon': polygons.remove(other) polygons.remove(polygon) - for p in diff: + for p in diff.geoms: polygons.append(p) # it is possible, that a polygons overlap with multiple # polygons, this means, we need to start all over again diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index b3b9434f..1d72e710 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -90,7 +90,7 @@ def which_outline(shape, coords): # fail sometimes. point = shgeo.Point(*coords) - outlines = list(shape.boundary.geoms) + outlines = ensure_multi_line_string(shape.boundary).geoms outline_indices = list(range(len(outlines))) closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) @@ -104,7 +104,7 @@ def project(shape, coords, outline_index): This returns the distance along the outline at which the point resides. """ - outline = list(shape.boundary.geoms)[outline_index] + outline = ensure_multi_line_string(shape.boundary).geoms[outline_index] return outline.project(shgeo.Point(*coords)) @@ -173,7 +173,7 @@ def insert_node(graph, shape, point): point = tuple(point) outline = which_outline(shape, point) projection = project(shape, point, outline) - projected_point = list(shape.boundary.geoms)[outline].interpolate(projection) + projected_point = ensure_multi_line_string(shape.boundary).geoms[outline].interpolate(projection) node = (projected_point.x, projected_point.y) edges = [] @@ -200,7 +200,8 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): def add_boundary_travel_nodes(graph, shape): - for outline_index, outline in enumerate(shape.boundary.geoms): + outlines = ensure_multi_line_string(shape.boundary).geoms + for outline_index, outline in enumerate(outlines): prev = None for point in outline.coords: point = shgeo.Point(point) @@ -265,7 +266,10 @@ def fallback(shape, running_stitch_length): matter. """ - return running_stitch(line_string_to_point_list(shape.boundary[0]), running_stitch_length) + boundary = ensure_multi_line_string(shape.boundary) + outline = boundary.geoms[0] + + return running_stitch(line_string_to_point_list(outline), running_stitch_length) @debug.time -- cgit v1.2.3 From 469c32a4975b241e583fd09d21959896e8988d63 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 6 May 2022 23:30:54 -0400 Subject: remove constants --- lib/stitches/constants.py | 52 -------------------------------------------- lib/stitches/contour_fill.py | 18 +++++---------- 2 files changed, 6 insertions(+), 64 deletions(-) delete mode 100644 lib/stitches/constants.py (limited to 'lib') diff --git a/lib/stitches/constants.py b/lib/stitches/constants.py deleted file mode 100644 index 012fac7c..00000000 --- a/lib/stitches/constants.py +++ /dev/null @@ -1,52 +0,0 @@ -import math - -# Used in the simplify routine of shapely -simplification_threshold = 0.01 - -# 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 -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 -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_factor = 1.5 - -# Used to handle numerical inaccuracies during comparisons -eps = 1e-3 - -# 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 - -# if points are closer than abs_offset*factor_offset_remove_points one of it is removed -factor_offset_remove_points = 0.5 - -# 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 - -# 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 - -# 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 - -# 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/contour_fill.py b/lib/stitches/contour_fill.py index a4ee727c..2466a04b 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -10,9 +10,7 @@ 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 @@ -48,9 +46,8 @@ nearest_neighbor_tuple = namedtuple( 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) + rings = rings.simplify(0.01, False) return _take_only_valid_linear_rings(rings) @@ -197,7 +194,6 @@ def _convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes): 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: @@ -257,8 +253,7 @@ def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold): return nearest_points(parent_point, next_line) -def _create_nearest_points_list( - travel_line, tree, children, threshold, threshold_hard): +def _create_nearest_points_list(travel_line, tree, children, threshold, threshold_hard): """Determine the best place to enter each of parent's children Arguments: @@ -297,8 +292,7 @@ def _create_nearest_points_list( return children_nearest_points -def _find_path_inner_to_outer(tree, node, offset, starting_point, - avoid_self_crossing, forward=True): +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 @@ -338,8 +332,8 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, current_ring, tree, tree[node], - constants.offset_factor_for_adjacent_geometry * offset, - 2.05 * offset + threshold=1.5 * offset, + threshold_hard=2.05 * offset ) nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent) @@ -451,7 +445,7 @@ def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None): points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights) result = LineString(points) - return result.simplify(constants.simplification_threshold, False) + return result.simplify(0.1, False) def _check_and_prepare_tree_for_valid_spiral(tree): -- cgit v1.2.3 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') 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.2.3 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') 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.2.3 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') 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.2.3 From 1b0d46af36c38fda204f4107d1b8b469277fefa6 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Tue, 17 May 2022 17:51:16 +0200 Subject: geoms: an other one --- lib/patterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/patterns.py b/lib/patterns.py index 1650523c..aca6155c 100644 --- a/lib/patterns.py +++ b/lib/patterns.py @@ -62,7 +62,7 @@ def _get_pattern_points(first, second, pattern): if isinstance(intersection, shgeo.Point): points.append(Point(intersection.x, intersection.y)) if isinstance(intersection, shgeo.MultiPoint): - for point in intersection: + for point in intersection.geoms: points.append(Point(point.x, point.y)) # sort points after their distance to first points.sort(key=lambda point: point.distance(first)) -- cgit v1.2.3 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') 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.2.3