added tangential and guided fill

pull/1548/head
Andreas 2021-10-21 16:24:40 +02:00 zatwierdzone przez Kaalleen
rodzic ea66eb8685
commit 0fcf8bb97c
23 zmienionych plików z 2440 dodań i 302 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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))

Wyświetl plik

@ -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)

Wyświetl plik

@ -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]

Wyświetl plik

@ -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))

Wyświetl plik

@ -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,

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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:

Wyświetl plik

@ -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 = """<marker
refX="10"
refY="5"
orient="auto"
id="inkstitch-guide-line-marker">
<g
id="inkstitch-guide-line-group">
<path
style="fill:#fafafa;stroke:#ff00ff;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1, 1;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:0.8;"
d="M 10.12911,5.2916678 A 4.8374424,4.8374426 0 0 1 5.2916656,10.12911 4.8374424,4.8374426 0 0 1 0.45422399,5.2916678 4.8374424,4.8374426 0 0 1 5.2916656,0.45422399 4.8374424,4.8374426 0 0 1 10.12911,5.2916678 Z"
id="inkstitch-guide-line-marker-circle" />
<path
style="fill:none;stroke:#ff00ff;stroke-width:0.4;stroke-linecap:round;stroke-miterlimit:4;"
id="inkstitch-guide-line-marker-spiral"
d="M 4.9673651,5.7245662 C 4.7549848,5.7646159 4.6247356,5.522384 4.6430021,5.3419847 4.6765851,5.0103151 5.036231,4.835347 5.3381858,4.8987426 5.7863901,4.9928495 6.0126802,5.4853625 5.9002872,5.9065088 5.7495249,6.4714237 5.1195537,6.7504036 4.5799191,6.5874894 3.898118,6.3816539 3.5659013,5.6122905 3.7800789,4.9545192 4.0402258,4.1556558 4.9498996,3.7699484 5.7256318,4.035839 6.6416744,4.3498087 7.0810483,5.4003986 6.7631909,6.2939744 6.395633,7.3272552 5.2038143,7.8204128 4.1924535,7.4503931 3.0418762,7.0294421 2.4948761,5.6961604 2.9171752,4.567073 3.3914021,3.2991406 4.8663228,2.6982592 6.1130974,3.1729158 7.4983851,3.7003207 8.1531869,5.3169977 7.6260947,6.6814205 7.0456093,8.1841025 5.2870784,8.8928844 3.8050073,8.3132966 2.1849115,7.6797506 1.4221671,5.7793073 2.0542715,4.1796074 2.7408201,2.4420977 4.7832541,1.6253548 6.5005435,2.310012 8.3554869,3.0495434 9.2262638,5.2339874 8.4890181,7.0688861 8.4256397,7.2266036 8.3515789,7.379984 8.2675333,7.5277183" />
</g>
</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))

Wyświetl plik

@ -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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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])<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)
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 (<abs_offset) we remove one of it
if (((merged_point_list[segment_start_index][0].point_source == PointSource.OVERNEXT and
merged_point_list[segment_end_index][0].point_source == PointSource.DIRECT) or
(merged_point_list[segment_start_index][0].point_source == PointSource.DIRECT and
merged_point_list[segment_end_index][0].point_source == PointSource.OVERNEXT)) and
abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset):
result_list.pop()
result_list.append(merged_point_list[segment_end_index])
#To have a chance to replace all forbidden points afterwards
if merged_point_list[segment_end_index][0].point_source == PointSource.FORBIDDEN_POINT:
forbidden_point_list.append(len(result_list)-1)
segment_start_index = segment_end_index
segment_end_index+=1
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)
#Finally we create the final return_point_list and return_point_source_list
for i in range(len(result_list)):
return_point_list.append(result_list[i][0].point.coords[0])
#if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2:
# print("GEFUNDEN")
if result_list[i][0].point_source == PointSource.HARD_EDGE_INTERNAL:
point_source = PointSource.HARD_EDGE
elif result_list[i][0].point_source == PointSource.SOFT_EDGE_INTERNAL:
point_source = PointSource.SOFT_EDGE
elif result_list[i][0].point_source == PointSource.REGULAR_SPACING_INTERNAL:
point_source = PointSource.REGULAR_SPACING
elif result_list[i][0].point_source == PointSource.FORBIDDEN_POINT:
point_source = PointSource.FORBIDDEN_POINT
else:
point_source = PointSource.PROJECTED_POINT
return_point_source_list.append(point_source)
assert(len(return_point_list) == len(return_point_source_list))
#return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset)
return return_point_list, return_point_source_list
#Rasters a line between start_distance and end_distance.
#Input:
#-line: The line to be rastered
#-start_distance: The distance along the line from which the rastering should start
#-end_distance: The distance along the line until which the rastering should be done
#-maxstitch_distance: The maximum allowed stitch distance
#-stitching_direction: =1 is stitched along line direction, =-1 if stitched in reversed order. Note that
# 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_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 (<abs_offset) we remove one of it
if (((merged_point_list[segment_start_index][0].point_source == PointSource.OVERNEXT and
merged_point_list[segment_end_index][0].point_source == PointSource.DIRECT) or
(merged_point_list[segment_start_index][0].point_source == PointSource.DIRECT and
merged_point_list[segment_end_index][0].point_source == PointSource.OVERNEXT)) and
abs(merged_point_list[segment_end_index][1] - merged_point_list[segment_start_index][1]) < abs_offset):
result_list.pop()
result_list.append(merged_point_list[segment_end_index])
#To have a chance to replace all forbidden points afterwards
if merged_point_list[segment_end_index][0].point_source == PointSource.FORBIDDEN_POINT:
forbidden_point_list.append(len(result_list)-1)
segment_start_index = segment_end_index
segment_end_index+=1
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)
#Finally we create the final return_point_list and return_point_source_list
for i in range(len(result_list)):
return_point_list.append(result_list[i][0].point.coords[0])
#if abs(result_list[i][0].point.coords[0][0]-91.7) < 0.2 and abs(result_list[i][0].point.coords[0][1]-106.15)< 0.2:
# print("GEFUNDEN")
if result_list[i][0].point_source == PointSource.HARD_EDGE_INTERNAL:
point_source = PointSource.HARD_EDGE
elif result_list[i][0].point_source == PointSource.SOFT_EDGE_INTERNAL:
point_source = PointSource.SOFT_EDGE
elif result_list[i][0].point_source == PointSource.REGULAR_SPACING_INTERNAL:
point_source = PointSource.REGULAR_SPACING
elif result_list[i][0].point_source == PointSource.FORBIDDEN_POINT:
point_source = PointSource.FORBIDDEN_POINT
else:
point_source = PointSource.PROJECTED_POINT
return_point_source_list.append(point_source)
assert(len(return_point_list) == len(return_point_source_list))
#return remove_dense_points(returnpointlist, returnpointsourcelist, maxstitch_distance,abs_offset)
return return_point_list, return_point_source_list
def replace_forbidden_points(line, result_list, forbidden_point_list_indices, abs_offset):
current_index_shift = 0 #since we add and remove points in the result_list, we need to adjust the indices stored in forbidden_point_list_indices
for index in forbidden_point_list_indices:
#if abs(result_list[index][0].point.coords[0][0]-40.7) < 0.2 and abs(result_list[index][0].point.coords[0][1]-1.3)< 0.2:
# print("GEFUNDEN")
index+=current_index_shift
distance_left = result_list[index][0].point.distance(result_list[index-1][0].point)/2.0
distance_right = result_list[index][0].point.distance(result_list[(index+1)%len(result_list)][0].point)/2.0
while distance_left > constants.point_spacing_to_be_considered_equal and distance_right > constants.point_spacing_to_be_considered_equal:
new_point_left_proj = result_list[index][1]-distance_left
if new_point_left_proj < 0:
new_point_left_proj += line.length
new_point_right_proj = result_list[index][1]+distance_right
if new_point_right_proj > line.length:
new_point_right_proj-=line.length
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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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))

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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',

@ -1 +1 @@
Subproject commit 235e5d044bc0913b2a01758935151e10d3e1db49
Subproject commit e6157caf813c6b60807749880c3419958d836928

Wyświetl plik

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Selection to guide line</name>
<id>org.inkstitch.selection_to_guide_line</id>
<param name="extension" type="string" gui-hidden="true">selection_to_guide_line</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch">
<submenu name="Edit" />
</submenu>
</effects-menu>
</effect>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>