* guide line position

* use direction from line to shape

* optimize intersection detection

* fix flapack elf

* handle weird guide lines better

* update starting point for self crossing (multiple) fills

* ripple: fixes and non circular join style

* avoid jumps in ripple stitch

* fallback only necessary if shape does not intersect grating

* make valid may return a polygon

* add profiling

* Stitch.__init__ didn't work right and was super slow

* shrink or grow to multipolygon

Co-authored-by: Lex Neva
pull/1718/head
Kaalleen 2022-06-30 19:22:33 +02:00 zatwierdzone przez GitHub
rodzic 725281f075
commit 8d5ef5b663
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 236 dodań i 98 usunięć

Wyświetl plik

@ -75,7 +75,7 @@ elif [ "$BUILD" = "linux" ]; then
# error: # error:
# #
# ELF load command address/offset not properly aligned # ELF load command address/offset not properly aligned
find dist/inkstitch -type f | grep -E '.so($|\.)' | grep -v _fblas | xargs strip find dist/inkstitch -type f | grep -E '\.so($|\.)' | grep -v _fblas | grep -v _flapack | xargs strip
else else
LD_LIBRARY_PATH="${site_packages}/wx" python -m PyInstaller $pyinstaller_args --strip inkstitch.py; LD_LIBRARY_PATH="${site_packages}/wx" python -m PyInstaller $pyinstaller_args --strip inkstitch.py;
fi fi

Wyświetl plik

@ -2,7 +2,8 @@
# #
# Copyright (c) 2010 Authors # Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import cProfile
import pstats
import logging import logging
import os import os
import sys import sys
@ -50,6 +51,11 @@ my_args, remaining_args = parser.parse_known_args()
if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "DEBUG")): if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "DEBUG")):
debug.enable() debug.enable()
profiler = None
if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "PROFILE")):
profiler = cProfile.Profile()
profiler.enable()
extension_name = my_args.extension extension_name = my_args.extension
# example: foo_bar_baz -> FooBarBaz # example: foo_bar_baz -> FooBarBaz
@ -58,8 +64,19 @@ extension_class_name = extension_name.title().replace("_", "")
extension_class = getattr(extensions, extension_class_name) extension_class = getattr(extensions, extension_class_name)
extension = extension_class() extension = extension_class()
if hasattr(sys, 'gettrace') and sys.gettrace(): if (hasattr(sys, 'gettrace') and sys.gettrace()) or profiler is not None:
extension.run(args=remaining_args) extension.run(args=remaining_args)
if profiler:
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "profile_stats")
profiler.disable()
profiler.dump_stats(path + ".prof")
with open(path, 'w') as stats_file:
stats = pstats.Stats(profiler, stream=stats_file)
stats.sort_stats(pstats.SortKey.CUMULATIVE)
stats.print_stats()
print(f"profiling stats written to {path} and {path}.prof", file=sys.stderr)
else: else:
save_stderr() save_stderr()
exception = None exception = None

Wyświetl plik

