Add "the tartan universe" (#2782)

pull/2805/head
Kaalleen 2024-03-29 10:25:02 +01:00 zatwierdzone przez GitHub
rodzic fb1ecd0bad
commit 2439adafa8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
26 zmienionych plików z 3262 dodań i 37 usunięć

Wyświetl plik

@ -22,7 +22,7 @@ from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length,
get_node_transform)
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
from ..utils import Point, cache
from ..utils.cache import get_stitch_plan_cache, CacheKeyGenerator
from ..utils.cache import CacheKeyGenerator, get_stitch_plan_cache
class Param(object):
@ -557,6 +557,9 @@ class EmbroideryElement(object):
gradient['styles'] = [(style['stop-color'], style['stop-opacity']) for style in self.gradient.stop_styles]
return gradient
def _get_tartan_key_data(self):
return (self.node.get('inkstitch:tartan', None))
def get_cache_key_data(self, previous_stitch):
return []
@ -572,6 +575,7 @@ class EmbroideryElement(object):
cache_key_generator.update(self._get_patterns_cache_key_data())
cache_key_generator.update(self._get_guides_cache_key_data())
cache_key_generator.update(self.get_cache_key_data(previous_stitch))
cache_key_generator.update(self._get_tartan_key_data())
cache_key = cache_key_generator.get_cache_key()
debug.log(f"cache key for {self.node.get('id')} {self.node.get(INKSCAPE_LABEL)} {previous_stitch}: {cache_key}")

Wyświetl plik

@ -18,11 +18,13 @@ from ..i18n import _
from ..marker import get_marker_elements
from ..stitch_plan import StitchGroup
from ..stitches import (auto_fill, circular_fill, contour_fill, guided_fill,
legacy_fill, linear_gradient_fill, meander_fill)
legacy_fill, linear_gradient_fill, meander_fill,
tartan_fill)
from ..stitches.linear_gradient_fill import gradient_angle
from ..svg import PIXELS_PER_MM, get_node_transform
from ..svg.clip import get_clip_path
from ..svg.tags import INKSCAPE_LABEL
from ..tartan.utils import get_tartan_settings, get_tartan_stripes
from ..utils import cache
from ..utils.geometry import ensure_multi_polygon
from ..utils.param import ParamOption
@ -112,6 +114,25 @@ class NoGradientWarning(ValidationWarning):
]
class NoTartanStripeWarning(ValidationWarning):
name = _("No stripes to render")
description = _("Tartan fill: There is no active fill stripe to render")
steps_to_solve = [
_('Go to Extensions > Ink/Stitch > Fill Tools > Tartan and adjust stripe settings:'),
_('* Check if stripes are active'),
_('* Check the minimum stripe width setting and the scale factor')
]
class DefaultTartanStripeWarning(ValidationWarning):
name = _("No customized pattern")
description = _("Tartan fill: Using default pattern")
steps_to_solve = [
_('Go to Extensions > Ink/Stitch > Fill Tools > Tartan and adjust stripe settings:'),
_('* Customize your pattern')
]
class InvalidShapeError(ValidationError):
name = _("This shape is invalid")
description = _('Fill: This shape cannot be stitched out. Please try to repair it with the "Break Apart Fill Objects" extension.')
@ -129,11 +150,12 @@ class FillStitch(EmbroideryElement):
return self.get_boolean_param('auto_fill', True)
_fill_methods = [ParamOption('auto_fill', _("Auto Fill")),
ParamOption('circular_fill', _("Circular Fill")),
ParamOption('contour_fill', _("Contour Fill")),
ParamOption('guided_fill', _("Guided Fill")),
ParamOption('meander_fill', _("Meander Fill")),
ParamOption('circular_fill', _("Circular Fill")),
ParamOption('linear_gradient_fill', _("Linear Gradient Fill")),
ParamOption('meander_fill', _("Meander Fill")),
ParamOption('tartan_fill', _("Tartan Fill")),
ParamOption('legacy_fill', _("Legacy Fill"))]
@property
@ -247,6 +269,7 @@ class FillStitch(EmbroideryElement):
('fill_method', 'guided_fill'),
('fill_method', 'meander_fill'),
('fill_method', 'circular_fill'),
('fill_method', 'tartan_fill'),
('fill_method', 'linear_gradient_fill')])
def expand(self):
return self.get_float_param('expand_mm', 0)
@ -264,6 +287,19 @@ class FillStitch(EmbroideryElement):
def angle(self):
return math.radians(self.get_float_param('angle', 0))
@property
@param('tartan_angle',
_('Angle of lines of stitches'),
tooltip=_('Relative to the tartan stripe direction.'),
unit='deg',
type='float',
sort_index=21,
select_items=[('fill_method', 'tartan_fill')],
default=45)
@cache
def tartan_angle(self):
return self.get_float_param('tartan_angle', -45)
@property
@param('max_stitch_length_mm',
_('Maximum fill stitch length'),
@ -276,6 +312,7 @@ class FillStitch(EmbroideryElement):
('fill_method', 'contour_fill'),
('fill_method', 'guided_fill'),
('fill_method', 'linear_gradient_fill'),
('fill_method', 'tartan_fill'),
('fill_method', 'legacy_fill')],
default=3.0)
def max_stitch_length(self):
@ -293,6 +330,7 @@ class FillStitch(EmbroideryElement):
('fill_method', 'guided_fill'),
('fill_method', 'circular_fill'),
('fill_method', 'linear_gradient_fill'),
('fill_method', 'tartan_fill'),
('fill_method', 'legacy_fill')],
default=0.25)
def row_spacing(self):
@ -323,6 +361,7 @@ class FillStitch(EmbroideryElement):
select_items=[('fill_method', 'auto_fill'),
('fill_method', 'guided_fill'),
('fill_method', 'linear_gradient_fill'),
('fill_method', 'tartan_fill'),
('fill_method', 'legacy_fill')],
default=4)
def staggers(self):
@ -357,6 +396,18 @@ class FillStitch(EmbroideryElement):
def flip(self):
return self.get_boolean_param("flip", False)
@property
@param(
'reverse',
_('Reverse fill'),
tooltip=_('Reverses fill path.'),
type='boolean',
sort_index=28,
select_items=[('fill_method', 'legacy_fill')],
default=False)
def reverse(self):
return self.get_boolean_param("reverse", False)
@property
@param(
'stop_at_ending_point',
@ -396,7 +447,8 @@ class FillStitch(EmbroideryElement):
('fill_method', 'guided_fill'),
('fill_method', 'meander_fill'),
('fill_method', 'circular_fill'),
('fill_method', 'linear_gradient_fill')],
('fill_method', 'linear_gradient_fill'),
('fill_method', 'tartan_fill')],
sort_index=31)
def running_stitch_length(self):
return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01)
@ -434,7 +486,8 @@ class FillStitch(EmbroideryElement):
'A pattern with various repeats can be created with a list of values separated by a space.'),
type='str',
select_items=[('fill_method', 'meander_fill'),
('fill_method', 'circular_fill')],
('fill_method', 'circular_fill'),
('fill_method', 'tartan_fill')],
default=0,
sort_index=34)
def bean_stitch_repeats(self):
@ -466,6 +519,31 @@ class FillStitch(EmbroideryElement):
def zigzag_width(self):
return self.get_float_param("zigzag_width_mm", 3)
@property
@param(
'rows_per_thread',
_("Rows per tartan thread"),
tooltip=_("Consecutive rows of the same color"),
type='int',
default="2",
select_items=[('fill_method', 'tartan_fill')],
sort_index=35
)
def rows_per_thread(self):
return max(1, self.get_int_param("rows_per_thread", 2))
@property
@param('herringbone_width_mm',
_('Herringbone width'),
tooltip=_('Defines width of a herringbone pattern. Use 0 for regular rows.'),
unit='mm',
type='int',
default=0,
select_items=[('fill_method', 'tartan_fill')],
sort_index=36)
def herringbone_width(self):
return self.get_float_param('herringbone_width_mm', 0)
@property
def color(self):
# SVG spec says the default fill is black
@ -679,6 +757,15 @@ class FillStitch(EmbroideryElement):
# they may used a fill on a straight line
yield StrokeAndFillWarning(self.paths[0][0])
# tartan fill
if self.fill_method == 'tartan_fill':
settings = get_tartan_settings(self.node)
warp, weft = get_tartan_stripes(settings)
if not (warp or weft):
yield NoTartanStripeWarning(self.shape.representative_point())
if not self.node.get('inkstitch:tartan', ''):
yield DefaultTartanStripeWarning(self.shape.representative_point())
for warning in super(FillStitch, self).validation_warnings():
yield warning
@ -778,19 +865,38 @@ class FillStitch(EmbroideryElement):
stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end))
elif self.fill_method == 'linear_gradient_fill':
stitch_groups.extend(self.do_linear_gradient_fill(fill_shape, previous_stitch_group, start, end))
elif self.fill_method == 'tartan_fill':
stitch_groups.extend(self.do_tartan_fill(fill_shape, previous_stitch_group, start, end))
else:
# auto_fill
stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end))
if stitch_groups:
previous_stitch_group = stitch_groups[-1]
# sort colors of linear gradient (if multiple shapes)
if self.fill_method == 'linear_gradient_fill':
colors = [stitch_group.color for stitch_group in stitch_groups]
stitch_groups.sort(key=lambda group: colors.index(group.color))
# sort colors of linear gradient
if len(shapes) > 1 and self.fill_method == 'linear_gradient_fill':
self.color_sort(stitch_groups)
# sort colors of tartan fill
if len(shapes) > 1 and self.fill_method == 'tartan_fill':
# while color sorting make sure stroke lines go still on top of the fills
fill_groups = []
stroke_groups = []
for stitch_group in stitch_groups:
if "tartan_run" in stitch_group.stitches[0].tags:
stroke_groups.append(stitch_group)
else:
fill_groups.append(stitch_group)
self.color_sort(fill_groups)
self.color_sort(stroke_groups)
stitch_groups = fill_groups + stroke_groups
return stitch_groups
def color_sort(self, stitch_groups):
colors = [stitch_group.color for stitch_group in stitch_groups]
stitch_groups.sort(key=lambda group: colors.index(group.color))
def do_legacy_fill(self):
stitch_lists = legacy_fill(
self.shape,
@ -799,6 +905,7 @@ class FillStitch(EmbroideryElement):
self.end_row_spacing,
self.max_stitch_length,
self.flip,
self.reverse,
self.staggers,
self.skip_last
)
@ -996,3 +1103,6 @@ class FillStitch(EmbroideryElement):
def do_linear_gradient_fill(self, shape, last_stitch_group, start, end):
return linear_gradient_fill(self, shape, start, end)
def do_tartan_fill(self, shape, last_stitch_group, start, end):
return tartan_fill(self, shape, start, end)

Wyświetl plik

