auto-fill pull compensation ()

pull/3006/head
Lex Neva 2024-06-20 10:45:53 -07:00 zatwierdzone przez GitHub
rodzic c017cae01a
commit e8017e0bcc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
7 zmienionych plików z 138 dodań i 41 usunięć

Wyświetl plik

@ -596,6 +596,37 @@ class FillStitch(EmbroideryElement):
def herringbone_width(self):
return self.get_float_param('herringbone_width_mm', 0)
@property
@param(
'pull_compensation_mm',
_('Pull compensation'),
tooltip=_('Fill stitch can pull the fabric together, resulting in a shape narrower than you draw in Inkscape. '
'This setting expands each row of stitches outward from the center of the row by a fixed length. '
'Two values separated by a space may be used for an asymmetric effect.'),
select_items=[('fill_method', 'auto_fill')],
unit=_('mm (each side)'),
type='float',
default=0,
sort_index=26)
@cache
def pull_compensation_px(self):
return np.maximum(self.get_split_mm_param_as_px("pull_compensation_mm", (0, 0)), 0)
@property
@param(
'pull_compensation_percent',
_('Pull compensation percentage'),
tooltip=_('Additional pull compensation which varies as a percentage of row width. '
'Two values separated by a space may be used for an asymmetric effect.'),
select_items=[('fill_method', 'auto_fill')],
unit=_('% (each side)'),
type='float',
default=0,
sort_index=27)
@cache
def pull_compensation_percent(self):
return np.maximum(self.get_split_float_param("pull_compensation_percent", (0, 0)), 0)
@property
def color(self):
# SVG spec says the default fill is black
@ -1027,6 +1058,8 @@ class FillStitch(EmbroideryElement):
self.enable_random_stitch_length,
self.random_stitch_length_jitter,
self.random_seed,
self.pull_compensation_px,
self.pull_compensation_percent / 100,
)
)
return [stitch_group]

Wyświetl plik

@ -20,7 +20,7 @@ from ..metadata import InkStitchMetadata
from ..stitch_plan import StitchGroup
from ..stitches import running_stitch
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, cut, cut_multiple, prng
from ..utils import Point, cache, cut, cut_multiple, offset_points, prng
from ..utils.param import ParamOption
from ..utils.threading import check_stop_flag
from .element import PIXELS_PER_MM, EmbroideryElement, param
@ -260,7 +260,7 @@ class SatinColumn(EmbroideryElement):
'pull_compensation_percent',
_('Pull compensation percentage'),
tooltip=_('Additional pull compensation which varies as a percentage of stitch width. '
'Two values separated by a space may be used for an aysmmetric effect.'),
'Two values separated by a space may be used for an asymmetric effect.'),
unit=_('% (each side)'),
type='float',
default=0,
@ -276,7 +276,7 @@ class SatinColumn(EmbroideryElement):
_('Pull compensation'),
tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. '
'This setting expands each pair of needle penetrations outward from the center of the satin column by a fixed length. '
'Two values separated by a space may be used for an aysmmetric effect.'),
'Two values separated by a space may be used for an asymmetric effect.'),
unit=_('mm (each side)'),
type='float',
default=0,
@ -1015,34 +1015,6 @@ class SatinColumn(EmbroideryElement):
center_walk = [center_walk[0], center_walk[0]]
return shgeo.LineString(center_walk)
def offset_points(self, pos1, pos2, offset_px, offset_proportional):
# Expand or contract two points about their midpoint. This is
# useful for pull compensation and insetting underlay.
distance = (pos1 - pos2).length()
if distance < 0.0001:
# if they're the same point, we don't know which direction
# to offset in, so we have to just return the points
return pos1, pos2
# calculate the offset for each side
offset_a = offset_px[0] + (distance * offset_proportional[0])
offset_b = offset_px[1] + (distance * offset_proportional[1])
offset_total = offset_a + offset_b
# don't contract beyond the midpoint, or we'll start expanding
if offset_total < -distance:
scale = -distance / offset_total
offset_a = offset_a * scale
offset_b = offset_b * scale
# convert offset to float before using because it may be a numpy.float64
out1 = pos1 + (pos1 - pos2).unit() * float(offset_a)
out2 = pos2 + (pos2 - pos1).unit() * float(offset_b)
return out1, out2
def _stitch_distance(self, pos0, pos1, previous_pos0, previous_pos1):
"""Return the distance from one stitch to the next."""
@ -1567,7 +1539,7 @@ class SatinColumn(EmbroideryElement):
offset_px[0] = -inset_px
if b.distance(pairs[i-1][1]) < min_dist:
offset_px[1] = -inset_px
shortened.append(self.offset_points(a, b, offset_px, (0, 0)))
shortened.append(offset_points(a, b, offset_px, (0, 0)))
return shortened
def _get_inset_point(self, point1, point2, distance_fraction):
@ -1656,7 +1628,7 @@ class SatinProcessor:
else:
offset_prop = self.offset_proportional
a, b = self.satin.offset_points(pos0, pos1, self.offset_px, offset_prop)
a, b = offset_points(pos0, pos1, self.offset_px, offset_prop)
return a, b
def get_stitch_spacing_multiple(self):