@ -266,6 +266,10 @@ class FillStitch(EmbroideryElement):
valid_shape = make_valid(shape) valid_shape = make_valid(shape)
logger.setLevel(level) logger.setLevel(level)
if isinstance(valid_shape, shgeo.Polygon):
return shgeo.MultiPolygon([valid_shape])
polygons = [] polygons = []
for polygon in valid_shape.geoms: for polygon in valid_shape.geoms:
if isinstance(polygon, shgeo.Polygon): if isinstance(polygon, shgeo.Polygon):
@ -499,15 +503,18 @@ class FillStitch(EmbroideryElement):
return self.get_boolean_param('underlay_underpath', True) return self.get_boolean_param('underlay_underpath', True)
def shrink_or_grow_shape(self, shape, amount, validate=False): def shrink_or_grow_shape(self, shape, amount, validate=False):
new_shape = shape
if amount: if amount:
shape = shape.buffer(amount) new_shape = shape.buffer(amount)
# changing the size can empty the shape # changing the size can empty the shape
# in this case we want to use the original shape rather than returning an error # in this case we want to use the original shape rather than returning an error
if shape.is_empty and not validate: if (new_shape.is_empty and not validate):
return shape new_shape = shape
if not isinstance(shape, shgeo.MultiPolygon):
shape = shgeo.MultiPolygon([shape]) if not isinstance(new_shape, shgeo.MultiPolygon):
return shape new_shape = shgeo.MultiPolygon([new_shape])
return new_shape
def underlay_shape(self, shape): def underlay_shape(self, shape):
return self.shrink_or_grow_shape(shape, -self.fill_underlay_inset) return self.shrink_or_grow_shape(shape, -self.fill_underlay_inset)
@ -532,26 +539,31 @@ class FillStitch(EmbroideryElement):
else: else:
return None return None
def to_stitch_groups(self, last_patch): def to_stitch_groups(self, last_patch): # noqa: C901
# backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False # backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False
if not self.auto_fill or self.fill_method == 3: if not self.auto_fill or self.fill_method == 3:
return self.do_legacy_fill() return self.do_legacy_fill()
else: else:
stitch_groups = [] stitch_groups = []
start = self.get_starting_point(last_patch)
end = self.get_ending_point() end = self.get_ending_point()
for shape in self.shape.geoms: for shape in self.shape.geoms:
start = self.get_starting_point(last_patch)
try: try:
if self.fill_underlay: if self.fill_underlay:
underlay_stitch_groups, start = self.do_underlay(shape, start) underlay_shapes = self.underlay_shape(shape)
stitch_groups.extend(underlay_stitch_groups) for underlay_shape in underlay_shapes.geoms:
if self.fill_method == 0: underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
stitch_groups.extend(self.do_auto_fill(shape, last_patch, start, end)) stitch_groups.extend(underlay_stitch_groups)
if self.fill_method == 1:
stitch_groups.extend(self.do_contour_fill(self.fill_shape(shape), last_patch, start)) fill_shapes = self.fill_shape(shape)
elif self.fill_method == 2: for fill_shape in fill_shapes.geoms:
stitch_groups.extend(self.do_guided_fill(shape, last_patch, start, end)) if self.fill_method == 0:
stitch_groups.extend(self.do_auto_fill(fill_shape, last_patch, start, end))
if self.fill_method == 1:
stitch_groups.extend(self.do_contour_fill(fill_shape, last_patch, start))
elif self.fill_method == 2:
stitch_groups.extend(self.do_guided_fill(fill_shape, last_patch, start, end))
except Exception: except Exception:
self.fatal_fill_error() self.fatal_fill_error()
last_patch = stitch_groups[-1] last_patch = stitch_groups[-1]
@ -576,7 +588,7 @@ class FillStitch(EmbroideryElement):
color=self.color, color=self.color,
tags=("auto_fill", "auto_fill_underlay"), tags=("auto_fill", "auto_fill_underlay"),
stitches=auto_fill( stitches=auto_fill(
self.underlay_shape(shape), shape,
self.fill_underlay_angle[i], self.fill_underlay_angle[i],
self.fill_underlay_row_spacing, self.fill_underlay_row_spacing,
self.fill_underlay_row_spacing, self.fill_underlay_row_spacing,
@ -597,7 +609,7 @@ class FillStitch(EmbroideryElement):
color=self.color, color=self.color,
tags=("auto_fill", "auto_fill_top"), tags=("auto_fill", "auto_fill_top"),
stitches=auto_fill( stitches=auto_fill(
self.fill_shape(shape), shape,
self.angle, self.angle,
self.row_spacing, self.row_spacing,
self.end_row_spacing, self.end_row_spacing,
@ -663,7 +675,7 @@ class FillStitch(EmbroideryElement):
color=self.color, color=self.color,
tags=("guided_fill", "auto_fill_top"), tags=("guided_fill", "auto_fill_top"),
stitches=guided_fill( stitches=guided_fill(
self.fill_shape(shape), shape,
guide_line.geoms[0], guide_line.geoms[0],
self.angle, self.angle,
self.row_spacing, self.row_spacing,

Wyświetl plik

@ -150,7 +150,7 @@ class Stroke(EmbroideryElement):
return max(self.get_int_param("line_count", 10), 1) return max(self.get_int_param("line_count", 10), 1)
def get_line_count(self): def get_line_count(self):
if self.is_closed: if self.is_closed or self.join_style == 1:
return self.line_count + 1 return self.line_count + 1
return self.line_count return self.line_count
@ -246,7 +246,7 @@ class Stroke(EmbroideryElement):
type='dropdown', type='dropdown',
default=0, default=0,
# 0: xy, 1: x, 2: y, 3: none # 0: xy, 1: x, 2: y, 3: none
options=[_("X Y"), _("X"), _("Y"), _("None")], options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 1)], select_items=[('stroke_method', 1)],
sort_index=12) sort_index=12)
def scale_axis(self): def scale_axis(self):
@ -286,6 +286,19 @@ class Stroke(EmbroideryElement):
def rotate_ripples(self): def rotate_ripples(self):
return self.get_boolean_param("rotate_ripples", True) return self.get_boolean_param("rotate_ripples", True)
@property
@param('join_style',
_('Join style'),
tooltip=_('Join style for non circular ripples.'),
type='dropdown',
default=0,
options=(_("flat"), _("point")),
select_items=[('stroke_method', 1)],
sort_index=16)
@cache
def join_style(self):
return self.get_int_param('join_style', 0)
@property @property
@cache @cache
def is_closed(self): def is_closed(self):

Wyświetl plik

@ -4,7 +4,6 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..utils.geometry import Point from ..utils.geometry import Point
from copy import deepcopy
class Stitch(Point): class Stitch(Point):
@ -12,10 +11,14 @@ class Stitch(Point):
def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False, def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False,
tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None): tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None):
base_stitch = None
if isinstance(x, Stitch): if isinstance(x, Stitch):
# Allow creating a Stitch from another Stitch. Attributes passed as # Allow creating a Stitch from another Stitch. Attributes passed as
# arguments will override any existing attributes. # arguments will override any existing attributes.
vars(self).update(deepcopy(vars(x))) base_stitch = x
self.x = base_stitch.x
self.y = base_stitch.y
elif isinstance(x, Point): elif isinstance(x, Point):
# Allow creating a Stitch from a Point # Allow creating a Stitch from a Point
point = x point = x
@ -24,17 +27,19 @@ class Stitch(Point):
else: else:
Point.__init__(self, x, y) Point.__init__(self, x, y)
self.color = color self._set('color', color, base_stitch)
self.jump = jump self._set('jump', jump, base_stitch)
self.trim = trim self._set('trim', trim, base_stitch)
self.stop = stop self._set('stop', stop, base_stitch)
self.color_change = color_change self._set('color_change', color_change, base_stitch)
self.force_lock_stitches = force_lock_stitches self._set('force_lock_stitches', force_lock_stitches, base_stitch)
self.tie_modus = tie_modus self._set('tie_modus', tie_modus, base_stitch)
self.no_ties = no_ties self._set('no_ties', no_ties, base_stitch)
self.tags = set()
self.tags = set()
self.add_tags(tags or []) self.add_tags(tags or [])
if base_stitch is not None:
self.add_tags(base_stitch.tags)
def __repr__(self): def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x, return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x,
@ -48,6 +53,14 @@ class Stitch(Point):
"NO TIES" if self.no_ties else " ", "NO TIES" if self.no_ties else " ",
"COLOR CHANGE" if self.color_change else " ") "COLOR CHANGE" if self.color_change else " ")
def _set(self, attribute, value, base_stitch):
# Set an attribute. If the caller passed a Stitch object, use its value, unless
# they overrode it with arguments.
if base_stitch is not None:
setattr(self, attribute, getattr(base_stitch, attribute))
if value or base_stitch is None:
setattr(self, attribute, value)
def add_tags(self, tags): def add_tags(self, tags):
for tag in tags: for tag in tags:
self.add_tag(tag) self.add_tag(tag)