@ -54,6 +54,7 @@ from .simulator import Simulator
from .stitch_plan_preview import StitchPlanPreview
from .stitch_plan_preview_undo import StitchPlanPreviewUndo
from .stroke_to_lpe_satin import StrokeToLpeSatin
from .tartan import Tartan
from .test_swatches import TestSwatches
from .troubleshoot import Troubleshoot
from .update_svg import UpdateSvg
@ -111,6 +112,7 @@ __all__ = extensions = [ApplyPalette,
StitchPlanPreview,
StitchPlanPreviewUndo,
StrokeToLpeSatin,
Tartan,
TestSwatches,
Troubleshoot,
UpdateSvg,

Wyświetl plik

@ -0,0 +1,78 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import sys
import wx
import wx.adv
from inkex import errormsg
from ..gui.simulator import SplitSimulatorWindow
from ..gui.tartan import TartanMainPanel
from ..i18n import _
from ..svg.tags import EMBROIDERABLE_TAGS, INKSTITCH_TARTAN, SVG_GROUP_TAG
from .base import InkstitchExtension
class Tartan(InkstitchExtension):
def __init__(self, *args, **kwargs):
self.elements = set()
self.cancelled = False
InkstitchExtension.__init__(self, *args, **kwargs)
def cancel(self):
self.cancelled = True
def get_tartan_elements(self):
if self.svg.selection:
self._get_elements()
def _get_elements(self):
for node in self.svg.selection:
node = self.get_outline(node)
if node.style('fill'):
self.elements.add(node)
def get_outline(self, node):
# existing tartans are marked through their outline element
# we have either selected the element itself or some other element within a tartan group
if node.get(INKSTITCH_TARTAN, None):
return node
if node.get_id().startswith('inkstitch-tartan'):
for element in node.iterchildren(EMBROIDERABLE_TAGS):
if element.get(INKSTITCH_TARTAN, None):
return element
for group in node.iterancestors(SVG_GROUP_TAG):
if group.get_id().startswith('inkstitch-tartan'):
for element in group.iterchildren(EMBROIDERABLE_TAGS):
if element.get(INKSTITCH_TARTAN, None):
return element
# if we don't find an existing tartan, return node
return node
def effect(self):
self.get_tartan_elements()
if not self.elements:
errormsg(_("To create a tartan pattern please select at least one element with a fill color."))
return
metadata = self.get_inkstitch_metadata()
app = wx.App()
frame = SplitSimulatorWindow(
title=_("Ink/Stitch Tartan"),
panel_class=TartanMainPanel,
elements=list(self.elements),
on_cancel=self.cancel,
metadata=metadata,
target_duration=1
)
frame.Show()
app.MainLoop()
if self.cancelled:
# This prevents the superclass from outputting the SVG, because we
# may have modified the DOM.
sys.exit(0)

Wyświetl plik

@ -0,0 +1,10 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from .code_panel import CodePanel
from .customize_panel import CustomizePanel
from .embroidery_panel import EmbroideryPanel
from .help_panel import HelpPanel
from .main_panel import TartanMainPanel

Wyświetl plik

@ -0,0 +1,59 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import wx
import wx.adv
from ...i18n import _
class CodePanel(wx.Panel):
def __init__(self, parent, panel):
self.panel = panel
wx.Panel.__init__(self, parent)
code_sizer = wx.BoxSizer(wx.VERTICAL)
load_palette_sizer = wx.BoxSizer(wx.HORIZONTAL)
tt_unit_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.threadcount_text = wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.threadcount_text.Bind(wx.EVT_TEXT, self.set_tt_unit_status)
code_sizer.Add(self.threadcount_text, 1, wx.EXPAND | wx.ALL, 10)
self.tt_unit_label = wx.StaticText(self, label=_("1 Tartan thread equals (mm)"))
self.tt_unit_spin = wx.SpinCtrlDouble(self, min=0.01, max=50, initial=0.2, inc=0.1, style=wx.SP_WRAP)
self.tt_unit_spin.SetDigits(2)
tt_unit_sizer.Add(self.tt_unit_label, 0, wx.CENTER | wx.ALL, 10)
tt_unit_sizer.Add(self.tt_unit_spin, 0, wx.ALL, 10)
self.tt_unit_label.SetToolTip(_("Used only for Threadcount code (The Scottish Register of Tartans)"))
self.tt_unit_spin.SetToolTip(_("Used only for Threadcount code (The Scottish Register of Tartans)"))
code_sizer.Add(tt_unit_sizer, 0, wx.ALL, 10)
load_button = wx.Button(self, label="Apply Code")
load_button.Bind(wx.EVT_BUTTON, self._load_palette_code)
load_palette_sizer.Add(load_button, 0, wx.ALL, 10)
code_sizer.Add(load_palette_sizer, 0, wx.ALL, 10)
self.SetSizer(code_sizer)
def _load_palette_code(self, event):
self.panel.palette.tt_unit = self.tt_unit_spin.GetValue()
self.panel.update_from_code()
self.panel.settings['palette'] = self.threadcount_text.GetValue()
def set_tt_unit_status(self, event):
# we always want to convert the width into mm
# when threadcount code is given we have to enable the threadcount unit field
# so they can define the mm-width of one tartan thread
threadcount_text = self.threadcount_text.GetValue()
if '(' in threadcount_text and 'Threadcount' not in threadcount_text:
# depending on how much of the mailed text is copied into the code field,
# we may have brackets in there (1997). So let's also check for "threadcount"
self.tt_unit_label.Enable(False)
self.tt_unit_spin.Enable(False)
else:
self.tt_unit_label.Enable(True)
self.tt_unit_spin.Enable(True)

Wyświetl plik

@ -0,0 +1,300 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from math import floor
import wx
from wx.lib.scrolledpanel import ScrolledPanel
from ...i18n import _
class CustomizePanel(ScrolledPanel):
def __init__(self, parent, panel):
self.panel = panel
self.mouse_position = None
ScrolledPanel.__init__(self, parent)
self.customize_sizer = wx.BoxSizer(wx.VERTICAL)
general_settings_sizer = wx.BoxSizer(wx.HORIZONTAL)
positional_settings_sizer = wx.FlexGridSizer(2, 4, 5, 5)
stripe_header_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.stripe_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.warp_outer_sizer = wx.BoxSizer(wx.VERTICAL)
self.weft_outer_sizer = wx.BoxSizer(wx.VERTICAL)
self.warp_sizer = wx.BoxSizer(wx.VERTICAL)
self.weft_sizer = wx.BoxSizer(wx.VERTICAL)
general_settings_headline = wx.StaticText(self, label=_("Pattern Settings"))
general_settings_headline.SetFont(wx.Font().Bold())
self.symmetry_checkbox = wx.CheckBox(self, label=_("Symmetrical / reflective sett"))
self.symmetry_checkbox.SetToolTip(_("Disabled: asymmetrical / repeating sett"))
self.symmetry_checkbox.Bind(wx.EVT_CHECKBOX, self.update_symmetry)
self.warp_weft_checkbox = wx.CheckBox(self, label=_("Equal threadcount for warp and weft"))
self.warp_weft_checkbox.Bind(wx.EVT_CHECKBOX, self._update_warp_weft_event)
positional_settings_headline = wx.StaticText(self, label=_("Position"))
positional_settings_headline.SetFont(wx.Font().Bold())
self.rotate = wx.SpinCtrlDouble(self, min=-180, max=180, initial=0, inc=0.1, style=wx.SP_WRAP)
self.rotate.SetDigits(2)
self.rotate.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("rotate", event))
rotate_label = wx.StaticText(self, label=_("Rotate"))
self.scale = wx.SpinCtrl(self, min=0, max=1000, initial=100, style=wx.SP_WRAP)
self.scale.Bind(wx.EVT_SPINCTRL, self.update_scale)
scale_label = wx.StaticText(self, label=_("Scale (%)"))
self.offset_x = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
self.offset_x.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("offset_x", event))
self.offset_x.SetDigits(2)
offset_x_label = wx.StaticText(self, label=_("Offset X (mm)"))
self.offset_y = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
self.offset_y.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("offset_y", event))
self.offset_y.SetDigits(2)
offset_y_label = wx.StaticText(self, label=_("Offset Y (mm)"))
stripe_settings_headline = wx.StaticText(self, label=_("Stripes"))
stripe_settings_headline.SetFont(wx.Font().Bold())
self.link_colors_checkbox = wx.CheckBox(self, label=_("Link colors"))
self.link_colors_checkbox.SetToolTip(_("When enabled update all equal colors simultaneously."))
self.warp_headline = wx.StaticText(self, label=_("Warp"))
self.warp_headline.SetFont(wx.Font().Bold())
self.weft_headline = wx.StaticText(self, label=_("Weft"))
self.weft_headline.SetFont(wx.Font().Bold())
self.add_warp_button = wx.Button(self, label=_("Add"))
self.add_warp_button.Bind(wx.EVT_BUTTON, self._add_warp_event)
self.add_weft_button = wx.Button(self, label=_("Add"))
self.add_weft_button.Bind(wx.EVT_BUTTON, self._add_weft_event)
# Add to sizers
general_settings_sizer.Add(self.symmetry_checkbox, 0, wx.CENTER | wx.ALL, 10)
general_settings_sizer.Add(self.warp_weft_checkbox, 0, wx.CENTER | wx.ALL, 10)
positional_settings_sizer.Add(rotate_label, 0, wx.ALIGN_CENTRE, 0)
positional_settings_sizer.Add(self.rotate, 0, wx.EXPAND | wx.RIGHT, 30)
positional_settings_sizer.Add(offset_x_label, 0, wx.ALIGN_CENTRE, 0)
positional_settings_sizer.Add(self.offset_x, 0, wx.EXPAND, 0)
positional_settings_sizer.Add(scale_label, 0, wx.ALIGN_CENTRE, 0)
positional_settings_sizer.Add(self.scale, 0, wx.EXPAND | wx.RIGHT, 30)
positional_settings_sizer.Add(offset_y_label, 0, wx.ALIGN_CENTRE, 0)
positional_settings_sizer.Add(self.offset_y, 0, wx.EXPAND, 0)
positional_settings_sizer.AddGrowableCol(1)
positional_settings_sizer.AddGrowableCol(3)
self.warp_outer_sizer.Add(self.warp_headline, 0, wx.EXPAND, 0)
self.weft_outer_sizer.Add(self.weft_headline, 0, wx.EXPAND, 0)
self.warp_outer_sizer.Add(self.warp_sizer, 0, wx.EXPAND, 0)
self.weft_outer_sizer.Add(self.weft_sizer, 0, wx.EXPAND, 0)
self.warp_outer_sizer.Add(self.add_warp_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.weft_outer_sizer.Add(self.add_weft_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.stripe_sizer.Add(self.warp_outer_sizer, 1, wx.EXPAND, 0)
self.stripe_sizer.Add(self.weft_outer_sizer, 1, wx.EXPAND, 0)
stripe_header_sizer.Add(stripe_settings_headline, 0, wx.ALL, 10)
stripe_header_sizer.Add((0, 0), 1, wx.ALL | wx.EXPAND, 10)
stripe_header_sizer.Add(self.link_colors_checkbox, 0, wx.ALL, 10)
self.customize_sizer.Add(positional_settings_headline, 0, wx.ALL, 10)
self.customize_sizer.Add(positional_settings_sizer, 0, wx.ALL | wx.EXPAND, 10)
self.customize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.customize_sizer.Add(general_settings_headline, 0, wx.ALL, 10)
self.customize_sizer.Add(general_settings_sizer, 0, wx.ALL | wx.EXPAND, 10)
self.customize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.customize_sizer.Add(stripe_header_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.customize_sizer.Add(self.stripe_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.SetSizer(self.customize_sizer)
def _add_warp_event(self, event):
self.add_stripe()
def _add_weft_event(self, event):
self.add_stripe(False)
def add_stripe(self, warp=True, stripe=None, update=True):
stripesizer = wx.BoxSizer(wx.HORIZONTAL)
position = wx.Button(self, label='', style=wx.BU_EXACTFIT)
position.SetToolTip(_("Drag and drop to adjust position."))
position.Bind(wx.EVT_LEFT_DOWN, self._move_stripe_start)
position.Bind(wx.EVT_LEFT_UP, self._move_stripe_end)
visibility = wx.CheckBox(self)
visibility.SetToolTip(_("Stitch this stripe"))
visibility.SetValue(True)
visibility.Bind(wx.EVT_CHECKBOX, self._update_stripes_event)
# hidden label used for linked colors
# there seems to be no native way to catch the old color setting
colorinfo = wx.StaticText(self, label='black')
colorinfo.Hide()
colorpicker = wx.ColourPickerCtrl(self, colour=wx.Colour('black'))
colorpicker.SetToolTip(_("Select stripe color"))
colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self._update_color)
stripe_width = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=5, style=wx.SP_WRAP)
stripe_width.SetDigits(2)
stripe_width.SetToolTip(_("Set stripe width (mm)"))
stripe_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._update_stripes_event)
remove_button = wx.Button(self, label='X')
remove_button.SetToolTip(_("Remove stripe"))
remove_button.Bind(wx.EVT_BUTTON, self._remove_stripe)
stripesizer.Add(position, 0, wx.CENTER | wx.RIGHT | wx.TOP, 5)
stripesizer.Add(visibility, 0, wx.CENTER | wx.RIGHT | wx.TOP, 5)
stripesizer.Add(colorinfo, 0, wx.RIGHT | wx.TOP, 5)
stripesizer.Add(colorpicker, 0, wx.RIGHT | wx.TOP, 5)
stripesizer.Add(stripe_width, 1, wx.RIGHT | wx.TOP, 5)
stripesizer.Add(remove_button, 0, wx.CENTER | wx.TOP, 5)
if stripe is not None:
visibility.SetValue(stripe['render'])
colorinfo.SetLabel(wx.Colour(stripe['color']).GetAsString(wx.C2S_HTML_SYNTAX))
colorpicker.SetColour(wx.Colour(stripe['color']))
stripe_width.SetValue(stripe['width'])
if warp:
self.warp_sizer.Add(stripesizer, 0, wx.EXPAND | wx.ALL, 5)
else:
self.weft_sizer.Add(stripesizer, 0, wx.EXPAND | wx.ALL, 5)
if update:
self.panel.update_from_stripes()
self.set_stripe_width_color(stripe_width)
self.FitInside()
def _move_stripe_start(self, event):
self.mouse_position = wx.GetMousePosition()
def _move_stripe_end(self, event):
stripe = event.GetEventObject()
sizer = stripe.GetContainingSizer()
if self.warp_sizer.GetItem(sizer):
main_sizer = self.warp_sizer
else:
main_sizer = self.weft_sizer
for i, item in enumerate(main_sizer.GetChildren()):
if item.GetSizer() == sizer:
index = i
break
position = wx.GetMousePosition()
sizer_height = sizer.GetSize()[1] + 10
move = floor((position[1] - self.mouse_position[1]) / sizer_height)
index = min(len(main_sizer.Children) - 1, max(0, (index + move)))
main_sizer.Detach(sizer)
main_sizer.Insert(index, sizer, 0, wx.EXPAND | wx.ALL, 5)
self.panel.update_from_stripes()
self.FitInside()
def _remove_stripe(self, event):
sizer = event.GetEventObject().GetContainingSizer()
sizer.Clear(True)
self.warp_sizer.Remove(sizer)
try:
self.weft_sizer.Remove(sizer)
except RuntimeError:
# we may have removed it already
pass
self.panel.update_from_stripes()
self.FitInside()
def on_change(self, attribute, event):
self.panel.settings[attribute] = event.EventObject.GetValue()
self.panel.update_preview()
def update_scale(self, event):
self.panel.settings['scale'] = event.EventObject.GetValue()
# self.update_stripes(self.panel.palette.palette_stripes)
self.update_stripe_width_colors()
self.panel.update_preview()
def _update_stripes_event(self, event):
self.set_stripe_width_color(event.EventObject)
self.panel.update_from_stripes()
def update_stripe_width_colors(self):
for sizer in [self.warp_sizer, self.weft_sizer]:
for stripe_sizer in sizer.GetChildren():
inner_sizer = stripe_sizer.GetSizer()
for stripe_widget in inner_sizer:
widget = stripe_widget.GetWindow()
if isinstance(widget, wx.SpinCtrlDouble):
self.set_stripe_width_color(widget)
def set_stripe_width_color(self, stripe_width_ctrl):
scale = self.scale.GetValue()
min_stripe_width = self.panel.embroidery_panel.min_stripe_width.GetValue()
stripe_width = stripe_width_ctrl.GetValue() * scale / 100
if stripe_width <= min_stripe_width:
stripe_width_ctrl.SetBackgroundColour(wx.Colour('#efefef'))
stripe_width_ctrl.SetForegroundColour('black')
else:
stripe_width_ctrl.SetBackgroundColour(wx.NullColour)
stripe_width_ctrl.SetForegroundColour(wx.NullColour)
def update_stripes(self, stripes):
self.warp_sizer.Clear(True)
self.weft_sizer.Clear(True)
warp = True
for direction in stripes:
for stripe in direction:
self.add_stripe(warp, stripe, False)
warp = False
self.panel.update_from_stripes()
def _update_color(self, event):
linked = self.link_colors_checkbox.GetValue()
widget = event.GetEventObject()
colorinfo = widget.GetPrevSibling()
old_color = wx.Colour(colorinfo.GetLabel())
new_color = event.Colour
if linked:
self._update_color_picker(old_color, new_color, self.warp_sizer)
self._update_color_picker(old_color, new_color, self.weft_sizer)
colorinfo.SetLabel(new_color.GetAsString(wx.C2S_HTML_SYNTAX))
self.panel.update_from_stripes()
def _update_color_picker(self, old_color, new_color, sizer):
for stripe_sizer in sizer.Children:
stripe_info = stripe_sizer.GetSizer()
for widget in stripe_info.GetChildren():
widget = widget.GetWindow()
if isinstance(widget, wx.ColourPickerCtrl):
color = widget.GetColour()
if color == old_color:
widget.SetColour(new_color)
widget.GetPrevSibling().SetLabel(new_color.GetAsString(wx.C2S_HTML_SYNTAX))
def update_symmetry(self, event=None):
symmetry = self.symmetry_checkbox.GetValue()
self.panel.settings['symmetry'] = symmetry
self.panel.palette.update_symmetry(symmetry)
self.panel.update_from_stripes()
self.FitInside()
def update_warp_weft(self):
equal_warp_weft = self.warp_weft_checkbox.GetValue()
if equal_warp_weft:
self.stripe_sizer.Hide(self.warp_headline, recursive=True)
self.stripe_sizer.Hide(self.weft_outer_sizer, recursive=True)
else:
self.stripe_sizer.Show(self.warp_headline, recursive=True)
self.stripe_sizer.Show(self.weft_outer_sizer, recursive=True)
# We just made the weft colorinfo visible. Let's hide it again.
self._hide_colorinfo()
self.FitInside()
def _update_warp_weft_event(self, event):
self.panel.settings['equal_warp_weft'] = event.GetEventObject().GetValue()
self.update_warp_weft()
self.panel.update_from_stripes()
def _hide_colorinfo(self):
for stripe_sizer in self.weft_sizer.Children:
stripe_info = stripe_sizer.GetSizer()
for stripe in stripe_info.GetChildren():
widget = stripe.GetWindow()
if isinstance(widget, wx.StaticText):
widget.Hide()

Wyświetl plik

@ -0,0 +1,201 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import wx
from ...i18n import _
from ...utils.param import ParamOption
class EmbroideryPanel(wx.Panel):
def __init__(self, parent, panel):
self.panel = panel
wx.Panel.__init__(self, parent)
self.embroidery_sizer = wx.BoxSizer(wx.VERTICAL)
self.embroidery_element_sizer = wx.FlexGridSizer(6, 2, 5, 5)
self.embroidery_element_sizer.AddGrowableCol(1)
self.svg_elements_sizer = wx.FlexGridSizer(6, 2, 5, 5)
self.svg_elements_sizer.AddGrowableCol(1)
self.common_settings_sizer = wx.FlexGridSizer(1, 2, 5, 5)
self.common_settings_sizer.AddGrowableCol(1)
help_text = wx.StaticText(self, -1, _("Embroidery settings can be refined in the params dialog."))
# Method
self.output_method = wx.ComboBox(self, choices=[], style=wx.CB_READONLY)
for choice in embroider_choices:
self.output_method.Append(choice.name, choice)
self.output_method.SetSelection(0)
self.output_method.Bind(wx.EVT_COMBOBOX, self.update_output_method)
# Embroidery Element Params
stitch_angle_label = wx.StaticText(self, label=_("Angle of lines of stitches"))
stitch_angle_label.SetToolTip(_('Relative to the tartan stripe direction.'))
self.stitch_angle = wx.SpinCtrlDouble(self, min=-90, max=90, initial=-45, style=wx.SP_WRAP)
self.stitch_angle.SetDigits(2)
self.stitch_angle.SetIncrement(1)
self.stitch_angle.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("tartan_angle", event))
rows_per_thread_label = wx.StaticText(self, label=_("Rows per tartan thread"))
self.rows_per_thread = wx.SpinCtrl(self, min=1, max=50, initial=2, style=wx.SP_WRAP)
lines_text = _("Consecutive rows of the same color")
rows_per_thread_label.SetToolTip(lines_text)
self.rows_per_thread.SetToolTip(lines_text)
self.rows_per_thread.Bind(wx.EVT_SPINCTRL, lambda event: self.on_param_change("rows_per_thread", event))
row_spacing_label = wx.StaticText(self, label=_("Row spacing (mm)"))
self.row_spacing = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=0.25, style=wx.SP_WRAP)
self.row_spacing.SetDigits(2)
self.row_spacing.SetIncrement(0.01)
self.row_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("row_spacing_mm", event))
underlay_label = wx.StaticText(self, label=_("Underlay"))
self.underlay = wx.CheckBox(self)
self.underlay.Bind(wx.EVT_CHECKBOX, lambda event: self.on_param_change("fill_underlay", event))
herringbone_label = wx.StaticText(self, label=_("Herringbone width (mm)"))
self.herringbone = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
self.herringbone.SetDigits(2)
self.herringbone.SetIncrement(1)
self.herringbone.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("herringbone_width_mm", event))
bean_stitch_repeats_label = wx.StaticText(self, label=_("Bean stitch repeats"))
self.bean_stitch_repeats = wx.TextCtrl(self)
self.bean_stitch_repeats.Bind(wx.EVT_TEXT, lambda event: self.on_param_change("bean_stitch_repeats", event))
# SVG Output Settings
stitch_type_label = wx.StaticText(self, label=_("Stitch type"))
self.stitch_type = wx.ComboBox(self, choices=[], style=wx.CB_READONLY)
for choice in stitch_type_choices:
self.stitch_type.Append(choice.name, choice)
self.stitch_type.SetSelection(0)
self.stitch_type.Bind(wx.EVT_COMBOBOX, self.on_change_stitch_type)
svg_row_spacing_label = wx.StaticText(self, label=_("Row spacing"))
self.svg_row_spacing = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=1, style=wx.SP_WRAP)
self.svg_row_spacing.SetDigits(2)
self.row_spacing.SetIncrement(0.01)
self.svg_row_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("row_spacing", event))
angle_warp_label = wx.StaticText(self, label=_("Stitch angle (warp)"))
self.angle_warp = wx.SpinCtrl(self, min=-90, max=90, initial=0, style=wx.SP_WRAP)
self.angle_warp.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("angle_warp", event))
angle_weft_label = wx.StaticText(self, label=_("Stitch angle (weft)"))
self.angle_weft = wx.SpinCtrl(self, min=-90, max=90, initial=90, style=wx.SP_WRAP)
self.angle_weft.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("angle_weft", event))
min_stripe_width_label = wx.StaticText(self, label=_("Minimum stripe width for fills"))
self.min_stripe_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=1, style=wx.SP_WRAP)
self.min_stripe_width.SetDigits(2)
self.row_spacing.SetIncrement(0.1)
min_width_text = _("Stripes smaller than this will be stitched as a running stitch")
min_stripe_width_label.SetToolTip(min_width_text)
self.min_stripe_width.SetToolTip(min_width_text)
self.min_stripe_width.Bind(wx.EVT_SPINCTRLDOUBLE, self.on_change_min_stripe_width)
svg_bean_stitch_repeats_label = wx.StaticText(self, label=_("Bean stitch repeats"))
self.svg_bean_stitch_repeats = wx.SpinCtrl(self, min=0, max=10, initial=0, style=wx.SP_WRAP)
self.svg_bean_stitch_repeats.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("bean_stitch_repeats", event))
# Add to sizers
self.embroidery_element_sizer.Add(stitch_angle_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.stitch_angle, 0, wx.EXPAND, 0)
self.embroidery_element_sizer.Add(rows_per_thread_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.rows_per_thread, 0, wx.EXPAND, 0)
self.embroidery_element_sizer.Add(row_spacing_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.row_spacing, 0, wx.EXPAND, 0)
self.embroidery_element_sizer.Add(herringbone_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.herringbone, 0, wx.EXPAND, 0)
self.embroidery_element_sizer.Add(underlay_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.underlay, 0, wx.EXPAND, 0)
self.embroidery_element_sizer.Add(bean_stitch_repeats_label, 0, wx.ALIGN_CENTRE, 0)
self.embroidery_element_sizer.Add(self.bean_stitch_repeats, 0, wx.EXPAND, 0)
self.svg_elements_sizer.Add(stitch_type_label, 0, wx.ALIGN_CENTRE, 0)
self.svg_elements_sizer.Add(self.stitch_type, 0, wx.EXPAND, 0)
self.svg_elements_sizer.Add(svg_row_spacing_label, 0, wx.ALIGN_CENTRE, 0)
self.svg_elements_sizer.Add(self.svg_row_spacing, 0, wx.EXPAND, 0)
self.svg_elements_sizer.Add(angle_warp_label, 0, wx.ALIGN_CENTRE, 0)
self.svg_elements_sizer.Add(self.angle_warp, 0, wx.EXPAND, 0)
self.svg_elements_sizer.Add(angle_weft_label, 0, wx.ALIGN_CENTRE, 0)
self.svg_elements_sizer.Add(self.angle_weft, 0, wx.EXPAND, 0)
self.svg_elements_sizer.Add(svg_bean_stitch_repeats_label, 0, wx.ALIGN_CENTRE, 0)
self.svg_elements_sizer.Add(self.svg_bean_stitch_repeats, 0, wx.EXPAND, 0)
self.common_settings_sizer.Add(min_stripe_width_label, 0, wx.ALIGN_CENTRE, 0)
self.common_settings_sizer.Add(self.min_stripe_width, 0, wx.EXPAND, 0)
self.embroidery_sizer.Add(self.output_method, 0, wx.EXPAND | wx.ALL, 10)
self.embroidery_sizer.Add(self.embroidery_element_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.embroidery_sizer.Add(self.svg_elements_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.embroidery_sizer.Add(self.common_settings_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.embroidery_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.embroidery_sizer.Add(help_text, 0, wx.EXPAND | wx.ALL, 10)
self.embroidery_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.embroidery_sizer.Hide(self.svg_elements_sizer)
self.SetSizer(self.embroidery_sizer)
def update_output_method(self, event):
output = self.output_method.GetClientData(self.output_method.GetSelection()).id
if output == "svg":
self.embroidery_sizer.Show(self.svg_elements_sizer)
self.embroidery_sizer.Hide(self.embroidery_element_sizer)
for element in self.panel.elements:
element.pop('inkstitch:fill_method')
else:
self.embroidery_sizer.Show(self.embroidery_element_sizer)
self.embroidery_sizer.Hide(self.svg_elements_sizer)
for element in self.panel.elements:
element.set('inkstitch:fill_method', 'tartan_fill')
self.panel.settings['output'] = output
self.Layout()
self.panel.update_preview()
def set_output(self, choice):
for option in embroider_choices:
if option.id == choice:
self.output_method.SetValue(option.name)
self.update_output_method(None)
break
def on_change(self, attribute, event):
self.panel.settings[attribute] = event.GetEventObject().GetValue()
self.panel.update_preview()
def on_change_stitch_type(self, event):
stitch_type = self.stitch_type.GetClientData(self.stitch_type.GetSelection()).id
self.panel.settings['stitch_type'] = stitch_type
self.panel.update_preview()
def on_change_min_stripe_width(self, event):
self.panel.settings['min_stripe_width'] = event.EventObject.GetValue()
self.panel.customize_panel.update_stripe_width_colors()
self.panel.update_preview()
def on_param_change(self, attribute, event):
for element in self.panel.elements:
element.set(f'inkstitch:{attribute}', str(event.GetEventObject().GetValue()))
self.panel.update_preview()
def set_stitch_type(self, choice):
for option in stitch_type_choices:
if option.id == choice:
self.stitch_type.SetValue(option.name)
break
embroider_choices = [
ParamOption("embroidery", _("Embroidery Element")),
ParamOption("svg", _("SVG Elements"))
]
stitch_type_choices = [
ParamOption("auto_fill", _("AutoFill")),
ParamOption("legacy_fill", _("Legacy Fill"))
]

Wyświetl plik

@ -0,0 +1,42 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import wx
from ...i18n import _
class HelpPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
help_sizer = wx.BoxSizer(wx.VERTICAL)
help_text = wx.StaticText(
self,
wx.ID_ANY,
_("This extension fills shapes with a tartan (or tartan like) pattern."),
style=wx.ALIGN_LEFT
)
help_text.Wrap(500)
help_sizer.Add(help_text, 0, wx.ALL, 8)
help_sizer.Add((20, 20), 0, 0, 0)
website_info = wx.StaticText(self, wx.ID_ANY, _("More information on our website:"))
help_sizer.Add(website_info, 0, wx.ALL, 8)
website_link = wx.adv.HyperlinkCtrl(
self,
wx.ID_ANY,
_("https://inkstitch.org/docs/stitches/tartan-fill"),
_("https://inkstitch.org/docs/stitches/tartan-fill")
)
website_link.Bind(wx.adv.EVT_HYPERLINK, self.on_link_clicked)
help_sizer.Add(website_link, 0, wx.ALL, 8)
self.SetSizer(help_sizer)
def on_link_clicked(self, event):
event.Skip()

Wyświetl plik

@ -0,0 +1,271 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import json
from copy import copy
import inkex
import wx
import wx.adv
from ...elements import FillStitch, nodes_to_elements
from ...exceptions import InkstitchException, format_uncaught_exception
from ...i18n import _
from ...stitch_plan import stitch_groups_to_stitch_plan
from ...svg.tags import INKSTITCH_TARTAN
from ...tartan.fill_element import prepare_tartan_fill_element
from ...tartan.palette import Palette
from ...tartan.svg import TartanSvgGroup
from ...utils import DotDict
from ...utils.threading import ExitThread, check_stop_flag
from .. import PresetsPanel, PreviewRenderer, WarningPanel
from . import CodePanel, CustomizePanel, EmbroideryPanel, HelpPanel
class TartanMainPanel(wx.Panel):
def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, output_groups=inkex.Group()):
self.parent = parent
self.simulator = simulator
self.elements = elements
self.cancel_hook = on_cancel
self.palette = Palette()
self.metadata = metadata or dict()
self.output_groups = output_groups
super().__init__(parent, wx.ID_ANY)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
# preview
self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
self.presets_panel = PresetsPanel(self)
# warnings
self.warning_panel = WarningPanel(self)
self.warning_panel.Hide()
# notebook
self.notebook_sizer = wx.BoxSizer(wx.VERTICAL)
self.notebook = wx.Notebook(self, wx.ID_ANY)
self.notebook_sizer.Add(self.warning_panel, 0, wx.EXPAND | wx.ALL, 10)
self.notebook_sizer.Add(self.notebook, 1, wx.EXPAND, 0)
# customize
self.customize_panel = CustomizePanel(self.notebook, self)
self.notebook.AddPage(self.customize_panel, _('Customize'))
self.customize_panel.SetupScrolling() # scroll_x=False)
# code
self.code_panel = CodePanel(self.notebook, self)
self.notebook.AddPage(self.code_panel, _("Palette Code"))
# embroidery settings
self.embroidery_panel = EmbroideryPanel(self.notebook, self)
self.notebook.AddPage(self.embroidery_panel, _("Embroidery Settings"))
# help
help_panel = HelpPanel(self.notebook)
self.notebook.AddPage(help_panel, _("Help"))
# apply and cancel buttons
apply_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.cancel_button = wx.Button(self, label=_("Cancel"))
self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel)
self.apply_button = wx.Button(self, label=_("Apply"))
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
apply_sizer.Add(self.cancel_button, 0, wx.RIGHT | wx.BOTTOM, 5)
apply_sizer.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 10)
self.notebook_sizer.Add(self.presets_panel, 0, wx.EXPAND | wx.ALL, 10)
self.notebook_sizer.Add(apply_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.SetSizer(self.notebook_sizer)
self.load_settings()
self.apply_settings()
self.Layout()
self.SetMinSize(self.notebook_sizer.CalcMin())
def update_from_code(self):
self.palette.update_from_code(self.code_panel.threadcount_text.GetValue())
self.customize_panel.symmetry_checkbox.SetValue(self.palette.symmetry)
self.customize_panel.warp_weft_checkbox.SetValue(self.palette.equal_warp_weft)
self.code_panel.threadcount_text.SetValue(self.palette.palette_code)
self.customize_panel.update_stripes(self.palette.palette_stripes)
self.customize_panel.update_symmetry()
self.customize_panel.update_warp_weft()
self.customize_panel.FitInside()
self.update_preview()
def update_from_stripes(self):
sizers = [self.customize_panel.warp_sizer]
if not self.customize_panel.warp_weft_checkbox.GetValue():
sizers.append(self.customize_panel.weft_sizer)
self.palette.update_from_stripe_sizer(
sizers,
self.customize_panel.symmetry_checkbox.GetValue(),
self.customize_panel.warp_weft_checkbox.GetValue()
)
self.update_code_text()
self.update_preview()
def update_code_text(self):
self.code_panel.threadcount_text.SetValue(self.palette.palette_code)
self.settings['palette'] = self.palette.palette_code
def load_settings(self):
"""Load the settings saved into the SVG element"""
self.settings = DotDict({
"symmetry": True,
"equal_warp_weft": True,
"rotate": 0.0,
"scale": 100,
"offset_x": 0.0,
"offset_y": 0.0,
"palette": "K/10 W/?10",
"output": "embroidery",
"stitch_type": "legacy_fill",
"row_spacing": 1.0,
"angle_warp": 0.0,
"angle_weft": 90.0,
"min_stripe_width": 1.0,
"bean_stitch_repeats": 0
})
try:
self.settings.update(json.loads(self.elements[0].get(INKSTITCH_TARTAN)))
except (TypeError, ValueError, IndexError):
pass
def apply_settings(self):
"""Make the settings in self.settings visible in the UI."""
self.customize_panel.rotate.SetValue(self.settings.rotate)
self.customize_panel.scale.SetValue(int(self.settings.scale))
self.customize_panel.offset_x.SetValue(self.settings.offset_x)
self.customize_panel.offset_y.SetValue(self.settings.offset_y)
self.code_panel.threadcount_text.SetValue(self.settings.palette)
self.embroidery_panel.set_output(self.settings.output)
self.embroidery_panel.set_stitch_type(self.settings.stitch_type)
self.embroidery_panel.svg_row_spacing.SetValue(self.settings.row_spacing)
self.embroidery_panel.angle_warp.SetValue(self.settings.angle_warp)
self.embroidery_panel.angle_weft.SetValue(self.settings.angle_weft)
self.embroidery_panel.min_stripe_width.SetValue(self.settings.min_stripe_width)
self.embroidery_panel.svg_bean_stitch_repeats.SetValue(self.settings.bean_stitch_repeats)
self.embroidery_panel.stitch_angle.SetValue(self.elements[0].get('inkstitch:tartan_angle', -45))
self.embroidery_panel.rows_per_thread.SetValue(self.elements[0].get('inkstitch:rows_per_thread', 2))
self.embroidery_panel.row_spacing.SetValue(self.elements[0].get('inkstitch:row_spacing_mm', 0.25))
underlay = self.elements[0].get('inkstitch:fill_underlay', "True").lower() in ('yes', 'y', 'true', 't', '1')
self.embroidery_panel.underlay.SetValue(underlay)
self.embroidery_panel.herringbone.SetValue(self.elements[0].get('inkstitch:herringbone_width_mm', 0))
self.embroidery_panel.bean_stitch_repeats.SetValue(self.elements[0].get('inkstitch:bean_stitch_repeats', '0'))
self.update_from_code()
self.customize_panel.symmetry_checkbox.SetValue(bool(self.settings.symmetry))
self.palette.update_symmetry(self.settings.symmetry)
self.customize_panel.warp_weft_checkbox.SetValue(bool(self.settings.equal_warp_weft))
self.customize_panel.update_warp_weft()
def save_settings(self):
"""Save the settings into the SVG elements."""
for element in self.elements:
element.set(INKSTITCH_TARTAN, json.dumps(self.settings))
def get_preset_data(self):
# called by self.presets_panel
settings = dict(self.settings)
return settings
def _hide_warning(self):
self.warning_panel.clear()
self.warning_panel.Hide()
self.Layout()
def _show_warning(self, warning_text):
self.warning_panel.set_warning_text(warning_text)
self.warning_panel.Show()
self.Layout()
def update_preview(self, event=None):
self.preview_renderer.update()
def apply_preset_data(self, preset_data):
settings = DotDict(preset_data)
self.settings = settings
self.apply_settings()
def get_preset_suite_name(self):
# called by self.presets_panel
return "tartan"
def close(self):
self.GetTopLevelParent().Close()
def cancel(self, event):
if self.cancel_hook:
self.cancel_hook()
self.close()
def apply(self, event):
self.update_tartan()
self.save_settings()
self.close()
def render_stitch_plan(self):
if self.settings['output'] == 'svg':
self.update_tartan()
stitch_groups = self._get_svg_stitch_groups()
else:
self.save_settings()
stitch_groups = []
previous_stitch_group = None
for element in self.elements:
try:
# copy the embroidery element to drop the cache
stitch_groups.extend(copy(FillStitch(element)).embroider(previous_stitch_group))
if stitch_groups:
previous_stitch_group = stitch_groups[-1]
except (SystemExit, ExitThread):
raise
except InkstitchException as exc:
wx.CallAfter(self._show_warning, str(exc))
except Exception:
wx.CallAfter(self._show_warning, format_uncaught_exception())
if stitch_groups:
return stitch_groups_to_stitch_plan(
stitch_groups,
collapse_len=self.metadata['collapse_len_mm'],
min_stitch_len=self.metadata['min_stitch_len_mm']
)
def _get_svg_stitch_groups(self):
stitch_groups = []
previous_stitch_group = None
for element in self.elements:
parent = element.getparent()
embroidery_elements = nodes_to_elements(parent.iterdescendants())
for embroidery_element in embroidery_elements:
check_stop_flag()
if embroidery_element.node == element:
continue
try:
# copy the embroidery element to drop the cache
stitch_groups.extend(copy(embroidery_element).embroider(previous_stitch_group))
if stitch_groups:
previous_stitch_group = stitch_groups[-1]
except InkstitchException:
pass
except Exception:
pass
return stitch_groups
def update_tartan(self):
for element in self.elements:
check_stop_flag()
if self.settings['output'] == 'svg':
TartanSvgGroup(self.settings).generate(element)
else:
prepare_tartan_fill_element(element)
def on_stitch_plan_rendered(self, stitch_plan):
self.simulator.stop()
self.simulator.load(stitch_plan)
self.simulator.go()

Wyświetl plik

@ -9,6 +9,7 @@ from .fill import legacy_fill
from .guided_fill import guided_fill
from .linear_gradient_fill import linear_gradient_fill
from .meander_fill import meander_fill
from .tartan_fill import tartan_fill
# Can't put this here because we get a circular import :(
# from .auto_satin import auto_satin

Wyświetl plik

@ -7,6 +7,7 @@
import math
from itertools import chain, groupby
from typing import Iterator
import networkx
from shapely import geometry as shgeo
@ -53,11 +54,15 @@ class PathEdge(object):
def __eq__(self, other):
return self._sorted_nodes == other._sorted_nodes and self.key == other.key
def __iter__(self) -> Iterator:
for i in range(2):
yield self[i]
def is_outline(self):
return self.key in self.OUTLINE_KEYS
return self.key.startswith(self.OUTLINE_KEYS)
def is_segment(self):
return self.key == self.SEGMENT_KEY
return self.key.startswith(self.SEGMENT_KEY)
@debug.time
@ -87,7 +92,7 @@ def auto_fill(shape,
return fallback(shape, running_stitch_length, running_stitch_tolerance)
# ensure graph is eulerian
fill_stitch_graph = graph_make_valid(fill_stitch_graph)
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
@ -298,8 +303,24 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
def graph_make_valid(graph):
if not networkx.is_eulerian(graph):
return networkx.eulerize(graph)
return graph
newgraph = networkx.eulerize(graph)
for start, end, key, data in newgraph.edges(keys=True, data=True):
if isinstance(key, int):
# make valid duplicated edges, we cannot use the very same key
# again, but the automatic naming will not apply to the autofill algorithm
graph_edges = graph[start][end]
if 'segment' in graph_edges.keys():
data = graph_edges['segment']
graph.add_edge(start, end, key=f'segment-{key}', **data)
elif 'outline' in graph_edges.keys():
data = graph_edges['outline']
graph.add_edge(start, end, key='outline-{key}', **data)
elif 'extra' in graph_edges.keys():
data = graph_edges['extra']
graph.add_edge(start, end, key='extra-{key}', **data)
elif 'initial' in graph_edges.keys():
data = graph_edges['initial']
graph.add_edge(start, end, key='initial-{key}', **data)
def fallback(shape, running_stitch_length, running_stitch_tolerance):
@ -380,7 +401,7 @@ def weight_edges_by_length(graph, multiplier=1):
def get_segments(graph):
segments = []
for start, end, key, data in graph.edges(keys=True, data=True):
if key == 'segment':
if key.startswith('segment'):
segments.append(data["geometry"])
return segments

Wyświetl plik

@ -77,7 +77,7 @@ def circular_fill(shape,
if is_empty(fill_stitch_graph):
return fallback(shape, running_stitch_length, running_stitch_tolerance)
fill_stitch_graph = graph_make_valid(fill_stitch_graph)
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)

Wyświetl plik

@ -14,12 +14,15 @@ from ..utils import cache
from ..utils.threading import check_stop_flag
def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last):
def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, reverse, staggers, skip_last):
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip)
groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing)
return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last)
for group in groups_of_segments]
stitches = [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last)
for group in groups_of_segments]
if reverse:
stitches = [segment[::-1] for segment in stitches[::-1]]
return stitches
@cache
@ -223,14 +226,8 @@ def pull_runs(rows, shape, row_spacing):
# over to midway up the lower right leg. We want to stop there and
# start a new patch.
# for row in rows:
# print >> sys.stderr, len(row)
# print >>sys.stderr, "\n".join(str(len(row)) for row in rows)
rows = list(rows)
runs = []
count = 0
while (len(rows) > 0):
run = []
prev = None
@ -248,10 +245,7 @@ def pull_runs(rows, shape, row_spacing):
rows[row_num] = rest
# print >> sys.stderr, len(run)
runs.append(run)
rows = [r for r in rows if len(r) > 0]
count += 1
return runs

