kopia lustrzana https://github.com/inkstitch/inkstitch
Various fixes (#3028)
* several thread palette extension fixes * fix svg tartan when original shape is invalid * tartan stroke spaces * style * fix tartan color substituion at pattern start * ripple: do not render too small paths * use less space for params warning headline * fix clone shape path * zip export template fix (typo) * add realistic stitch plan output warning (help tab)pull/3033/head dev-build-claudine-a_few_tartan_fonts_correction
rodzic
7c06dee44a
commit
e52886a64a
|
@ -195,7 +195,7 @@ class Clone(EmbroideryElement):
|
|||
transform = Transform(self.node.composed_transform())
|
||||
path = path.transform(transform)
|
||||
path = path.to_superpath()
|
||||
return MultiLineString(path)
|
||||
return MultiLineString(path[0])
|
||||
|
||||
def center(self, source_node):
|
||||
transform = get_node_transform(self.node.getparent())
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# Copyright (c) 2024 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from ..elements import FillStitch
|
||||
from ..elements import Clone, FillStitch
|
||||
from ..threads import ThreadCatalog, ThreadColor
|
||||
from .base import InkstitchExtension
|
||||
|
||||
|
@ -29,6 +29,16 @@ class ApplyPalette(InkstitchExtension):
|
|||
|
||||
# Iterate through the color blocks to apply colors
|
||||
for element in self.elements:
|
||||
if isinstance(element, Clone):
|
||||
# clones use the color of their source element
|
||||
continue
|
||||
elif hasattr(element, 'gradient') and element.gradient is not None:
|
||||
# apply colors to each gradient stop
|
||||
for i, gradient_style in enumerate(element.gradient.stop_styles):
|
||||
color = gradient_style['stop-color']
|
||||
gradient_style['stop-color'] = palette.nearest_color(ThreadColor(color)).to_hex_str()
|
||||
continue
|
||||
|
||||
nearest_color = palette.nearest_color(ThreadColor(element.color))
|
||||
if isinstance(element, FillStitch):
|
||||
element.node.style['fill'] = nearest_color.to_hex_str()
|
||||
|
|
|
@ -62,7 +62,10 @@ class GeneratePalette(InkstitchExtension):
|
|||
def _get_color_from_elements(self, elements):
|
||||
colors = []
|
||||
for element in elements:
|
||||
if 'fill' not in element.style.keys() or not isinstance(element, inkex.TextElement): # type(element) != inkex.TextElement:
|
||||
if element.TAG == 'g':
|
||||
colors.extend(self._get_color_from_elements(element.getchildren()))
|
||||
continue
|
||||
if 'fill' not in element.style.keys() or not isinstance(element, inkex.TextElement):
|
||||
continue
|
||||
|
||||
color = inkex.Color(element.style['fill']).to_rgb()
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
# Copyright (c) 2022 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import inkex
|
||||
|
||||
from ..i18n import _
|
||||
from .base import InkstitchExtension
|
||||
from .utils.inkex_command import inkscape
|
||||
|
||||
|
||||
class PaletteSplitText(InkstitchExtension):
|
||||
|
@ -30,7 +33,14 @@ class PaletteSplitText(InkstitchExtension):
|
|||
transform = text.transform
|
||||
text.pop('transform')
|
||||
|
||||
bbox = text.get_inkscape_bbox()
|
||||
# the inkex command `bbox = text.get_inkscape_bbox()` is causing problems for our pyinstaller bundled
|
||||
# releases, this code block is taken from inkex/elements/_text
|
||||
with TemporaryDirectory(prefix="inkscape-command") as tmpdir:
|
||||
svg_file = inkex.command.write_svg(text.root, tmpdir, "input.svg")
|
||||
bbox = inkscape(svg_file, "-X", "-Y", "-W", "-H", query_id=text.get_id())
|
||||
bbox = list(map(text.root.viewport_to_unit, bbox.splitlines()))
|
||||
bbox = inkex.BoundingBox.new_xywh(*bbox[1:])
|
||||
|
||||
x = bbox.left
|
||||
y = bbox.bottom
|
||||
height = bbox.height / (len(lines))
|
||||
|
|
|
@ -8,6 +8,7 @@ import os
|
|||
import inkex
|
||||
|
||||
from ..i18n import _
|
||||
from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
|
||||
from ..threads.palette import ThreadPalette
|
||||
from .base import InkstitchExtension
|
||||
|
||||
|
@ -31,11 +32,15 @@ class PaletteToText(InkstitchExtension):
|
|||
inkex.errormsg(_("Cannot read palette: invalid GIMP palette header"))
|
||||
return
|
||||
|
||||
current_layer = self.svg.get_current_layer()
|
||||
layer = inkex.Group(attrib={
|
||||
'id': '__inkstitch_palette__',
|
||||
INKSCAPE_LABEL: _('Thread Palette') + f': {thread_palette.name}',
|
||||
INKSCAPE_GROUPMODE: 'layer',
|
||||
})
|
||||
self.svg.append(layer)
|
||||
|
||||
x = 0
|
||||
y = 0
|
||||
pos = 0
|
||||
for color in thread_palette:
|
||||
line = "%s %s" % (color.name, color.number)
|
||||
element = inkex.TextElement()
|
||||
|
@ -43,10 +48,9 @@ class PaletteToText(InkstitchExtension):
|
|||
element.style = "fill:%s;font-size:4px;" % color.to_hex_str()
|
||||
element.set('x', x)
|
||||
element.set('y', str(y))
|
||||
current_layer.insert(pos, element)
|
||||
layer.append(element)
|
||||
|
||||
y = float(y) + 5
|
||||
pos += 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -171,7 +171,7 @@ class SatinPattern:
|
|||
|
||||
def get_path(self, add_rungs, min_width, max_width, length, to_unit):
|
||||
# scale the pattern path to fit the unit of the current svg
|
||||
scale_factor = scale_factor = 1 / inkex.units.convert_unit('1mm', f'{to_unit}')
|
||||
scale_factor = 1 / inkex.units.convert_unit('1mm', f'{to_unit}')
|
||||
pattern_path = inkex.Path(self.path).transform(inkex.Transform(f'scale({scale_factor})'), True)
|
||||
|
||||
# create a path element
|
||||
|
|
|
@ -28,11 +28,8 @@ class Tartan(InkstitchExtension):
|
|||
|
||||
def get_tartan_elements(self):
|
||||
if self.svg.selection:
|
||||
self._get_elements()
|
||||
|
||||
def _get_elements(self):
|
||||
for node in self.svg.selection:
|
||||
self.get_selection(node)
|
||||
for node in self.svg.selection:
|
||||
self.get_selection(node)
|
||||
|
||||
def get_selection(self, node):
|
||||
if node.TAG == 'g' and not node.get_id().startswith('inkstitch-tartan'):
|
||||
|
@ -40,13 +37,13 @@ class Tartan(InkstitchExtension):
|
|||
self.get_selection(child_node)
|
||||
else:
|
||||
node = self.get_outline(node)
|
||||
if node.tag in EMBROIDERABLE_TAGS and node.style('fill'):
|
||||
if node.tag in EMBROIDERABLE_TAGS and node.style('fill') is not None:
|
||||
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):
|
||||
if node.get(INKSTITCH_TARTAN, None) is not None:
|
||||
return node
|
||||
if node.get_id().startswith('inkstitch-tartan'):
|
||||
for element in node.iterchildren(EMBROIDERABLE_TAGS):
|
||||
|
@ -55,7 +52,7 @@ class Tartan(InkstitchExtension):
|
|||
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):
|
||||
if element.get(INKSTITCH_TARTAN, None) is not None:
|
||||
return element
|
||||
# if we don't find an existing tartan, return node
|
||||
return node
|
||||
|
|
|
@ -20,7 +20,7 @@ class WarningPanel(wx.Panel):
|
|||
self.warning = wx.StaticText(self)
|
||||
self.warning.SetLabel(_("An error occurred while rendering the stitch plan:"))
|
||||
self.warning.SetForegroundColour(wx.Colour(255, 25, 25))
|
||||
self.main_sizer.Add(self.warning, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
|
||||
self.main_sizer.Add(self.warning, 0, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
|
||||
|
||||
tc_style = wx.TE_MULTILINE | wx.TE_READONLY | wx.VSCROLL | wx.TE_RICH2
|
||||
self.warning_text = wx.TextCtrl(self, size=(300, 300), style=tc_style)
|
||||
|
|
|
@ -24,6 +24,9 @@ def ripple_stitch(stroke):
|
|||
If more sublines are present interpolation will take place between the first two.
|
||||
'''
|
||||
|
||||
if stroke.as_multi_line_string().length < 0.1:
|
||||
return []
|
||||
|
||||
is_linear, helper_lines = _get_helper_lines(stroke)
|
||||
|
||||
num_lines = len(helper_lines[0])
|
||||
|
|
|
@ -24,7 +24,7 @@ from ..stitches.auto_fill import (PathEdge, build_fill_stitch_graph,
|
|||
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
|
||||
from .utils import sort_fills_and_strokes, stripes_to_shapes, get_palette_width
|
||||
|
||||
|
||||
class TartanSvgGroup:
|
||||
|
@ -51,6 +51,8 @@ class TartanSvgGroup:
|
|||
self.symmetry = self.palette.symmetry
|
||||
self.stripes = self.palette.palette_stripes
|
||||
self.warp, self.weft = self.stripes
|
||||
self.warp_width = get_palette_width(settings)
|
||||
self.weft_width = get_palette_width(settings)
|
||||
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:
|
||||
|
@ -80,8 +82,16 @@ class TartanSvgGroup:
|
|||
index = parent_group.index(outline)
|
||||
parent_group.insert(index, group)
|
||||
|
||||
outline_shape = FillStitch(outline).shape
|
||||
transform = get_correction_transform(outline)
|
||||
outline_shapes = FillStitch(outline).shape
|
||||
for outline_shape in outline_shapes.geoms:
|
||||
self._generate_tartan_group_elements(group, outline_shape, transform)
|
||||
|
||||
# set outline invisible
|
||||
outline.style['display'] = 'none'
|
||||
group.append(outline)
|
||||
|
||||
def _generate_tartan_group_elements(self, group, outline_shape, transform):
|
||||
dimensions, rotation_center = self._get_dimensions(outline_shape)
|
||||
|
||||
warp = stripes_to_shapes(
|
||||
|
@ -129,11 +139,6 @@ class TartanSvgGroup:
|
|||
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
|
||||
|
@ -492,6 +497,8 @@ class TartanSvgGroup:
|
|||
"""
|
||||
Calculates the dimensions for the tartan pattern.
|
||||
Make sure it is big enough for pattern rotations.
|
||||
We also need additional space to ensure fill stripes go to their full extend, this might be problematic if
|
||||
start or end stripes use render mode 2 (stroke spacing).
|
||||
|
||||
:param outline: the shape to be filled with a tartan pattern
|
||||
:returns: [0] a list with boundaries and [1] the center point (for rotations)
|
||||
|
@ -509,6 +516,13 @@ class TartanSvgGroup:
|
|||
miny = center.y - min_radius
|
||||
maxx = center.x + min_radius
|
||||
maxy = center.y + min_radius
|
||||
|
||||
extra_space = max(self.warp_width * PIXELS_PER_MM, self.weft_width * PIXELS_PER_MM)
|
||||
minx -= extra_space
|
||||
maxx += extra_space
|
||||
miny -= extra_space
|
||||
maxy += extra_space
|
||||
|
||||
return (float(minx), float(miny), float(maxx), float(maxy)), center
|
||||
|
||||
def _polygon_to_path(
|
||||
|
|
|
@ -46,63 +46,138 @@ def stripes_to_shapes(
|
|||
:returns: a dictionary with shapes grouped by color
|
||||
"""
|
||||
|
||||
full_sett = _stripes_to_sett(stripes, symmetry, scale, min_stripe_width)
|
||||
|
||||
minx, miny, maxx, maxy = dimensions
|
||||
shapes: defaultdict = defaultdict(list)
|
||||
|
||||
original_stripes = stripes
|
||||
if len(original_stripes) == 0:
|
||||
if len(full_sett) == 0:
|
||||
return shapes
|
||||
|
||||
left = minx
|
||||
top = miny
|
||||
add_to_stroke = 0
|
||||
add_to_fill = 0
|
||||
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)
|
||||
for stripe in full_sett:
|
||||
width = stripe['width']
|
||||
right = left + width
|
||||
bottom = top + width
|
||||
|
||||
if ((top > maxy and weft) or (left > maxx and not weft) or
|
||||
(add_to_stroke > maxy and weft) or (add_to_stroke > maxx and not weft)):
|
||||
if (top > maxy and weft) or (left > maxx and not weft):
|
||||
return _merge_polygons(shapes, outline, intersect_outline)
|
||||
|
||||
if stripe['render'] == 0:
|
||||
left = right + add_to_stroke
|
||||
top = bottom + add_to_stroke
|
||||
add_to_stroke = 0
|
||||
continue
|
||||
elif stripe['render'] == 2:
|
||||
add_to_stroke += width
|
||||
if stripe['color'] is None or 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:
|
||||
add_to_fill = add_to_stroke
|
||||
shape_dimensions[0] += add_to_stroke
|
||||
shape_dimensions[2] += add_to_stroke
|
||||
if stripe['is_stroke']:
|
||||
linestrings = _get_linestrings(outline, shape_dimensions, rotation, rotation_center, weft)
|
||||
shapes[stripe['color']].extend(linestrings)
|
||||
add_to_stroke += width
|
||||
continue
|
||||
add_to_stroke = 0
|
||||
else:
|
||||
polygon = _get_polygon(shape_dimensions, rotation, rotation_center, weft)
|
||||
shapes[stripe['color']].append(polygon)
|
||||
left = right
|
||||
top = bottom
|
||||
return shapes
|
||||
|
||||
# add the space of the lines to the following object to avoid bad symmetry
|
||||
shape_dimensions[1] += add_to_fill
|
||||
shape_dimensions[3] += add_to_fill
|
||||
|
||||
polygon = _get_polygon(shape_dimensions, rotation, rotation_center, weft)
|
||||
shapes[stripe['color']].append(polygon)
|
||||
left = right + add_to_fill
|
||||
top = bottom + add_to_fill
|
||||
add_to_fill = 0
|
||||
def _stripes_to_sett(
|
||||
stripes: List[dict],
|
||||
symmetry: bool,
|
||||
scale: int,
|
||||
min_stripe_width: float,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Builds a full sett for easier conversion into elements
|
||||
|
||||
:param stripes: a list of dictionaries with stripe information
|
||||
: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
|
||||
:returns: a list of dictionaries with stripe information (color, width, is_stroke, render)
|
||||
"""
|
||||
|
||||
last_fill_color = _get_last_fill_color(stripes, scale, min_stripe_width, symmetry)
|
||||
first_was_stroke = False
|
||||
last_was_stroke = False
|
||||
add_width = 0
|
||||
sett = []
|
||||
for stripe in stripes:
|
||||
width = stripe['width'] * PIXELS_PER_MM * (scale / 100)
|
||||
is_stroke = width <= min_stripe_width * PIXELS_PER_MM
|
||||
render = stripe['render']
|
||||
|
||||
if render == 0:
|
||||
sett.append({'color': None, 'width': width + add_width, 'is_stroke': False, 'render': False})
|
||||
last_fill_color = None
|
||||
add_width = 0
|
||||
last_was_stroke = False
|
||||
continue
|
||||
|
||||
if render == 2:
|
||||
sett.append({'color': last_fill_color, 'width': width + add_width, 'is_stroke': False, 'render': True})
|
||||
add_width = 0
|
||||
last_was_stroke = False
|
||||
continue
|
||||
|
||||
if is_stroke:
|
||||
if len(sett) == 0:
|
||||
first_was_stroke = True
|
||||
width /= 2
|
||||
sett.append({'color': last_fill_color, 'width': width + add_width, 'is_stroke': False, 'render': True})
|
||||
sett.append({'color': stripe['color'], 'width': 0, 'is_stroke': True, 'render': True})
|
||||
add_width = width
|
||||
last_was_stroke = True
|
||||
else:
|
||||
sett.append({'color': stripe['color'], 'width': width + add_width, 'is_stroke': False, 'render': True})
|
||||
last_fill_color = stripe['color']
|
||||
last_was_stroke = False
|
||||
|
||||
if add_width > 0:
|
||||
sett.append({'color': last_fill_color, 'width': add_width, 'is_stroke': False, 'render': True})
|
||||
|
||||
# For symmetric setts we want to mirror the sett and append to receive a full sett
|
||||
# We do not repeat at pivot points, which means we exclude the first and the last list item from the mirror
|
||||
if symmetry:
|
||||
reversed_sett = list(reversed(sett[1:-1]))
|
||||
if first_was_stroke:
|
||||
reversed_sett = reversed_sett[:-1]
|
||||
if last_was_stroke:
|
||||
reversed_sett = reversed_sett[1:]
|
||||
sett.extend(reversed_sett)
|
||||
|
||||
return sett
|
||||
|
||||
|
||||
def _get_last_fill_color(stripes: List[dict], scale: int, min_stripe_width: float, symmetry: bool,) -> List[dict]:
|
||||
'''
|
||||
Returns the first fill color of a pattern to substitute spaces if the pattern starts with strokes or
|
||||
stripes with render mode 2
|
||||
|
||||
:param stripes: a list of dictionaries with stripe information
|
||||
:param scale: the scale value (percent) for the pattern
|
||||
:param min_stripe_width: min stripe width before it is rendered as running stitch
|
||||
:param symmetry: reflective sett (True) / repeating sett (False)
|
||||
:returns: a list with fill colors or a list with one None item if there are no fills
|
||||
'''
|
||||
fill_colors = []
|
||||
for stripe in stripes:
|
||||
if stripe['render'] == 0:
|
||||
fill_colors.append(None)
|
||||
elif stripe['render'] == 2:
|
||||
continue
|
||||
elif stripe['width'] * (scale / 100) > min_stripe_width:
|
||||
fill_colors.append(stripe['color'])
|
||||
if len(fill_colors) == 0:
|
||||
fill_colors = [None]
|
||||
|
||||
if symmetry:
|
||||
return fill_colors[0]
|
||||
else:
|
||||
return fill_colors[-1]
|
||||
|
||||
|
||||
def _merge_polygons(
|
||||
|
|
|
@ -37,6 +37,12 @@
|
|||
<page name="info" gui-text="Help">
|
||||
<label>Use this extension to render the stitch plan into the canvas.</label>
|
||||
<spacer />
|
||||
<label>
|
||||
Please use realistic output options with care. Realistic render outputs can cause Inkscape to slow massivley down.
|
||||
Designs with a lot of stitches shouldn't be rendered with vector, while the vector option can be prefered if the design
|
||||
is huge. For huge designs with a lot of stitches consider to render in chunks or choose simple as the render mode.
|
||||
</label>
|
||||
<spacer />
|
||||
<label>More information on our website</label>
|
||||
<label appearance="url">https://inkstitch.org/docs/visualize/#stitch-plan-preview</label>
|
||||
</page>
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
{%- endif %}
|
||||
{%- endfor %}
|
||||
<param name="format-threadlist" type="boolean" gui-text=".TXT: Threadlist [COLOR]">false</param>
|
||||
<param name="format-png_realistic" type="boolean" gui-text=".PNG: Portable Network Graphics (Realistic) [COLOR]">false</param>
|
||||
<param name="format-png_simple" type="boolean" gui-text=".PNG: Portable Network Graphics (Simple) [COLOR]">false</param>
|
||||
<param name="format-png_realistic" type="boolean" gui-text=".PNG: Portable Network Graphics (Realistic) [IMAGE]">false</param>
|
||||
<param name="format-png_simple" type="boolean" gui-text=".PNG: Portable Network Graphics (Simple) [IMAGE]">false</param>
|
||||
<param name="png_simple_line_width" type="float" precision="2" min="0.01" max="5" gui-text="Line width (mm)" indent="4">0.3</param>
|
||||
<param name="format-svg" type="boolean" gui-text=".SVG: Scalable Vector Graphic">false</param>
|
||||
<param name="extension" type="string" gui-hidden="true">zip</param>
|
||||
|
@ -39,7 +39,7 @@
|
|||
<label appearance="header">Zip File Export</label>
|
||||
<label>Export multiple embroidery file formats at once.</label>
|
||||
<separator />
|
||||
<label>Read more on our webiste:</label>
|
||||
<label>Read more on our website:</label>
|
||||
<label appearance="url">https://inkstitch.org/docs/import-export/#batch-export</label>
|
||||
</page>
|
||||
</param>
|
||||
|
|
Ładowanie…
Reference in New Issue