Wyświetl plik

@ -59,14 +59,14 @@ def auto_fill(shape,
starting_point, starting_point,
ending_point=None, ending_point=None,
underpath=True): underpath=True):
try: rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) if not rows:
segments = [segment for row in rows for segment in row] # Small shapes may not intersect with the grating at all.
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
except ValueError:
# Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node
return fallback(shape, running_stitch_length, running_stitch_tolerance) 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)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length, running_stitch_tolerance) return fallback(shape, running_stitch_length, running_stitch_tolerance)

Wyświetl plik

@ -132,7 +132,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
start -= (start + normal * center) % row_spacing start -= (start + normal * center) % row_spacing
current_row_y = start current_row_y = start
rows = []
while current_row_y < end: while current_row_y < end:
p0 = center + normal * current_row_y + direction * half_length p0 = center + normal * current_row_y + direction * half_length
p1 = center + normal * current_row_y - direction * half_length p1 = center + normal * current_row_y - direction * half_length
@ -157,13 +157,15 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
runs.reverse() runs.reverse()
runs = [tuple(reversed(run)) for run in runs] runs = [tuple(reversed(run)) for run in runs]
yield runs rows.append(runs)
if end_row_spacing: if end_row_spacing:
current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height) current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height)
else: else:
current_row_y += row_spacing current_row_y += row_spacing
return rows
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last): def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last):
stitches = [] stitches = []