Wyświetl plik

@ -43,7 +43,7 @@ def guided_fill(shape,
if is_empty(fill_stitch_graph):
return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
fill_stitch_graph = graph_make_valid(fill_stitch_graph)
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
@ -156,14 +156,14 @@ def take_only_line_strings(thing):
return shgeo.MultiLineString(line_strings)
def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None):
def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None) -> shgeo.LineString:
if num_staggers == 0:
num_staggers = 1 # sanity check to avoid division by zero.
start = ((row_num / num_staggers) % 1) * max_stitch_length
projections = np.arange(start, line.length, max_stitch_length)
points = np.array([line.interpolate(projection).coords[0] for projection in projections])
if len(points) <= 2:
if len(points) < 2:
return line
stitched_line = shgeo.LineString(points)

Wyświetl plik

@ -268,7 +268,7 @@ def _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_
if is_empty(fill_stitch_graph):
continue
fill_stitch_graph = graph_make_valid(fill_stitch_graph)
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, fill.angle, False)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)

Wyświetl plik

@ -0,0 +1,808 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from collections import defaultdict
from itertools import chain
from math import cos, radians, sin
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from networkx import is_empty
from shapely import get_point, line_merge, minimum_bounding_radius, segmentize
from shapely.affinity import rotate, scale, translate
from shapely.geometry import LineString, MultiLineString, Point, Polygon
from shapely.ops import nearest_points
from ..stitch_plan import Stitch, StitchGroup
from ..svg import PIXELS_PER_MM
from ..tartan.utils import (get_palette_width, get_tartan_settings,
get_tartan_stripes, sort_fills_and_strokes,
stripes_to_shapes)
from ..utils import cache, ensure_multi_line_string
from ..utils.threading import check_stop_flag
from .auto_fill import (build_fill_stitch_graph, build_travel_graph,
find_stitch_path, graph_make_valid)
from .circular_fill import path_to_stitches
from .guided_fill import apply_stitches
from .linear_gradient_fill import remove_start_end_travel
from .running_stitch import bean_stitch
if TYPE_CHECKING:
from ..elements import FillStitch
def tartan_fill(fill: 'FillStitch', outline: Polygon, starting_point: Union[tuple, Stitch, None], ending_point: Union[tuple, Stitch, None]):
"""
Main method to fill the tartan element with tartan fill stitches
:param fill: FillStitch element
:param outline: the outline of the fill
:param starting_point: the starting point (or None)
:param ending_point: the ending point (or None)
:returns: stitch_groups forming the tartan pattern
"""
tartan_settings = get_tartan_settings(fill.node)
warp, weft = get_tartan_stripes(tartan_settings)
warp_width = get_palette_width(tartan_settings)
weft_width = get_palette_width(tartan_settings, 1)
offset = (abs(tartan_settings['offset_x']), abs(tartan_settings['offset_y']))
rotation = tartan_settings['rotate']
dimensions = _get_dimensions(fill, outline, offset, warp_width, weft_width)
rotation_center = _get_rotation_center(outline)
warp_shapes = stripes_to_shapes(
warp,
dimensions,
outline,
rotation,
rotation_center,
tartan_settings['symmetry'],
tartan_settings['scale'],
tartan_settings['min_stripe_width'],
False, # weft
False # do not cut polygons just yet
)
weft_shapes = stripes_to_shapes(
weft,
dimensions,
outline,
rotation,
rotation_center,
tartan_settings['symmetry'],
tartan_settings['scale'],
tartan_settings['min_stripe_width'],
True, # weft
False # do not cut polygons just yet
)
if fill.herringbone_width > 0:
lines = _generate_herringbone_lines(outline, fill, dimensions, rotation)
warp_lines, weft_lines = _split_herringbone_warp_weft(lines, fill.rows_per_thread, fill.running_stitch_length)
warp_color_lines = _get_herringbone_color_segments(warp_lines, warp_shapes, outline, rotation, fill.running_stitch_length)
weft_color_lines = _get_herringbone_color_segments(weft_lines, weft_shapes, outline, rotation, fill.running_stitch_length, True)
else:
lines = _generate_tartan_lines(outline, fill, dimensions, rotation)
warp_lines, weft_lines = _split_warp_weft(lines, fill.rows_per_thread)
warp_color_lines = _get_tartan_color_segments(warp_lines, warp_shapes, outline, rotation, fill.running_stitch_length)
weft_color_lines = _get_tartan_color_segments(weft_lines, weft_shapes, outline, rotation, fill.running_stitch_length, True)
if not lines:
return []
warp_color_runs = _get_color_runs(warp_shapes, fill.running_stitch_length)
weft_color_runs = _get_color_runs(weft_shapes, fill.max_stitch_length)
color_lines = defaultdict(list)
for color, lines in chain(warp_color_lines.items(), weft_color_lines.items()):
color_lines[color].extend(lines)
color_runs = defaultdict(list)
for color, lines in chain(warp_color_runs.items(), weft_color_runs.items()):
color_runs[color].extend(lines)
color_lines, color_runs = sort_fills_and_strokes(color_lines, color_runs)
stitch_groups = _get_fill_stitch_groups(fill, outline, color_lines)
if stitch_groups:
starting_point = stitch_groups[-1].stitches[-1]
stitch_groups += _get_run_stitch_groups(fill, outline, color_runs, starting_point, ending_point)
return stitch_groups
def _generate_herringbone_lines(
outline: Polygon,
fill: 'FillStitch',
dimensions: Tuple[float, float, float, float],
rotation: float,
) -> List[List[List[LineString]]]:
"""
Generates herringbone lines with staggered stitch positions
:param outline: the outline to fill with the herringbone lines
:param fill: the tartan fill element
:param dimensions: minx, miny, maxx, maxy
:param rotation: the rotation value
:returns: a tuple of two list with herringbone stripes [0] up segments / [1] down segments \
"""
rotation_center = _get_rotation_center(outline)
minx, miny, maxx, maxy = dimensions
herringbone_lines: list = [[], []]
odd = True
while minx < maxx:
odd = not odd
right = minx + fill.herringbone_width
if odd:
left_line = LineString([(minx, miny), (minx, maxy + fill.herringbone_width)])
else:
left_line = LineString([(minx, miny - fill.herringbone_width), (minx, maxy)])
if odd:
right_line = LineString([(right, miny - fill.herringbone_width), (right, maxy)])
else:
right_line = LineString([(right, miny), (right, maxy + fill.herringbone_width)])
left_line = segmentize(left_line, max_segment_length=fill.row_spacing)
right_line = segmentize(right_line, max_segment_length=fill.row_spacing)
lines = list(zip(left_line.coords, right_line.coords))
staggered_lines = []
for i, line in enumerate(lines):
linestring = LineString(line)
staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
staggered_lines.append(staggered_line)
if odd:
herringbone_lines[0].append(list(rotate(MultiLineString(staggered_lines), rotation, rotation_center).geoms))
else:
herringbone_lines[1].append(list(rotate(MultiLineString(staggered_lines), rotation, rotation_center).geoms))
# add some little space extra to make things easier with line_merge later on
# (avoid spots with 4 line points)
minx += fill.herringbone_width + 0.005
return herringbone_lines
def _generate_tartan_lines(
outline: Polygon,
fill: 'FillStitch',
dimensions: Tuple[float, float, float, float],
rotation: float,
) -> List[LineString]:
"""
Generates tartan lines with staggered stitch positions
:param outline: the outline to fill with the herringbone lines
:param fill: the tartan fill element
:param dimensions: minx, miny, maxx, maxy
:param rotation: the rotation value
:returns: a list with the tartan lines
"""
rotation_center = _get_rotation_center(outline)
# default angle is 45°
rotation += fill.tartan_angle
minx, miny, maxx, maxy = dimensions
left_line = LineString([(minx, miny), (minx, maxy)])
left_line = rotate(left_line, rotation, rotation_center)
left_line = segmentize(left_line, max_segment_length=fill.row_spacing)
right_line = LineString([(maxx, miny), (maxx, maxy)])
right_line = rotate(right_line, rotation, rotation_center)
right_line = segmentize(right_line, max_segment_length=fill.row_spacing)
lines = list(zip(left_line.coords, right_line.coords))
staggered_lines = []
for i, line in enumerate(lines):
linestring = LineString(line)
staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
staggered_lines.append(staggered_line)
return staggered_lines
def _split_herringbone_warp_weft(
lines: List[List[List[LineString]]],
rows_per_thread: int,
stitch_length: float
) -> tuple:
"""
Split the herringbone lines into warp lines and weft lines as defined by rows rows_per_thread
Merge weft lines for each block.
:param lines: lines to divide
:param rows_per_thread: length of line blocks
:param stitch_length: maximum stitch length for weft connector lines
:returns: [0] warp and [1] weft list of MultiLineString objects
"""
warp_lines: List[LineString] = []
weft_lines: List[LineString] = []
for i, line_blocks in enumerate(lines):
for line_block in line_blocks:
if i == 0:
warp, weft = _split_warp_weft(line_block, rows_per_thread)
else:
weft, warp = _split_warp_weft(line_block, rows_per_thread)
warp_lines.append(warp)
weft_lines.append(weft)
connected_weft = []
line2 = None
for multilinestring in weft_lines:
connected_line_block = []
geoms = list(multilinestring.geoms)
for line1, line2 in zip(geoms[:-1], geoms[1:]):
connected_line_block.append(line1)
connector_line = LineString([get_point(line1, -1), get_point(line2, 0)])
connector_line = segmentize(connector_line, max_segment_length=stitch_length)
connected_line_block.append(connector_line)
if line2:
connected_line_block.append(line2)
connected_weft.append(ensure_multi_line_string(line_merge(MultiLineString(connected_line_block))))
return warp_lines, connected_weft
def _split_warp_weft(lines: List[LineString], rows_per_thread: int) -> Tuple[List[LineString], List[LineString]]:
"""
Divide given lines in warp and weft, sort afterwards
:param lines: a list of LineString shapes
:param rows_per_thread: length of line blocks
:returns: tuple with sorted [0] warp and [1] weft LineString shapes
"""
warp_lines = []
weft_lines = []
for i in range(rows_per_thread):
warp_lines.extend(lines[i::rows_per_thread*2])
weft_lines.extend(lines[i+rows_per_thread::rows_per_thread*2])
return _sort_lines(warp_lines), _sort_lines(weft_lines)
def _sort_lines(lines: List[LineString]):
"""
Sort given list of LineString shapes by first coordinate
and reverse every second line
:param lines: a list of LineString shapes
:returns: sorted list of LineString shapes with alternating directions
"""
# sort lines
lines.sort(key=lambda line: line.coords[0])
# reverse every second line
lines = [line if i % 2 == 0 else line.reverse() for i, line in enumerate(lines)]
return MultiLineString(lines)
@cache
def _get_rotation_center(outline: Polygon) -> Point:
"""
Returns the rotation center used for any tartan pattern rotation
:param outline: the polygon shape to be filled with the pattern
:returns: the center point of the shape
"""
# somehow outline.centroid doesn't deliver the point we need
bounds = outline.bounds
return LineString([(bounds[0], bounds[1]), (bounds[2], bounds[3])]).centroid
@cache
def _get_dimensions(
fill: 'FillStitch',
outline: Polygon,
offset: Tuple[float, float],
warp_width: float,
weft_width: float
) -> Tuple[float, float, float, float]:
"""
Calculates the dimensions for the tartan pattern.
Make sure it is big enough for pattern rotations, etc.
:param fill: the FillStitch element
:param outline: the shape to be filled with a tartan pattern
:param offset: mm offset for x, y
:param warp_width: mm warp width
:param weft_width: mm weft width
:returns: a tuple with boundaries (minx, miny, maxx, maxy)
"""
# add space to allow rotation and herringbone patterns to cover the shape
centroid = _get_rotation_center(outline)
min_radius = minimum_bounding_radius(outline)
minx = centroid.x - min_radius
miny = centroid.y - min_radius
maxx = centroid.x + min_radius
maxy = centroid.y + min_radius
# add some extra space
extra_space = max(
warp_width * PIXELS_PER_MM,
weft_width * PIXELS_PER_MM,
2 * fill.row_spacing * fill.rows_per_thread
)
minx -= extra_space
maxx += extra_space
miny -= extra_space
maxy += extra_space
minx -= (offset[0] * PIXELS_PER_MM)
miny -= (offset[1] * PIXELS_PER_MM)
return minx, miny, maxx, maxy
def _get_herringbone_color_segments(
lines: List[MultiLineString],
polygons: defaultdict,
outline: Polygon,
rotation: float,
stitch_length: float,
weft: bool = False
) -> defaultdict:
"""
Generate herringbone line segments in given tartan direction grouped by color
:param lines: the line segments forming the pattern
:param polygons: color grouped polygon stripes
:param outline: the outline to be filled with the herringbone pattern
:param rotation: degrees used for rotation
:param stitch_length: maximum stitch length for weft connector lines
:param weft: wether to render as warp or weft
:returns: defaultdict with color grouped herringbone segments
"""
line_segments: defaultdict = defaultdict(list)
if not polygons:
return line_segments
lines = line_merge(lines)
for line_blocks in lines:
segments = _get_tartan_color_segments(line_blocks, polygons, outline, rotation, stitch_length, weft, True)
for color, segment in segments.items():
if weft:
line_segments[color].append(MultiLineString(segment))
else:
line_segments[color].extend(segment)
if not weft:
return line_segments
return _get_weft_herringbone_color_segments(outline, line_segments, polygons, stitch_length)
def _get_weft_herringbone_color_segments(
outline: Polygon,
line_segments: defaultdict,
polygons: defaultdict,
stitch_length: float,
) -> defaultdict:
"""
Makes sure weft herringbone lines connect correctly
Herringbone weft lines need to connect in horizontal direction (or whatever the current rotation is)
which is opposed to the herringbone stripe blocks \\\\ //// \\\\ //// \\\\ ////
:param outline: the outline to be filled with the herringbone pattern
:param line_segments: the line segments forming the pattern
:param polygons: color grouped polygon stripes
:param stitch_length: maximum stitch length
:returns: defaultdict with color grouped weft lines
"""
weft_lines = defaultdict(list)
for color, lines in line_segments.items():
color_lines: List[LineString] = []
for polygon in polygons[color][0]:
polygon = polygon.normalize()
polygon_coords = list(polygon.exterior.coords)
polygon_top = LineString(polygon_coords[0:2])
polygon_bottom = LineString(polygon_coords[2:4]).reverse()
if not any([polygon_top.intersects(outline), polygon_bottom.intersects(outline)]):
polygon_top = LineString(polygon_coords[1:3])
polygon_bottom = LineString(polygon_coords[3:5]).reverse()
polygon_multi_lines = lines
polygon_multi_lines.sort(key=lambda line: polygon_bottom.project(line.centroid))
polygon_lines = []
for multiline in polygon_multi_lines:
polygon_lines.extend(multiline.geoms)
polygon_lines = [line for line in polygon_lines if line.intersects(polygon)]
if not polygon_lines:
continue
color_lines.extend(polygon_lines)
if polygon_top.intersects(outline) or polygon_bottom.intersects(outline):
connectors = _get_weft_herringbone_connectors(polygon_lines, polygon_top, polygon_bottom, stitch_length)
if connectors:
color_lines.extend(connectors)
check_stop_flag()
# Users are likely to type in a herringbone width which is a multiple (or fraction) of the stripe width.
# They may end up unconnected after line_merge, so we need to shift the weft for a random small number
multi_lines = translate(ensure_multi_line_string(line_merge(MultiLineString(color_lines))), 0.00123, 0.00123)
multi_lines = ensure_multi_line_string(multi_lines.intersection(outline))
weft_lines[color].extend(list(multi_lines.geoms))
return weft_lines
def _get_weft_herringbone_connectors(
polygon_lines: List[LineString],
polygon_top: LineString,
polygon_bottom: LineString,
stitch_length: float
) -> List[LineString]:
"""
Generates lines to connect lines
:param polygon_lines: lines to connect
:param polygon_top: top line of the polygon
:param polygon_bottom: bottom line of the polygon
:param stitch_length: stitch length
:returns: a list of LineString connectors
"""
connectors: List[LineString] = []
previous_end = None
for line in reversed(polygon_lines):
start = get_point(line, 0)
end = get_point(line, -1)
if previous_end is None:
# adjust direction of polygon lines if necessary
if polygon_top.project(start, True) > 0.5:
polygon_top = polygon_top.reverse()
polygon_bottom = polygon_bottom.reverse()
start_distance = polygon_top.project(start)
end_distance = polygon_top.project(end)
if start_distance > end_distance:
start, end = end, start
previous_end = end
continue
# adjust line direction and add connectors
prev_polygon_line = min([polygon_top, polygon_bottom], key=lambda polygon_line: previous_end.distance(polygon_line))
current_polygon_line = min([polygon_top, polygon_bottom], key=lambda polygon_line: start.distance(polygon_line))
if prev_polygon_line != current_polygon_line:
start, end = end, start
if not previous_end == start:
connector = LineString([previous_end, start])
if prev_polygon_line == polygon_top:
connector = connector.offset_curve(-0.0001)
else:
connector = connector.offset_curve(0.0001)
connectors.append(LineString([previous_end, get_point(connector, 0)]))
connectors.append(segmentize(connector, max_segment_length=stitch_length))
connectors.append(LineString([get_point(connector, -1), start]))
previous_end = end
return connectors
def _get_tartan_color_segments(
lines: List[LineString],
polygons: defaultdict,
outline: Polygon,
rotation: float,
stitch_length: float,
weft: bool = False,
herringbone: bool = False
) -> defaultdict:
"""
Generate tartan line segments in given tartan direction grouped by color
:param lines: the lines to form the tartan pattern with
:param polygons: color grouped polygon stripes
:param outline: the outline to fill with the tartan pattern
:param rotation: rotation in degrees
:param stitch_length: maximum stitch length for weft connector lines
:param weft: wether to render as warp or weft
:param herringbone: wether herringbone or normal tartan patterns are rendered
:returns: a dictionary with color grouped line segments
"""
line_segments: defaultdict = defaultdict(list)
if not polygons:
return line_segments
for color, shapes in polygons.items():
polygons = shapes[0]
for polygon in polygons:
segments = _get_segment_lines(polygon, lines, outline, stitch_length, rotation, weft, herringbone)
if segments:
line_segments[color].extend(segments)
check_stop_flag()
return line_segments
def _get_color_runs(lines: defaultdict, stitch_length: float) -> defaultdict:
"""
Segmentize running stitch segments and return in a separate color grouped dictionary
:param lines: tartan shapes grouped by color
:param stitch_length: stitch length used to segmentize the lines
:returns: defaultdict with segmentized running stitches grouped by color
"""
runs: defaultdict = defaultdict(list)
if not lines:
return runs
for color, shapes in lines.items():
for run in shapes[1]:
runs[color].append(segmentize(run, max_segment_length=stitch_length))
return runs
def _get_segment_lines(
polygon: Polygon,
lines: MultiLineString,
outline: Polygon,
stitch_length: float,
rotation: float,
weft: bool,
herringbone: bool
) -> List[LineString]:
"""
Fill the given polygon with lines
Each line should start and end at the outline border
:param polygon: the polygon stripe to fill
:param lines: the lines that form the pattern
:param outline: the outline to fill with the tartan pattern
:param stitch_length: maximum stitch length for weft connector lines
:param rotation: rotation in degrees
:param weft: wether to render as warp or weft
:param herringbone: wether herringbone or normal tartan patterns are rendered
:returns: a list of LineString objects
"""
boundary = outline.boundary
segments = []
if not lines.intersects(polygon):
return []
segment_lines = list(ensure_multi_line_string(lines.intersection(polygon), 0.5).geoms)
if not segment_lines:
return []
previous_line = None
for line in segment_lines:
segments.append(line)
if not previous_line:
previous_line = line
continue
point1 = get_point(previous_line, -1)
point2 = get_point(line, 0)
if point1.equals(point2):
previous_line = line
continue
# add connector from point1 to point2 if none of them touches the outline
connector = _get_connector(point1, point2, boundary, stitch_length)
if connector:
segments.append(connector)
previous_line = line
if not segments:
return []
lines = line_merge(MultiLineString(segments))
if not (herringbone and weft):
lines = lines.intersection(outline)
if not herringbone:
lines = _connect_lines_to_outline(lines, outline, rotation, stitch_length, weft)
return list(ensure_multi_line_string(lines).geoms)
def _get_connector(
point1: Point,
point2: Point,
boundary: Union[MultiLineString, LineString],
stitch_length: float
) -> Optional[LineString]:
"""
Constructs a line between the two points when they are not near the boundary
:param point1: first point
:param point2: last point
:param boundary: the outline of the shape (including holes)
:param stitch_length: maximum stitch length to segmentize new line
:returns: a LineString between point1 and point1, None if one of them touches the boundary
"""
connector = None
if point1.distance(boundary) > 0.005 and point2.distance(boundary) > 0.005:
connector = segmentize(LineString([point1, point2]), max_segment_length=stitch_length)
return connector
def _connect_lines_to_outline(
lines: Union[MultiLineString, LineString],
outline: Polygon,
rotation: float,
stitch_length: float,
weft: bool
) -> Union[MultiLineString, LineString]:
"""
Connects end points within the shape with the outline
This should only be necessary if the tartan angle is nearly 0 or 90 degrees
:param lines: lines to connect to the outline (if necessary)
:param outline: the shape to be filled with a tartan pattern
:param rotation: the rotation value
:param stitch_length: maximum stitch length to segmentize new line
:param weft: wether to render as warp or weft
:returns: merged line(s) connected to the outline
"""
boundary = outline.boundary
lines = list(ensure_multi_line_string(lines).geoms)
outline_connectors = []
for line in lines:
start = get_point(line, 0)
end = get_point(line, -1)
if start.intersects(outline) and start.distance(boundary) > 0.05:
outline_connectors.append(_connect_point_to_outline(start, outline, rotation, stitch_length, weft))
if end.intersects(outline) and end.distance(boundary) > 0.05:
outline_connectors.append(_connect_point_to_outline(end, outline, rotation, stitch_length, weft))
lines.extend(outline_connectors)
lines = line_merge(MultiLineString(lines))
return lines
def _connect_point_to_outline(
point: Point,
outline: Polygon,
rotation: float,
stitch_length: float,
weft: bool
) -> Union[LineString, list]:
"""
Connect given point to the outline
:param outline: the shape to be filled with a tartan pattern
:param rotation: the rotation value
:param stitch_length: maximum stitch length to segmentize new line
:param weft: wether to render as warp or weft
:returns: a Linestring with the correct angle for the given tartan direction (between outline and point)
"""
scale_factor = point.hausdorff_distance(outline) * 2
directional_vector = _get_angled_line_from_point(point, rotation, scale_factor, weft)
directional_vector = outline.boundary.intersection(directional_vector)
if directional_vector.is_empty:
return []
return segmentize(LineString([point, nearest_points(directional_vector, point)[0]]), max_segment_length=stitch_length)
def _get_angled_line_from_point(point: Point, rotation: float, scale_factor: float, weft: bool) -> LineString:
"""
Generates an angled line for the given tartan direction
:param point: the starting point for the new line
:param rotation: the rotation value
:param scale_factor: defines the length of the line
:param weft: wether to render as warp or weft
:returns: a LineString
"""
if not weft:
rotation += 90
rotation = radians(rotation)
x = point.coords[0][0] + cos(rotation)
y = point.coords[0][1] + sin(rotation)
return scale(LineString([point, (x, y)]), scale_factor, scale_factor)
def _get_fill_stitch_groups(
fill: 'FillStitch',
shape: Polygon,
color_lines: defaultdict,
) -> List[StitchGroup]:
"""
Route fill stitches
:param fill: the FillStitch element
:param shape: the shape to be filled
:param color_lines: lines grouped by color
:returns: a list with StitchGroup objects
"""
stitch_groups: List[StitchGroup] = []
i = 0
for color, lines in color_lines.items():
i += 1
if stitch_groups:
starting_point = stitch_groups[-1].stitches[-1]
else:
starting_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[0]
ending_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[0]
segments = [list(line.coords) for line in lines if len(line.coords) > 1]
stitch_group = _segments_to_stitch_group(fill, shape, segments, i, color, starting_point, ending_point)
if stitch_group is not None:
stitch_groups.append(stitch_group)
check_stop_flag()
return stitch_groups
def _get_run_stitch_groups(
fill: 'FillStitch',
shape: Polygon,
color_lines: defaultdict,
starting_point: Optional[Union[tuple, Stitch]],
ending_point: Optional[Union[tuple, Stitch]]
) -> List[StitchGroup]:
"""
Route running stitches
:param fill: the FillStitch element
:param shape: the shape to be filled
:param color_lines: lines grouped by color
:param starting_point: the starting point
:param ending_point: the ending point
:returns: a list with StitchGroup objects
"""
stitch_groups: List[StitchGroup] = []
for color, lines in color_lines.items():
segments = [list(line.coords) for line in lines if len(line.coords) > 1]
stitch_group = _segments_to_stitch_group(fill, shape, segments, None, color, starting_point, ending_point, True)
if stitch_group is not None:
stitch_groups.append(stitch_group)
check_stop_flag()
return stitch_groups
def _segments_to_stitch_group(
fill: 'FillStitch',
shape: Polygon,
segments: List[List[Tuple[float, float]]],
iteration: Optional[int],
color: str,
starting_point: Optional[Union[tuple, Stitch]],
ending_point: Optional[Union[tuple, Stitch]],
runs: bool = False
) -> Optional[StitchGroup]:
"""
Route segments and turn them into a stitch group
:param fill: the FillStitch element
:param shape: the shape to be filled
:param segments: a list with coordinate tuples
:param iteration: wether to remove start and end travel stitches from the stitch group
:param color: color information
:param starting_point: the starting point
:param ending_point: the ending point
:param runs: wether running_stitch options should be applied or not
:returns: a StitchGroup
"""
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
if is_empty(fill_stitch_graph):
return None
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, fill.angle, False)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
stitches = path_to_stitches(
shape,
path,
travel_graph,
fill_stitch_graph,
fill.running_stitch_length,
fill.running_stitch_tolerance,
fill.skip_last,
False # no underpath
)
if iteration:
stitches = remove_start_end_travel(fill, stitches, color, iteration)
if runs:
stitches = bean_stitch(stitches, fill.bean_stitch_repeats, ['auto_fill_travel'])
stitch_group = StitchGroup(
color=color,
tags=("tartan_fill", "auto_fill_top"),
stitches=stitches,
force_lock_stitches=fill.force_lock_stitches,
lock_stitches=fill.lock_stitches,
trim_after=fill.has_command("trim") or fill.trim_after
)
if runs:
stitch_group.add_tag("tartan_run")
return stitch_group

