kopia lustrzana https://github.com/inkstitch/inkstitch
Add "the tartan universe" (#2782)
rodzic
fb1ecd0bad
commit
2439adafa8
|
@ -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}")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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"))
|
||||
]
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
|
|
|
@ -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 ''
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>
|
Ładowanie…
Reference in New Issue