Wyświetl plik

@ -1,15 +1,20 @@
from math import atan2, copysign
from random import random
import numpy as np import numpy as np
import shapely.prepared
from shapely import geometry as shgeo from shapely import geometry as shgeo
from shapely.affinity import translate from shapely.affinity import translate
from shapely.ops import linemerge, unary_union from shapely.ops import linemerge, nearest_points, unary_union
from .auto_fill import (build_fill_stitch_graph,
build_travel_graph, collapse_sequential_outline_edges, fallback,
find_stitch_path, graph_is_valid, travel)
from ..debug import debug from ..debug import debug
from ..i18n import _
from ..stitch_plan import Stitch from ..stitch_plan import Stitch
from ..utils.geometry import Point as InkstitchPoint, ensure_geometry_collection, ensure_multi_line_string, reverse_line_string from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import (ensure_geometry_collection,
ensure_multi_line_string, reverse_line_string)
from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
collapse_sequential_outline_edges, find_stitch_path,
graph_is_valid, travel)
def guided_fill(shape, def guided_fill(shape,
@ -27,10 +32,15 @@ def guided_fill(shape,
strategy strategy
): ):
segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy) segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy)
if not segments:
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 = build_fill_stitch_graph(shape, segments, starting_point, ending_point) fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length, running_stitch_tolerance) return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
@ -39,6 +49,15 @@ def guided_fill(shape,
return result return result
def fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath):
# fall back to normal auto-fill with an angle that matches the guideline (sorta)
guide_start, guide_end = [guideline.coords[0], guideline.coords[-1]]
angle = atan2(guide_end[1] - guide_start[1], guide_end[0] - guide_start[0]) * -1
return auto_fill(shape, angle, row_spacing, None, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, running_stitch_tolerance, skip_last): def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, running_stitch_tolerance, skip_last):
path = collapse_sequential_outline_edges(path) path = collapse_sequential_outline_edges(path)
@ -75,22 +94,23 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, runni
def extend_line(line, shape): def extend_line(line, shape):
(minx, miny, maxx, maxy) = shape.bounds (minx, miny, maxx, maxy) = shape.bounds
line = line.simplify(0.01, False)
upper_left = InkstitchPoint(minx, miny) upper_left = InkstitchPoint(minx, miny)
lower_right = InkstitchPoint(maxx, maxy) lower_right = InkstitchPoint(maxx, maxy)
length = (upper_left - lower_right).length() length = (upper_left - lower_right).length()
point1 = InkstitchPoint(*line.coords[0]) start_point = InkstitchPoint.from_tuple(line.coords[0])
point2 = InkstitchPoint(*line.coords[1]) end_point = InkstitchPoint.from_tuple(line.coords[-1])
new_starting_point = point1 - (point2 - point1).unit() * length direction = (end_point - start_point).unit()
point3 = InkstitchPoint(*line.coords[-2]) new_start_point = start_point - direction * length
point4 = InkstitchPoint(*line.coords[-1]) new_end_point = end_point + direction * length
new_ending_point = point4 + (point4 - point3).unit() * length
return shgeo.LineString([new_starting_point.as_tuple()] + # without this, we seem especially likely to run into this libgeos bug:
line.coords[1:-1] + [new_ending_point.as_tuple()]) # https://github.com/shapely/shapely/issues/820
new_start_point += InkstitchPoint(random() * 0.01, random() * 0.01)
new_end_point += InkstitchPoint(random() * 0.01, random() * 0.01)
return shgeo.LineString((new_start_point, *line.coords, new_end_point))
def repair_multiple_parallel_offset_curves(multi_line): def repair_multiple_parallel_offset_curves(multi_line):
@ -114,8 +134,8 @@ def repair_non_simple_line(line):
repaired = unary_union(linemerge(line_segments)) repaired = unary_union(linemerge(line_segments))
counter += 1 counter += 1
if repaired.geom_type != 'LineString': if repaired.geom_type != 'LineString':
raise ValueError( # They gave us a line with complicated self-intersections. Use a fallback.
_("Guide line (or offset copy) is self crossing!")) return shgeo.LineString((line.coords[0], line.coords[-1]))
else: else:
return repaired return repaired
@ -158,7 +178,12 @@ def prepare_guide_line(line, shape):
if line.geom_type != 'LineString' or not line.is_simple: if line.geom_type != 'LineString' or not line.is_simple:
line = repair_non_simple_line(line) line = repair_non_simple_line(line)
# extend the line towards the ends to increase probability that all offsetted curves cross the shape if line.is_ring:
# If they pass us a ring, break it to avoid dividing by zero when
# calculating a unit vector from start to end.
line = shgeo.LineString(line.coords[:-2])
# extend the end points away from each other
line = extend_line(line, shape) line = extend_line(line, shape)
return line return line
@ -176,24 +201,41 @@ def clean_offset_line(offset_line):
return offset_line return offset_line
def _get_start_row(line, shape, row_spacing, line_direction):
if line.intersects(shape):
return 0
point1, point2 = nearest_points(line, shape.centroid)
distance = point1.distance(point2)
row = int(distance / row_spacing)
# This flips the sign of the starting row if the shape is on the other side
# of the guide line
shape_direction = InkstitchPoint.from_shapely_point(point2) - InkstitchPoint.from_shapely_point(point1)
return copysign(row, shape_direction * line_direction)
def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy): def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy):
debug.log_line_string(shape.exterior, "guided fill shape")
if strategy == 0:
translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
translate_direction = translate_direction.unit().rotate_left()
line = prepare_guide_line(line, shape) line = prepare_guide_line(line, shape)
row = 0 debug.log_line_string(shape.exterior, "guided fill shape")
translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
translate_direction = translate_direction.unit().rotate_left()
shape_envelope = shapely.prepared.prep(shape.convex_hull)
start_row = _get_start_row(line, shape, row_spacing, translate_direction)
row = start_row
direction = 1 direction = 1
offset_line = None offset_line = None
rows = []
while True: while True:
if strategy == 0: if strategy == 0:
translate_amount = translate_direction * row * direction * row_spacing translate_amount = translate_direction * row * row_spacing
offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y) offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y)
elif strategy == 1: elif strategy == 1:
offset_line = line.parallel_offset(row * row_spacing * direction, 'left', join_style=shgeo.JOIN_STYLE.bevel) offset_line = line.parallel_offset(row * row_spacing, 'left', join_style=shgeo.JOIN_STYLE.round)
offset_line = clean_offset_line(offset_line) offset_line = clean_offset_line(offset_line)
@ -201,18 +243,20 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
# negative parallel offsets are reversed, so we need to compensate # negative parallel offsets are reversed, so we need to compensate
offset_line = reverse_line_string(offset_line) offset_line = reverse_line_string(offset_line)
debug.log_line_string(offset_line, f"offset {row * direction}") debug.log_line_string(offset_line, f"offset {row}")
stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row * direction) stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row * direction)
intersection = shape.intersection(stitched_line) intersection = shape.intersection(stitched_line)
if intersection.is_empty: if shape_envelope.intersects(stitched_line):
for segment in take_only_line_strings(intersection).geoms:
rows.append(segment.coords[:])
row += direction
else:
if direction == 1: if direction == 1:
direction = -1 direction = -1
row = 1 row = start_row - 1
else: else:
break break
else:
for segment in take_only_line_strings(intersection).geoms: return rows
yield segment.coords[:]
row += 1

