inkstitch/lib/tartan/utils.py

280 wiersze
10 KiB
Python

# 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
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)
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)):
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
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
linestrings = _get_linestrings(outline, shape_dimensions, rotation, rotation_center, weft)
shapes[stripe['color']].extend(linestrings)
add_to_stroke += width
continue
add_to_stroke = 0
# 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 _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'] == 1]) == 0:
warp = []
if len([stripe for stripe in weft if stripe['render'] == 1]) == 0:
weft = []
if palette.equal_warp_weft:
weft = warp
return warp, weft