Wyświetl plik

@ -11,8 +11,8 @@ from typing import Iterator
import networkx
from shapely import geometry as shgeo
from shapely import segmentize
from shapely.ops import snap
from shapely import make_valid, segmentize
from shapely.ops import snap, unary_union
from shapely.strtree import STRtree
from ..debug.debug import debug
@ -22,7 +22,8 @@ from ..utils import cache
from ..utils.clamp_path import clamp_path_to_polygon
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import (ensure_multi_line_string,
line_string_to_point_list)
line_string_to_point_list, offset_points)
from ..utils.list import is_all_zeroes
from ..utils.prng import join_args
from ..utils.smoothing import smooth_path
from ..utils.threading import check_stop_flag
@ -82,12 +83,18 @@ def auto_fill(shape,
gap_fill_rows=0,
enable_random_stitch_length=False,
random_sigma=0.0,
random_seed=""):
random_seed="",
pull_compensation_px=(0, 0),
pull_compensation_percent=(0, 0)):
has_pull_compensation = not is_all_zeroes(pull_compensation_px) or not is_all_zeroes(pull_compensation_percent)
if has_pull_compensation:
spacing = min(row_spacing, end_row_spacing or row_spacing)
shape = adjust_shape_for_pull_compensation(shape, angle, spacing, pull_compensation_px, pull_compensation_percent)
rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
if not rows:
# Small shapes may not intersect with the grating at all.
return fallback(shape, running_stitch_length, running_stitch_tolerance)
segments = [segment for row in rows for segment in row]
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
@ -120,6 +127,34 @@ def round_to_multiple_of_2(number):
return number
@debug.time
def adjust_shape_for_pull_compensation(shape, angle, row_spacing, pull_compensation_px, pull_compensation_percent):
rows = intersect_region_with_grating(shape, angle, row_spacing)
if not rows:
return shape
segments = [segment for row in rows for segment in row]
segments = apply_pull_compensation(segments, pull_compensation_px, pull_compensation_percent)
lines = [shgeo.LineString((start, end)) for start, end in segments]
buffer_amount = row_spacing/2 + 0.01
buffered_lines = [line.buffer(buffer_amount) for line in lines]
polygon = unary_union(buffered_lines)
return make_valid(polygon)
def apply_pull_compensation(segments, pull_compensation_px, pull_compensation_percent):
new_segments = []
for segment in segments:
start = InkstitchPoint.from_tuple(segment[0])
end = InkstitchPoint.from_tuple(segment[1])
end = InkstitchPoint.from_tuple(segment[1])
new_start, new_end = offset_points(start, end, pull_compensation_px, pull_compensation_percent)
new_segments.append((new_start.as_tuple(), new_end.as_tuple()))
return new_segments
def which_outline(shape, coords):
"""return the index of the outline on which the point resides
@ -374,7 +409,6 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
calculation. We also weight the interior edges extra proportional to
how close they are to the boundary.
"""
graph = networkx.MultiGraph()
# Add all the nodes from the main graph. This will be all of the endpoints