Wyświetl plik

@ -63,6 +63,8 @@ def _get_helper_lines(stroke):
if stroke.is_closed: if stroke.is_closed:
return False, _get_circular_ripple_helper_lines(stroke, outline) return False, _get_circular_ripple_helper_lines(stroke, outline)
elif stroke.join_style == 1:
return True, _get_point_style_linear_helper_lines(stroke, outline)
else: else:
return True, _get_linear_ripple_helper_lines(stroke, outline) return True, _get_linear_ripple_helper_lines(stroke, outline)
@ -74,7 +76,7 @@ def _get_satin_ripple_helper_lines(stroke):
# use satin column points for satin like build ripple stitches # use satin column points for satin like build ripple stitches
rail_points = SatinColumn(stroke.node).plot_points_on_rails(length, 0) rail_points = SatinColumn(stroke.node).plot_points_on_rails(length, 0)
steps = _get_steps(stroke.line_count, exponent=stroke.exponent, flip=stroke.flip_exponent) steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
helper_lines = [] helper_lines = []
for point0, point1 in zip(*rail_points): for point0, point1 in zip(*rail_points):
@ -83,24 +85,39 @@ def _get_satin_ripple_helper_lines(stroke):
for step in steps: for step in steps:
helper_lines[-1].append(InkstitchPoint.from_shapely_point(helper_line.interpolate(step, normalized=True))) helper_lines[-1].append(InkstitchPoint.from_shapely_point(helper_line.interpolate(step, normalized=True)))
if stroke.join_style == 1:
helper_lines = _converge_helper_line_points(helper_lines, True)
return helper_lines return helper_lines
def _get_circular_ripple_helper_lines(stroke, outline): def _converge_helper_line_points(helper_lines, point_edge=False):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
# Now we want to adjust the helper lines to make a spiral.
num_lines = len(helper_lines) num_lines = len(helper_lines)
steps = _get_steps(num_lines) steps = _get_steps(num_lines)
for i, line in enumerate(helper_lines): for i, line in enumerate(helper_lines):
points = [] points = []
for j in range(len(line) - 1): for j in range(len(line) - 1):
points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i]) if point_edge and j % 2 == 1:
k = num_lines - 1 - i
points.append(line[j] * (1 - steps[k]) + line[j + 1] * steps[k])
else:
points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i])
helper_lines[i] = points helper_lines[i] = points
return helper_lines return helper_lines
def _get_circular_ripple_helper_lines(stroke, outline):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
# Now we want to adjust the helper lines to make a spiral.
return _converge_helper_line_points(helper_lines)
def _get_point_style_linear_helper_lines(stroke, outline):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
return _converge_helper_line_points(helper_lines, True)
def _get_linear_ripple_helper_lines(stroke, outline): def _get_linear_ripple_helper_lines(stroke, outline):
guide_line = stroke.get_guide_line() guide_line = stroke.get_guide_line()
max_dist = stroke.grid_size or stroke.running_stitch_length max_dist = stroke.grid_size or stroke.running_stitch_length
@ -124,10 +141,25 @@ def _target_point_helper_lines(stroke, outline):
return helper_lines return helper_lines
def _adjust_helper_lines_for_grid(stroke, helper_lines):
num_lines = stroke.line_count - stroke.skip_end
if stroke.reverse:
helper_lines = [helper_line[::-1] for helper_line in helper_lines]
num_lines = stroke.skip_start
if (num_lines % 2 != 0 and not stroke.is_closed) or (stroke.is_closed and not stroke.reverse):
helper_lines.reverse()
return helper_lines
def _do_grid(stroke, helper_lines): def _do_grid(stroke, helper_lines):
helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines)
start = stroke.get_skip_start()
skip_end = stroke.get_skip_end()
if stroke.reverse:
start, skip_end = skip_end, start
for i, helper in enumerate(helper_lines): for i, helper in enumerate(helper_lines):
start = stroke.get_skip_start() end = len(helper) - skip_end
end = len(helper) - stroke.get_skip_end()
points = helper[start:end] points = helper[start:end]
if i % 2 == 0: if i % 2 == 0:
points.reverse() points.reverse()
@ -146,7 +178,7 @@ def _get_guided_helper_lines(stroke, outline, max_distance):
def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line): def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
# helper lines are generated by making copies of the outline alog the guide line # helper lines are generated by making copies of the outline along the guide line
line_point_dict = defaultdict(list) line_point_dict = defaultdict(list)
outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance)) outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))