Wyświetl plik

@ -46,6 +46,7 @@ SODIPODI_INSENSITIVE = inkex.addNS('insensitive', 'sodipodi')
SODIPODI_NODETYPES = inkex.addNS('nodetypes', 'sodipodi')
INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch')
INKSTITCH_TARTAN = inkex.addNS('tartan', 'inkstitch')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_LINE_TAG, SVG_POLYLINE_TAG, SVG_POLYGON_TAG,
SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG)
@ -102,6 +103,9 @@ inkstitch_attribs = [
'stop_at_ending_point',
'flip',
'clip',
'rows_per_thread',
'herringbone_width_mm',
'tartan_angle',
# stroke
'stroke_method',
'bean_stitch_repeats',

Wyświetl plik

@ -0,0 +1,159 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
# Additional credits to https://github.com/clsn/pyTartan
# tartan colors according to https://www.tartanregister.gov.uk/docs/Colour_shades.pdf (as of december 2023)
# Problem: ambigious due to multiple usage of same color code
def string_to_color(color_string: str) -> str:
"""
Converts a color code from the tartan register to a hex color code or defaults to empty
:param color_string: color code from the tartan register
:returns: hex color code or empty string
"""
standards = {
# 'LR': '#F4CCCC', # Light Red
'LR': '#E87878', # Light Red
# 'LR': '#F04DB0', # Light Red
# 'R': '#A00048', # Red
# 'R': '#FA4B00', # Red
'R': '#FF0000', # Red
# 'R': '#DC0000', # Red
# 'R': '#C80000', # Red
# 'R': '#C82828', # Red
# 'R': '#C8002C', # Red
# 'R': '#B03000', # Red
# 'DR': '#A00000', # Dark Red
# 'DR': '#960000', # Dark Red
# 'DR': '#960028', # Dark Red
'DR': '#880000', # Dark Red
# 'DR': '#800028', # Dark Red
# 'DR': '#781C38', # Dark Red
# 'DR': '#4C0000', # Dark Red
# 'DR': '#901C38', # Dark Red
# 'DR': '#680028', # Dark Red
# 'O': '#EC8048', # Orange
# 'O': '#E86000', # Orange
'O': '#FF5000', # Orange
# 'O': '#DC943C', # Orange
# 'O': '#D87C00', # Orange
'DO': '#BE7832', # Dark Orange
'LY': '#F9F5C8', # Light Yellow
# 'LY': '#F8E38C', # Light Yellow
'Y': '#FFFF00', # Yellow
# 'Y': '#FFE600', # Yellow
# 'Y': '#FFD700', # Yellow
# 'Y': '#FCCC00', # Yellow
# 'Y': '#E0A126', # Yellow
# 'Y': '#E8C000', # Yellow
# 'Y': '#D8B000', # Yellow
# 'DY': '#BC8C00', # Dark Yellow
# 'DY': '#C89800', # Dark Yellow
'DY': '#C88C00', # Dark Yellow
# 'LG': '#789484', # Light Green
# 'LG': '#C4BC68', # Light Green
# 'LG': '#9C9C00', # Light Green
'LG': '#ACD74A', # Light Green
# 'LG': '#86C67C', # Light Green
# 'LG': '#649848', # Light Green
# 'G': '#008B00', # Green
# 'G': '#408060', # Green
'G': '#289C18', # Green
# 'G': '#006400', # Green
# 'G': '#007800', # Green
# 'G': '#3F5642', # Green
# 'G': '#767E52', # Green
# 'G': '#5C6428', # Green
# 'G': '#00643C', # Green
# 'G': '#146400', # Green
# 'G': '#006818', # Green
# 'G': '#004C00', # Green
# 'G': '#285800', # Green
# 'G': '#005020', # Green
# 'G': '#005448', # Green
# 'DG': '#003C14', # Dark Green
# 'DG': '#003820', # Dark Green
'DG': '#004028', # Dark Green
# 'DG': '#002814', # Dark Green
# 'LB': '#98C8E8', # Light Blue
'LB': '#82CFFD', # Light Blue
# 'LB': '#00FCFC', # Light Blue
# 'B': '#BCC3D2', # Blue
# 'B': '#048888', # Blue
# 'B': '#3C82AF', # Blue
# 'B': '#5C8CA8', # Blue
# 'B': '#2888C4', # Blue
# 'B': '#48A4C0', # Blue
# 'B': '#2474E8', # Blue
# 'B': '#0596FA', # Blue
'B': '#0000FF', # Blue
# 'B': '#3850C8', # Blue
# 'B': '#788CB4', # Blue
# 'B': '#5F749C', # Blue
# 'B': '#1870A4', # Blue
# 'B': '#1474B4', # Blue
# 'B': '#0000CD', # Blue
# 'B': '#2C4084', # Blue
# 'DB': '#055183', # Dark Blue
# 'DB': '#003C64', # Dark Blue
'DB': '#00008C', # Dark Blue
# 'DB': '#2C2C80', # Dark Blue
# 'DB': '#1C0070', # Dark Blue
# 'DB': '#000064', # Dark Blue
# 'DB': '#202060', # Dark Blue
# 'DB': '#000048', # Dark Blue
# 'DB': '#141E46', # Dark Blue
# 'DB': '#1C1C50', # Dark Blue
'LP': '#A8ACE8', # Light Purple
# 'LP': '#C49CD8', # Light Purple
# 'LP': '#806D84', # Light Purple
# 'LP': '#9C68A4', # Light Purple
# 'P': '#9058D8', # Purple
# 'P': '#AA00FF', # Purple
# 'P': '#B458AC', # Purple
# 'P': '#6C0070', # Purple
# 'P': '#5A008C', # Purple
# 'P': '#64008C', # Purple
'P': '#780078', # Purple
# 'DP': '#440044', # Dark Purple
'DP': '#1E0948', # Dark Purple
# 'W': '#E5DDD1', # White
# 'W': '#E8CCB8', # White
# 'W': '#F0E0C8', # White
# 'W': '#FCFCFC', # White
'W': '#FFFFFF', # White
# 'W': '#F8F8F8', # White
'LN': '#E0E0E0', # Light Grey
# 'N': '#C8C8C8', # Grey
# 'N': '#C0C0C0', # Grey
# 'N': '#B0B0B0', # Grey
'N': '#A0A0A0', # Grey
# 'N': '#808080', # Grey
# 'N': '#888888', # Grey
# 'N': '#646464', # Grey
# 'N': '#505050', # Dark Grey
'DN': '#555a64', # Dark Grey
# 'DN': '#1C1714', # Dark Grey
# 'DN': '#14283C', # Dark Grey
# 'DN': '#1C1C1C', # Dark Grey
# 'K': '#101010', # Black
'K': '#000000', # Black
# 'LT': '#A08858', # Light Brown
# 'LT': '#8C7038', # Light Brown
'LT': '#A07C58', # Light Brown
# 'LT': '#B07430', # Light Brown
# 'T': '#98481C', # Brown
'T': '#603800', # Brown
# 'T': '#604000', # Brown
# 'T': '#503C14', # Brown
# 'DT': '#4C3428', # Dark Brown
'DT': '#441800', # Dark Brown
# 'DT': '#230D00' # Dark Brown
}
try:
return standards[color_string.upper()]
except KeyError:
return ''

Wyświetl plik

@ -0,0 +1,24 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from inkex import BaseElement
def prepare_tartan_fill_element(element: BaseElement) -> None:
"""Prepares an svg element to be rendered as a tartan_fill embroidery element
:param element: svg element with a fill color (path, rectangle, or circle)
"""
parent_group = element.getparent()
if parent_group.get_id().startswith('inkstitch-tartan'):
# apply tartan group transform to element
transform = element.transform @ parent_group.transform
element.set('transform', transform)
# remove tartan group and place element in parent group
outer_group = parent_group.getparent()
outer_group.insert(outer_group.index(parent_group), element)
outer_group.remove(parent_group)
# make sure the element is invisible
element.style['display'] = 'inline'

Wyświetl plik

@ -0,0 +1,243 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
# Additional credits to: https://github.com/clsn/pyTartan
import re
from typing import List
import wx
from inkex import Color
from .colors import string_to_color
class Palette:
"""Holds information about the tartan palette"""
def __init__(
self,
palette_code: str = '',
palette_stripes: List[list] = [[], []],
symmetry: bool = True,
equal_warp_weft: bool = True,
tt_unit: float = 0.5
) -> None:
"""
:param palette_code: the palette code
:param palette_stripes: the palette stripes, lists of warp and weft stripe dictionaries
:param symmetry: reflective sett (True) / repeating sett (False)
:param equal_warp_weft:wether warp and weft are equal or not
:param tt_unit: mm per thread (used for the scottish register threadcount)
"""
self.palette_code = palette_code
self.palette_stripes = palette_stripes
self.symmetry = symmetry
self.equal_warp_weft = equal_warp_weft
self.tt_unit = tt_unit
def __repr__(self) -> str:
return self.palette_code
def update_symmetry(self, symmetry: bool) -> None:
self.symmetry = symmetry
self.update_code()
def update_from_stripe_sizer(self, sizers: List[wx.BoxSizer], symmetry: bool = True, equal_warp_weft: bool = True) -> None:
"""
Update palette code from stripes (customize panel)
:param sizers: a list of the stripe sizers
:param symmetry: reflective sett (True) / repeating sett (False)
:param equal_warp_weft: wether warp and weft are equal or not
"""
self.symmetry = symmetry
self.equal_warp_weft = equal_warp_weft
self.palette_stripes = [[], []]
for i, outer_sizer in enumerate(sizers):
stripes = []
for stripe_sizer in outer_sizer.Children:
stripe = {'render': True, 'color': '#000000', 'width': '5'}
stripe_info = stripe_sizer.GetSizer()
for color in stripe_info.GetChildren():
widget = color.GetWindow()
if isinstance(widget, wx.CheckBox):
# in embroidery it is ok to have gaps between the stripes
if not widget.GetValue():
stripe['render'] = False
elif isinstance(widget, wx.ColourPickerCtrl):
stripe['color'] = widget.GetColour().GetAsString(wx.C2S_HTML_SYNTAX)
elif isinstance(widget, wx.SpinCtrlDouble):
stripe['width'] = widget.GetValue()
elif isinstance(widget, wx.Button) or isinstance(widget, wx.StaticText):
continue
stripes.append(stripe)
self.palette_stripes[i] = stripes
if self.equal_warp_weft:
self.palette_stripes[1] = stripes
break
self.update_code()
def update_from_code(self, code: str) -> None:
"""
Update stripes (customize panel) according to the code applied by the user
Converts code to valid Ink/Stitch code
:param code: the tartan pattern code to apply
"""
self.symmetry = True
if '...' in code:
self.symmetry = False
self.equal_warp_weft = True
if '|' in code:
self.equal_warp_weft = False
code = code.replace('/', '')
code = code.replace('...', '')
self.palette_stripes = [[], []]
if "Threadcount" in code:
self.parse_threadcount_code(code)
elif '(' in code:
self.parse_inkstitch_code(code)
else:
self.parse_simple_code(code)
if self.equal_warp_weft:
self.palette_stripes[1] = self.palette_stripes[0]
self.update_code()
def update_code(self) -> None:
"""Updates the palette code, reading from stripe settings (customize panel)"""
code = []
for i, direction in enumerate(self.palette_stripes):
for stripe in direction:
render = '' if stripe['render'] else '?'
code.append(f"({stripe['color']}){render}{stripe['width']}")
if i == 0 and self.equal_warp_weft is False:
code.append("|")
else:
break
if self.symmetry and len(code) > 0:
code[0] = code[0].replace(')', ')/')
code[-1] = code[-1].replace(')', ')/')
code_str = ' '.join(code)
if not self.symmetry:
code_str = f'...{code}...'
self.palette_code = code_str
def parse_simple_code(self, code: str) -> None:
"""Example code:
B24 W4 B24 R2 K24 G24 W2
Each letter stands for a color defined in .colors.py (if not recognized, defaults to black)
The number indicates the threadcount (width) of the stripe
The width of one thread is user defined
:param code: the tartan pattern code to apply
"""
stripes = []
stripe_info = re.findall(r'([a-zA-Z]+)(\?)?([0-9.]*)', code)
for color, render, width in stripe_info:
if not width:
continue
color = string_to_color(color)
width = float(width) * self.tt_unit
if not color:
color = '#000000'
render = '?'
stripes.append({'render': not bool(render), 'color': color, 'width': float(width)})
self.palette_stripes[0] = stripes
def parse_inkstitch_code(self, code_str: str) -> None:
"""Example code:
(#0000FF)/2.4 (#FFFFFF)0.4 (#0000FF)2.4 (#FF0000)0.2 (#000000)2.4 (#006400)2.4 (#FFFFFF)/0.2
| = separator warp and weft (if not equal)
/ = indicates a symmetric sett
... = indicates an asymmetric sett
:param code_str: the tartan pattern code to apply
"""
code = code_str.split('|')
for i, direction in enumerate(code):
stripes = []
stripe_info = re.findall(r'\(([0-9A-Za-z#]+)\)(\?)?([0-9.]+)', direction)
for color, render, width in stripe_info:
try:
# on macOS we need to run wxpython color method inside the app otherwise
# the color picker has issues in some cases to accept our input
color = wx.Colour(color).GetAsString(wx.C2S_HTML_SYNTAX)
except wx.PyNoAppError:
# however when we render an embroidery element we do not want to open wx.App
color = str(Color(color).to_named())
if not color:
color = '#000000'
render = False
stripes.append({'render': not bool(render), 'color': color, 'width': float(width)})
self.palette_stripes[i] = stripes
def parse_threadcount_code(self, code: str) -> None:
"""Read in and work directly from a tartanregister.gov.uk threadcount response
Example code:
Threadcount:
B24W4B24R2K24G24W2
Palette:
B=0000FFBLUE;W=FFFFFFWHITE;R=FF0000RED;K=000000BLACK;G=289C18GREEN;
Threadcount given over a half sett with full count at the pivots.
Colors in the threadcount are defined by Letters. The Palette section declares the rgb value
:param code: the tartan pattern code to apply
"""
if 'full sett' in code:
self.symmetry = False
else:
self.symmetry = True
colors = []
thread_code = ''
stripes = []
lines = code.splitlines()
i = 0
while i < len(lines):
line = lines[i].strip()
if 'Threadcount:' in line and len(lines) > i:
thread_code = lines[i+1]
elif line.startswith('Palette:'):
palette = lines[i+1]
colors = re.findall(r'([A-Za-z]+)=#?([0-9afA-F]{6})', palette)
color_dict = dict(colors)
i += 1
stripe_info = re.findall(r'([a-zA-Z]+)([0-9.]*)', thread_code)
for color, width in stripe_info:
render = True
try:
color = f'#{color_dict[color]}'
except KeyError:
color = '#000000'
render = False
width = float(width) * self.tt_unit
stripes.append({'render': render, 'color': color, 'width': width})
self.palette_stripes[0] = stripes
def get_palette_width(self, scale: int, min_width: float, direction: int = 0) -> float:
"""
Get the rendered width of the tartan palette
:param scale: the scale value (percent) for the pattern
:param min_width: min stripe width (before it is rendered as running stitch).
Smaller stripes have 0 width.
:param direction: 0 (warp) or 1 (weft)
:returns: the width of all tartan stripes in given direction
"""
width = 0
for stripe in self.palette_stripes[direction]:
stripe_width = stripe['width'] * (scale / 100)
if stripe_width >= min_width or not stripe['render']:
width += stripe_width
return width

592
lib/tartan/svg.py 100644
Wyświetl plik

@ -0,0 +1,592 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import time
from collections import defaultdict
from copy import copy
from itertools import chain
from typing import List, Optional, Tuple
from inkex import BaseElement, Group, Path, PathElement
from networkx import MultiGraph, is_empty
from shapely import (LineString, MultiLineString, MultiPolygon, Point, Polygon,
dwithin, minimum_bounding_radius, reverse)
from shapely.affinity import scale
from shapely.ops import linemerge, substring
from ..commands import add_commands
from ..elements import FillStitch
from ..stitches.auto_fill import (PathEdge, build_fill_stitch_graph,
build_travel_graph, find_stitch_path,
graph_make_valid, which_outline)
from ..svg import PIXELS_PER_MM, get_correction_transform
from ..utils import DotDict, ensure_multi_line_string
from .palette import Palette
from .utils import sort_fills_and_strokes, stripes_to_shapes
class TartanSvgGroup:
"""Generates the tartan pattern for svg element tartans"""
def __init__(self, settings: DotDict) -> None:
"""
:param settings: the tartan settings
"""
self.rotate = settings['rotate']
self.scale = settings['scale']
self.offset_x = settings['offset_x'] * PIXELS_PER_MM
self.offset_y = settings['offset_y'] * PIXELS_PER_MM
self.output = settings['output']
self.stitch_type = settings['stitch_type']
self.row_spacing = settings['row_spacing']
self.angle_warp = settings['angle_warp']
self.angle_weft = settings['angle_weft']
self.min_stripe_width = settings['min_stripe_width']
self.bean_stitch_repeats = settings['bean_stitch_repeats']
self.palette = Palette()
self.palette.update_from_code(settings['palette'])
self.symmetry = self.palette.symmetry
self.stripes = self.palette.palette_stripes
self.warp, self.weft = self.stripes
if self.palette.get_palette_width(self.scale, self.min_stripe_width) == 0:
self.warp = []
if self.palette.get_palette_width(self.scale, self.min_stripe_width, 1) == 0:
self.weft = []
if self.palette.equal_warp_weft:
self.weft = self.warp
def __repr__(self) -> str:
return f'TartanPattern({self.rotate}, {self.scale}, ({self.offset_x}, {self.offset_y}), {self.symmetry}, {self.warp}, {self.weft})'
def generate(self, outline: BaseElement) -> Group:
"""
Generates a svg group which holds svg elements to represent the tartan pattern
:param outline: the outline to be filled with the tartan pattern
"""
parent_group = outline.getparent()
if parent_group.get_id().startswith('inkstitch-tartan'):
# remove everything but the tartan outline
for child in parent_group.iterchildren():
if child != outline:
parent_group.remove(child)
group = parent_group
else:
group = Group()
group.set('id', f'inkstitch-tartan-{int(time.time())}')
parent_group.append(group)
outline_shape = FillStitch(outline).shape
transform = get_correction_transform(outline)
dimensions, rotation_center = self._get_dimensions(outline_shape)
warp = stripes_to_shapes(
self.warp,
dimensions,
outline_shape,
self.rotate,
rotation_center,
self.symmetry,
self.scale,
self.min_stripe_width
)
warp_routing_lines = self._get_routing_lines(warp)
warp = self._route_shapes(warp_routing_lines, outline_shape, warp)
warp = self._shapes_to_elements(warp, warp_routing_lines, transform)
weft = stripes_to_shapes(
self.weft,
dimensions,
outline_shape,
self.rotate,
rotation_center,
self.symmetry,
self.scale,
self.min_stripe_width,
True
)
weft_routing_lines = self._get_routing_lines(weft)
weft = self._route_shapes(weft_routing_lines, outline_shape, weft, True)
weft = self._shapes_to_elements(weft, weft_routing_lines, transform, True)
fills, strokes = self._combine_shapes(warp, weft, outline_shape)
fills, strokes = sort_fills_and_strokes(fills, strokes)
for color, fill_elements in fills.items():
for element in fill_elements:
group.append(element)
if self.stitch_type == "auto_fill":
self._add_command(element)
else:
element.pop('inkstitch:start')
element.pop('inkstitch:end')
for color, stroke_elements in strokes.items():
for element in stroke_elements:
group.append(element)
# set outline invisible
outline.style['display'] = 'none'
group.append(outline)
return group
def _get_command_position(self, fill: FillStitch, point: Tuple[float, float]) -> Point:
"""
Shift command position out of the element shape
:param fill: the fill element to which to attach the command
:param point: position where the command should point to
"""
dimensions, center = self._get_dimensions(fill.shape)
line = LineString([center, point])
fact = 20 / line.length
line = scale(line, xfact=1+fact, yfact=1+fact, origin=center)
pos = line.coords[-1]
return Point(pos)
def _add_command(self, element: BaseElement) -> None:
"""
Add a command to given svg element
:param element: svg element to which to attach the command
"""
if not element.style('fill'):
return
fill = FillStitch(element)
if fill.shape.is_empty:
return
start = element.get('inkstitch:start')
end = element.get('inkstitch:end')
if start:
start = start[1:-1].split(',')
add_commands(fill, ['fill_start'], self._get_command_position(fill, (float(start[0]), float(start[1]))))
element.pop('inkstitch:start')
if end:
end = end[1:-1].split(',')
add_commands(fill, ['fill_end'], self._get_command_position(fill, (float(end[0]), float(end[1]))))
element.pop('inkstitch:end')
def _route_shapes(self, routing_lines: defaultdict, outline_shape: MultiPolygon, shapes: defaultdict, weft: bool = False) -> defaultdict:
"""
Route polygons and linestrings
:param routing_lines: diagonal lines representing the tartan stripes used for routing
:param outline_shape: the shape to be filled with the tartan pattern
:param shapes: the tartan shapes (stripes)
:param weft: wether to render warp or weft oriented stripes
"""
routed = defaultdict(list)
for color, lines in routing_lines.items():
routed_polygons = self._get_routed_shapes('polygon', shapes[color][0], lines[0], outline_shape, weft)
routed_linestrings = self._get_routed_shapes('linestring', None, lines[1], outline_shape, weft)
routed[color] = [routed_polygons, routed_linestrings]
return routed
def _get_routed_shapes(
self,
geometry_type: str,
polygons: Optional[List[Polygon]],
lines: Optional[List[LineString]],
outline_shape: MultiPolygon,
weft: bool
):
"""
Find path for given elements
:param geometry_type: wether to route 'polygon' or 'linestring'
:param polygons: list of polygons to route
:param lines: list of lines to route (for polygon routing these are the routing lines)
:param outline_shape: the shape to be filled with the tartan pattern
:param weft: wether to route warp or weft oriented stripes
:returns: a list of routed elements
"""
if not lines:
return []
if weft:
starting_point = lines[-1].coords[-1]
ending_point = lines[0].coords[0]
else:
starting_point = lines[0].coords[0]
ending_point = lines[-1].coords[-1]
segments = [list(line.coords) for line in lines if line.length > 5]
fill_stitch_graph = build_fill_stitch_graph(outline_shape, segments, starting_point, ending_point)
if is_empty(fill_stitch_graph):
return []
graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, outline_shape, 0, False)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
return self._path_to_shapes(path, fill_stitch_graph, polygons, geometry_type, outline_shape)
def _path_to_shapes(
self,
path: List[PathEdge],
fill_stitch_graph: MultiGraph,
polygons: Optional[List[Polygon]],
geometry_type: str,
outline_shape: MultiPolygon
) -> list:
"""
Return elements in given order (by path) and add strokes for travel between elements
:param path: routed PathEdges
:param fill_stitch_graph: the stitch graph
:param polygons: the polygon shapes (if not LineStrings)
:param geometry_type: wether to render 'polygon' or 'linestring' segments
:param outline_shape: the shape to be filkled with the tartan pattern
:returns: a list of routed shape elements
"""
outline = MultiLineString()
travel_linestring = LineString()
routed_shapes = []
start_distance = 0
for edge in path:
start, end = edge
if edge.is_segment():
if not edge.key == 'segment':
# networkx fixed the shape for us, we do not really want to insert the element twice
continue
if not travel_linestring.is_empty:
# insert edge run before segment
travel_linestring = self._get_shortest_travel(start, outline, travel_linestring)
if travel_linestring.geom_type == "LineString":
routed_shapes.append(travel_linestring)
travel_linestring = LineString()
routed = self._edge_segment_to_element(edge, geometry_type, fill_stitch_graph, polygons)
routed_shapes.extend(routed)
elif routed_shapes:
# prepare edge run between segments
if travel_linestring.is_empty:
outline_index = which_outline(outline_shape, start)
outline = ensure_multi_line_string(outline_shape.boundary).geoms[outline_index]
start_distance = outline.project(Point(start))
travel_linestring = self._get_travel(start, end, outline)
else:
end_distance = outline.project(Point(end))
travel_linestring = substring(outline, start_distance, end_distance)
return routed_shapes
def _edge_segment_to_element(
self,
edge: PathEdge,
geometry_type: str,
fill_stitch_graph: MultiGraph,
polygons: Optional[List[Polygon]]
) -> list:
"""
Turns an edge back into an element
:param edge: edge with start and end point information
:param geometry_type: wether to convert a 'polygon' or 'linestring'
:param fill_stitch_graph: the stitch graph
:param polygons: list of polygons if geom_type is 'poylgon'
:returns: a list of routed elements.
Polygons are wrapped in dictionaries to preserve information about start and end point.
"""
start, end = edge
routed = []
if geometry_type == 'polygon' and polygons is not None:
polygon = self._find_polygon(polygons, Point(start))
if polygon:
routed.append({'shape': polygon, 'start': start, 'end': end})
elif geometry_type == 'linestring':
try:
line = fill_stitch_graph[start][end]['segment'].get('geometry')
except KeyError:
line = LineString([start, end])
if not line.is_empty:
if start != tuple(line.coords[0]):
line = line.reverse()
if line:
routed.append(line)
return routed
@staticmethod
def _get_shortest_travel(start: Tuple[float, float], outline: LineString, travel_linestring: LineString) -> LineString:
"""
Replace travel_linestring with a shorter travel line if possible
:param start: travel starting point
:param outline: the part of the outline which is nearest to the starting point
:param travel_linestring: predefined travel which will be replaced if it is longer
"""
if outline.length / 2 < travel_linestring.length:
short_travel = outline.difference(travel_linestring)
if short_travel.geom_type == "MultiLineString":
short_travel = linemerge(short_travel)
if short_travel.geom_type == "LineString":
if Point(short_travel.coords[-1]).distance(Point(start)) > Point(short_travel.coords[0]).distance(Point(start)):
short_travel = reverse(short_travel)
return short_travel
return travel_linestring
@staticmethod
def _find_polygon(polygons: List[Polygon], point: Tuple[float, float]) -> Optional[Polygon]:
"""
Find the polygon for a given point
:param polygons: a list of polygons to chose from
:param point: the point to match a polygon to
:returns: a matching polygon or None if no polygon could be found
"""
for polygon in polygons:
if dwithin(point, polygon, 0.01):
return polygon
return None
@staticmethod
def _get_routing_lines(shapes: defaultdict) -> defaultdict:
"""
Generate routing lines for given polygon shapes
:param shapes: polygon shapes grouped by color
:returns: color grouped dictionary with lines which can be used for routing
"""
routing_lines = defaultdict(list)
for color, elements in shapes.items():
routed: list = [[], []]
for polygon in elements[0]:
bounding_coords = polygon.minimum_rotated_rectangle.exterior.coords
routing_line = LineString([bounding_coords[0], bounding_coords[2]])
routing_line = ensure_multi_line_string(routing_line.intersection(polygon)).geoms
routed[0].append(LineString([routing_line[0].coords[0], routing_line[-1].coords[-1]]))
routed[1].extend(elements[1])
routing_lines[color] = routed
return routing_lines
def _shapes_to_elements(self, shapes: defaultdict, routed_lines: defaultdict, transform: str, weft=False) -> defaultdict:
"""
Generates svg elements from given shapes
:param shapes: lists of shapes grouped by color
:param routed_lines: lists of routed lines grouped by color
:param transform: correction transform to apply to the elements
:param weft: wether to render warp or weft oriented stripes
:returns: lists of svg elements grouped by color
"""
shapes_copy = copy(shapes)
for color, shape in shapes_copy.items():
elements: list = [[], []]
polygons, linestrings = shape
for polygon in polygons:
if isinstance(polygon, dict):
path_element = self._polygon_to_path(color, polygon['shape'], weft, transform, polygon['start'], polygon['end'])
if self.stitch_type == 'legacy_fill':
polygon_start = Point(polygon['start'])
path_element = self._adapt_legacy_fill_params(path_element, polygon_start)
elements[0].append(path_element)
elif polygon.geom_type == "Polygon":
elements[0].append(self._polygon_to_path(color, polygon, weft, transform))
else:
elements[0].append(self._linestring_to_path(color, polygon, transform, True))
for line in linestrings:
segment = line.difference(MultiLineString(routed_lines[color][1])).is_empty
if segment:
linestring = self._linestring_to_path(color, line, transform)
else:
linestring = self._linestring_to_path(color, line, transform, True)
elements[1].append(linestring)
shapes[color] = elements
return shapes
@staticmethod
def _adapt_legacy_fill_params(path_element: PathElement, start: Point) -> PathElement:
"""
Find best legacy fill param setting
Flip and reverse so that the fill starts as near as possible to the starting point
:param path_element: a legacy fill svg path element
:param start: the starting point
:returns: the adapted path element
"""
if not FillStitch(path_element).to_stitch_groups(None):
return path_element
blank = Point(FillStitch(path_element).to_stitch_groups(None)[0].stitches[0])
path_element.set('inkstitch:reverse', True)
reverse = Point(FillStitch(path_element).to_stitch_groups(None)[0].stitches[0])
path_element.set('inkstitch:flip', True)
reverse_flip = Point(FillStitch(path_element).to_stitch_groups(None)[0].stitches[0])
path_element.pop('inkstitch:revers')
flip = Point(FillStitch(path_element).to_stitch_groups(None)[0].stitches[0])
start_positions = [blank.distance(start), reverse.distance(start), reverse_flip.distance(start), flip.distance(start)]
best_setting = start_positions.index(min(start_positions))
if best_setting == 0:
path_element.set('inkstitch:reverse', False)
path_element.set('inkstitch:flip', False)
elif best_setting == 1:
path_element.set('inkstitch:reverse', True)
path_element.set('inkstitch:flip', False)
elif best_setting == 2:
path_element.set('inkstitch:reverse', True)
path_element.set('inkstitch:flip', True)
elif best_setting == 3:
path_element.set('inkstitch:reverse', False)
path_element.set('inkstitch:flip', True)
return path_element
def _combine_shapes(self, warp: defaultdict, weft: defaultdict, outline: MultiPolygon) -> Tuple[defaultdict, defaultdict]:
"""
Combine warp and weft elements into color groups, but separated into polygons and linestrings
:param warp: dictionary with warp polygons and linestrings grouped by color
:param weft: dictionary with weft polygons and linestrings grouped by color
:returns: a dictionary with polygons and a dictionary with linestrings each grouped by color
"""
polygons: defaultdict = defaultdict(list)
linestrings: defaultdict = defaultdict(list)
for color, shapes in chain(warp.items(), weft.items()):
start = None
end = None
if shapes[0]:
if polygons[color]:
start = polygons[color][-1].get('inkstitch:end')
end = shapes[0][0].get('inkstitch:start')
if start and end:
start = start[1:-1].split(',')
end = end[1:-1].split(',')
first_outline = ensure_multi_line_string(outline.boundary).geoms[0]
travel = self._get_travel(start, end, first_outline)
travel_path_element = self._linestring_to_path(color, travel, shapes[0][0].get('transform', ''), True)
polygons[color].append(travel_path_element)
polygons[color].extend(shapes[0])
if shapes[1]:
if linestrings[color]:
start = tuple(list(linestrings[color][-1].get_path().end_points)[-1])
elif polygons[color]:
start = polygons[color][-1].get('inkstitch:end')
if start:
start = start[1:-1].split(',')
end = tuple(list(shapes[1][0].get_path().end_points)[0])
if start and end:
first_outline = ensure_multi_line_string(outline.boundary).geoms[0]
travel = self._get_travel(start, end, first_outline)
travel_path_element = self._linestring_to_path(color, travel, shapes[1][0].get('transform', ''), True)
linestrings[color].append(travel_path_element)
linestrings[color].extend(shapes[1])
return polygons, linestrings
@staticmethod
def _get_travel(start: Tuple[float, float], end: Tuple[float, float], outline: LineString) -> LineString:
"""
Returns a travel line from start point to end point along the outline
:param start: starting point
:param end: ending point
:param outline: the outline
:returns: a travel LineString from start to end along the outline
"""
start_distance = outline.project(Point(start))
end_distance = outline.project(Point(end))
return substring(outline, start_distance, end_distance)
def _get_dimensions(self, outline: MultiPolygon) -> Tuple[Tuple[float, float, float, float], Point]:
"""
Calculates the dimensions for the tartan pattern.
Make sure it is big enough for pattern rotations.
:param outline: the shape to be filled with a tartan pattern
:returns: [0] a list with boundaries and [1] the center point (for rotations)
"""
bounds = outline.bounds
minx, miny, maxx, maxy = bounds
minx -= self.offset_x
miny -= self.offset_y
center = LineString([(bounds[0], bounds[1]), (bounds[2], bounds[3])]).centroid
if self.rotate != 0:
# add as much space as necessary to perform a rotation without producing gaps
min_radius = minimum_bounding_radius(outline)
minx = center.x - min_radius
miny = center.y - min_radius
maxx = center.x + min_radius
maxy = center.y + min_radius
return (float(minx), float(miny), float(maxx), float(maxy)), center
def _polygon_to_path(
self,
color: str,
polygon: Polygon,
weft: bool,
transform: str,
start: Optional[Tuple[float, float]] = None,
end: Optional[Tuple[float, float]] = None
) -> Optional[PathElement]:
"""
Convert a polygon to an svg path element
:param color: hex color
:param polygon: the polygon to convert
:param weft: wether to render as warp or weft
:param transform: string of the transform to apply to the element
:param start: start position for routing
:param end: end position for routing
:returns: an svg path element or None if the polygon is empty
"""
path = Path(list(polygon.exterior.coords))
path.close()
if path is None:
return None
for interior in polygon.interiors:
interior_path = Path(list(interior.coords))
interior_path.close()
path += interior_path
path_element = PathElement(
attrib={'d': str(path)},
style=f'fill:{color};fill-opacity:0.6;',
transform=transform
)
if self.stitch_type == 'legacy_fill':
path_element.set('inkstitch:fill_method', 'legacy_fill')
elif self.stitch_type == 'auto_fill':
path_element.set('inkstitch:fill_method', 'auto_fill')
path_element.set('inkstitch:underpath', False)
path_element.set('inkstitch:fill_underlay', False)
path_element.set('inkstitch:row_spacing_mm', self.row_spacing)
if weft:
angle = self.angle_weft - self.rotate
path_element.set('inkstitch:angle', angle)
else:
angle = self.angle_warp - self.rotate
path_element.set('inkstitch:angle', angle)
if start is not None:
path_element.set('inkstitch:start', str(start))
if end is not None:
path_element.set('inkstitch:end', str(end))
return path_element
def _linestring_to_path(self, color: str, line: LineString, transform: str, travel: bool = False):
"""
Convert a linestring to an svg path element
:param color: hex color
:param line: the line to convert
:param transform: string of the transform to apply to the element
:param travel: wether to render as travel line or running stitch/bean stitch
:returns: an svg path element or None if the linestring path is empty
"""
path = str(Path(list(line.coords)))
if not path:
return
path_element = PathElement(
attrib={'d': path},
style=f'fill:none;stroke:{color};stroke-opacity:0.6;',
transform=transform
)
if not travel and self.bean_stitch_repeats > 0:
path_element.set('inkstitch:bean_stitch_repeats', self.bean_stitch_repeats)
return path_element