Wyświetl plik

@ -113,10 +113,10 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
# Now get a unit vector rotated to the requested angle. I use -angle
# because shapely rotates clockwise, but my geometry textbooks taught
# me to consider angles as counter-clockwise from the X axis.
direction = InkstitchPoint(1, 0).rotate(-angle)
direction = InkstitchPoint(-1, 0).rotate(-angle)
# and get a normal vector
normal = direction.rotate(math.pi / 2)
normal = direction.rotate(-math.pi / 2)
# I'll start from the center, move in the normal direction some amount,
# and then walk left and right half_length in each direction to create

Wyświetl plik

@ -1,5 +1,6 @@
from shapely.geometry import LineString, MultiPolygon
from shapely.geometry import Point as ShapelyPoint
from shapely.ops import nearest_points
from shapely.prepared import prep
from .geometry import (Point, ensure_geometry_collection,
@ -14,6 +15,9 @@ def path_to_segments(path):
def segments_to_path(segments):
"""Convert a list of contiguous LineStrings into a list of Points."""
if not segments:
return []
coords = [segments[0].coords[0]]
for segment in segments:
@ -68,6 +72,22 @@ def find_border(polygon, point):
return polygon.exterior
def clamp_fully_external_path(path, polygon):
"""Clamp a path that lies entirely outside a polygon."""
start = ShapelyPoint(path[0])
end = ShapelyPoint(path[-1])
start_on_outline = nearest_points(start, polygon.exterior)[1].buffer(0.01, resolution=1)
end_on_outline = nearest_points(end, polygon.exterior)[1].buffer(0.01, resolution=1)
border_pieces = ensure_multi_line_string(polygon.exterior.difference(MultiPolygon((start_on_outline, end_on_outline)))).geoms
border_pieces = fix_starting_point(border_pieces)
shorter = min(border_pieces, key=lambda piece: piece.length)
return adjust_line_end(shorter, start)
def clamp_path_to_polygon(path, polygon):
"""Constrain a path to a Polygon.
@ -143,4 +163,7 @@ def clamp_path_to_polygon(path, polygon):
else:
was_inside = False
if not result:
return clamp_fully_external_path(path, polygon)
return segments_to_path(result)

Wyświetl plik

@ -197,6 +197,37 @@ def cut_path(points, length):
return [Point(*point) for point in subpath.coords]
def offset_points(pos1, pos2, offset_px, offset_proportional):
"""Expand or contract two points about their midpoint.
This is useful for pull compensation and insetting underlay.
"""
distance = (pos1 - pos2).length()
if distance < 0.0001:
# if they're the same point, we don't know which direction
# to offset in, so we have to just return the points
return pos1, pos2
# calculate the offset for each side
offset_a = offset_px[0] + (distance * offset_proportional[0])
offset_b = offset_px[1] + (distance * offset_proportional[1])
offset_total = offset_a + offset_b
# don't contract beyond the midpoint, or we'll start expanding
if offset_total < -distance:
scale = -distance / offset_total
offset_a = offset_a * scale
offset_b = offset_b * scale
# convert offset to float before using because it may be a numpy.float64
out1 = pos1 + (pos1 - pos2).unit() * float(offset_a)
out2 = pos2 + (pos2 - pos1).unit() * float(offset_b)
return out1, out2
class Point:
def __init__(self, x: typing.Union[float, numpy.float64], y: typing.Union[float, numpy.float64]):
self.x = float(x)

Wyświetl plik

@ -21,3 +21,7 @@ def poprandom(sequence, rng=_rng):
sequence[index] = last_item
return item
def is_all_zeroes(sequence):
return all(item == 0 for item in sequence)