Wyświetl plik

@ -64,17 +64,7 @@ inkstitch_attribs = [
'join_style', 'join_style',
'avoid_self_crossing', 'avoid_self_crossing',
'clockwise', 'clockwise',
'line_count',
'skip_start',
'skip_end',
'grid_size',
'reverse', 'reverse',
'exponent',
'flip_exponent',
'scale_axis',
'scale_start',
'scale_end',
'rotate_ripples',
'expand_mm', 'expand_mm',
'fill_underlay', 'fill_underlay',
'fill_underlay_angle', 'fill_underlay_angle',
@ -97,6 +87,17 @@ inkstitch_attribs = [
'repeats', 'repeats',
'running_stitch_length_mm', 'running_stitch_length_mm',
'running_stitch_tolerance_mm', 'running_stitch_tolerance_mm',
# ripples
'line_count',
'exponent',
'flip_exponent',
'skip_start',
'skip_end',
'scale_axis',
'scale_start',
'scale_end',
'rotate_ripples',
'grid_size',
# satin column # satin column
'satin_column', 'satin_column',
'short_stitch_distance_mm', 'short_stitch_distance_mm',

Wyświetl plik

@ -129,6 +129,10 @@ class Point:
def from_shapely_point(cls, point): def from_shapely_point(cls, point):
return cls(point.x, point.y) return cls(point.x, point.y)
@classmethod
def from_tuple(cls, point):
return cls(point[0], point[1])
def __json__(self): def __json__(self):
return vars(self) return vars(self)