262
lib/tartan/utils.py 100644
Wyświetl plik

@ -0,0 +1,262 @@
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import json
from collections import defaultdict
from copy import copy
from typing import List, Tuple, Union
from inkex import BaseElement
from shapely import LineString, MultiPolygon, Point, Polygon, unary_union
from shapely.affinity import rotate
from ..svg import PIXELS_PER_MM
from ..svg.tags import INKSTITCH_TARTAN
from ..utils import ensure_multi_line_string, ensure_multi_polygon
from .palette import Palette
def stripes_to_shapes(
stripes: List[dict],
dimensions: Tuple[float, float, float, float],
outline: Union[MultiPolygon, Polygon],
rotation: float,
rotation_center: Point,
symmetry: bool,
scale: int,
min_stripe_width: float,
weft: bool = False,
intersect_outline: bool = True
) -> defaultdict:
"""
Convert tartan stripes to polygons and linestrings (depending on stripe width) sorted by color
:param stripes: a list of dictionaries with stripe information
:param dimensions: the dimension to fill with the tartan pattern (minx, miny, maxx, maxy)
:param outline: the shape to fill with the tartan pattern
:param rotation: the angle to rotate the pattern
:param rotation_center: the center point for rotation
:param symmetry: reflective sett (True) / repeating sett (False)
:param scale: the scale value (percent) for the pattern
:param min_stripe_width: min stripe width before it is rendered as running stitch
:param weft: wether to render warp or weft oriented stripes
:param intersect_outline: wether or not cut the shapes to fit into the outline
:returns: a dictionary with shapes grouped by color
"""
minx, miny, maxx, maxy = dimensions
shapes: defaultdict = defaultdict(list)
original_stripes = stripes
if len(original_stripes) == 0:
return shapes
left = minx
top = miny
i = -1
while True:
i += 1
stripes = original_stripes
segments = stripes
if symmetry and i % 2 != 0 and len(stripes) > 1:
segments = list(reversed(stripes[1:-1]))
for stripe in segments:
width = stripe['width'] * PIXELS_PER_MM * (scale / 100)
right = left + width
bottom = top + width
if (top > maxy and weft) or (left > maxx and not weft):
return _merge_polygons(shapes, outline, intersect_outline)
if not stripe['render']:
left = right
top = bottom
continue
shape_dimensions = [top, bottom, left, right, minx, miny, maxx, maxy]
if width <= min_stripe_width * PIXELS_PER_MM:
linestrings = _get_linestrings(outline, shape_dimensions, rotation, rotation_center, weft)
shapes[stripe['color']].extend(linestrings)
continue
polygon = _get_polygon(shape_dimensions, rotation, rotation_center, weft)
shapes[stripe['color']].append(polygon)
left = right
top = bottom
def _merge_polygons(
shapes: defaultdict,
outline: Union[MultiPolygon, Polygon],
intersect_outline: bool = True
) -> defaultdict:
"""
Merge polygons which are bordering each other (they most probably used a running stitch in between)
:param shapes: shapes grouped by color
:param outline: the shape to be filled with a tartan pattern
:intersect_outline: wether to return an intersection of the shapes with the outline or not
:returns: the shapes with merged polygons
"""
shapes_copy = copy(shapes)
for color, shape_group in shapes_copy.items():
polygons: List[Polygon] = []
lines: List[LineString] = []
for shape in shape_group:
if not shape.intersects(outline):
continue
if shape.geom_type == "Polygon":
polygons.append(shape)
else:
lines.append(shape)
merged_polygons = unary_union(polygons)
merged_polygons = merged_polygons.simplify(0.01)
if intersect_outline:
merged_polygons = merged_polygons.intersection(outline)
merged_polygons = ensure_multi_polygon(merged_polygons)
shapes[color] = [list(merged_polygons.geoms), lines]
return shapes
def _get_polygon(dimensions: List[float], rotation: float, rotation_center: Point, weft: bool) -> Polygon:
"""
Generates a rotated polygon with the given dimensions
:param dimensions: top, bottom, left, right, minx, miny, maxx, maxy
:param rotation: the angle to rotate the pattern
:param rotation_center: the center point for rotation
:param weft: wether to render warp or weft oriented stripes
:returns: the generated Polygon
"""
top, bottom, left, right, minx, miny, maxx, maxy = dimensions
if not weft:
polygon = Polygon([(left, miny), (right, miny), (right, maxy), (left, maxy)])
else:
polygon = Polygon([(minx, top), (maxx, top), (maxx, bottom), (minx, bottom)])
if rotation != 0:
polygon = rotate(polygon, rotation, rotation_center)
return polygon
def _get_linestrings(
outline: Union[MultiPolygon, Polygon],
dimensions: List[float],
rotation: float,
rotation_center: Point, weft: bool
) -> list:
"""
Generates a rotated linestrings with the given dimension (outline intersection)
:param outline: the outline to be filled with the tartan pattern
:param dimensions: top, bottom, left, right, minx, miny, maxx, maxy
:param rotation: the angle to rotate the pattern
:param rotation_center: the center point for rotation
:param weft: wether to render warp or weft oriented stripes
:returns: a list of the generated linestrings
"""
top, bottom, left, right, minx, miny, maxx, maxy = dimensions
linestrings = []
if weft:
linestring = LineString([(minx, top), (maxx, top)])
else:
linestring = LineString([(left, miny), (left, maxy)])
if rotation != 0:
linestring = rotate(linestring, rotation, rotation_center)
intersection = linestring.intersection(outline)
if not intersection.is_empty:
linestrings.extend(ensure_multi_line_string(intersection).geoms)
return linestrings
def sort_fills_and_strokes(fills: defaultdict, strokes: defaultdict) -> Tuple[defaultdict, defaultdict]:
"""
Lines should be stitched out last, so they won't be covered by following fill elements.
However, if we find lines of the same color as one of the polygon groups, we can make
sure that they stitch next to each other to reduce color changes by at least one.
:param fills: fills grouped by color
:param strokes: strokes grouped by color
:returns: the sorted fills and strokes
"""
colors_to_connect = [color for color in fills.keys() if color in strokes]
if colors_to_connect:
color_to_connect = colors_to_connect[-1]
last = fills[color_to_connect]
fills.pop(color_to_connect)
fills[color_to_connect] = last
sorted_strokes = defaultdict(list)
sorted_strokes[color_to_connect] = strokes[color_to_connect]
strokes.pop(color_to_connect)
sorted_strokes.update(strokes)
strokes = sorted_strokes
return fills, strokes
def get_tartan_settings(node: BaseElement) -> dict:
"""
Parse tartan settings from node inkstich:tartan attribute
:param node: the tartan svg element
:returns: the tartan settings in a dictionary
"""
settings = node.get(INKSTITCH_TARTAN, None)
if settings is None:
settings = {
'palette': '(#101010)/5.0 (#FFFFFF)/?5.0',
'rotate': 0.0,
'offset_x': 0.0,
'offset_y': 0.0,
'symmetry': True,
'scale': 100,
'min_stripe_width': 1.0
}
return settings
return json.loads(settings)
def get_palette_width(settings: dict, direction: int = 0) -> float:
"""
Calculate the width of all stripes (with a minimum width) in given direction
:param settings: tartan settings
:param direction: [0] warp [1] weft
:returns: the calculated palette width
"""
palette_code = settings['palette']
palette = Palette()
palette.update_from_code(palette_code)
return palette.get_palette_width(settings['scale'], settings['min_stripe_width'], direction)
def get_tartan_stripes(settings: dict) -> Tuple[list, list]:
"""
Get tartan stripes
:param settings: tartan settings
:returns: a list with warp stripe dictionaries and a list with weft stripe dictionaries
Lists are empty if total width is 0 (for example if there are only strokes)
"""
# get stripes, return empty lists if total width is 0
palette_code = settings['palette']
palette = Palette()
palette.update_from_code(palette_code)
warp, weft = palette.palette_stripes
if palette.get_palette_width(settings['scale'], settings['min_stripe_width']) == 0:
warp = []
if palette.get_palette_width(settings['scale'], settings['min_stripe_width'], 1) == 0:
weft = []
if len([stripe for stripe in warp if stripe['render'] is True]) == 0:
warp = []
if len([stripe for stripe in weft if stripe['render'] is True]) == 0:
weft = []
if palette.equal_warp_weft:
weft = warp
return warp, weft

Wyświetl plik

@ -1,6 +1,9 @@
from shapely.geometry import LineString, Point as ShapelyPoint, MultiPolygon
from shapely.geometry import LineString, MultiPolygon
from shapely.geometry import Point as ShapelyPoint
from shapely.prepared import prep
from .geometry import Point, ensure_geometry_collection
from .geometry import (Point, ensure_geometry_collection,
ensure_multi_line_string)
def path_to_segments(path):
@ -122,7 +125,7 @@ def clamp_path_to_polygon(path, polygon):
if not exit_point.intersects(entry_point):
# Now break the border into pieces using those points.
border = find_border(polygon, exit_point)
border_pieces = border.difference(MultiPolygon((entry_point, exit_point))).geoms
border_pieces = ensure_multi_line_string(border.difference(MultiPolygon((entry_point, exit_point)))).geoms
border_pieces = fix_starting_point(border_pieces)
# Pick the shortest way to get from the exiting to the

Wyświetl plik

@ -8,7 +8,7 @@ import typing
import numpy
from shapely.geometry import (GeometryCollection, LinearRing, LineString,
MultiLineString, MultiPolygon)
MultiLineString, MultiPoint, MultiPolygon)
from shapely.geometry import Point as ShapelyPoint
@ -159,6 +159,26 @@ def ensure_multi_polygon(thing, min_size=0):
return multi_polygon
def ensure_multi_point(thing):
"""Given a shapely geometry, return a MultiPoint"""
multi_point = MultiPoint()
if thing.is_empty:
return multi_point
if thing.geom_type == "MultiPoint":
return thing
elif thing.geom_type == "Point":
return MultiPoint([thing])
elif thing.geom_type == "GeometryCollection":
points = []
for shape in thing.geoms:
if shape.geom_type == "Point":
points.append(shape)
elif shape.geom_type == "MultiPoint":
points.extend(shape.geoms)
return MultiPoint(points)
return multi_point
def cut_path(points, length):
"""Return a subsection of at the start of the path that is length units long.

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>Tartan</name>
<id>org.{{ id_inkstitch }}.tartan</id>
<param name="extension" type="string" gui-hidden="true">tartan</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="{{ menu_inkstitch }}" translatable="no">
<submenu name="Tools: Fill" />
</submenu>
</effects-menu>
</effect>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>