kopia lustrzana https://github.com/inkstitch/inkstitch
Fixes (#1703)
* 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 Nevapull/1718/head
rodzic
725281f075
commit
8d5ef5b663
|
@ -75,7 +75,7 @@ elif [ "$BUILD" = "linux" ]; then
|
|||
# error:
|
||||
#
|
||||
# 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
|
||||
LD_LIBRARY_PATH="${site_packages}/wx" python -m PyInstaller $pyinstaller_args --strip inkstitch.py;
|
||||
fi
|
||||
|
|
21
inkstitch.py
21
inkstitch.py
|
@ -2,7 +2,8 @@
|
|||
#
|
||||
# Copyright (c) 2010 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
import cProfile
|
||||
import pstats
|
||||
import logging
|
||||
import os
|
||||
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")):
|
||||
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
|
||||
|
||||
# example: foo_bar_baz -> FooBarBaz
|
||||
|
@ -58,8 +64,19 @@ extension_class_name = extension_name.title().replace("_", "")
|
|||
extension_class = getattr(extensions, extension_class_name)
|
||||
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)
|
||||
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:
|
||||
save_stderr()
|
||||
exception = None
|
||||
|
|
|
@ -266,6 +266,10 @@ class FillStitch(EmbroideryElement):
|
|||
valid_shape = make_valid(shape)
|
||||
|
||||
logger.setLevel(level)
|
||||
|
||||
if isinstance(valid_shape, shgeo.Polygon):
|
||||
return shgeo.MultiPolygon([valid_shape])
|
||||
|
||||
polygons = []
|
||||
for polygon in valid_shape.geoms:
|
||||
if isinstance(polygon, shgeo.Polygon):
|
||||
|
@ -499,15 +503,18 @@ class FillStitch(EmbroideryElement):
|
|||
return self.get_boolean_param('underlay_underpath', True)
|
||||
|
||||
def shrink_or_grow_shape(self, shape, amount, validate=False):
|
||||
new_shape = shape
|
||||
if amount:
|
||||
shape = shape.buffer(amount)
|
||||
new_shape = shape.buffer(amount)
|
||||
# changing the size can empty the shape
|
||||
# in this case we want to use the original shape rather than returning an error
|
||||
if shape.is_empty and not validate:
|
||||
return shape
|
||||
if not isinstance(shape, shgeo.MultiPolygon):
|
||||
shape = shgeo.MultiPolygon([shape])
|
||||
return shape
|
||||
if (new_shape.is_empty and not validate):
|
||||
new_shape = shape
|
||||
|
||||
if not isinstance(new_shape, shgeo.MultiPolygon):
|
||||
new_shape = shgeo.MultiPolygon([new_shape])
|
||||
|
||||
return new_shape
|
||||
|
||||
def underlay_shape(self, shape):
|
||||
return self.shrink_or_grow_shape(shape, -self.fill_underlay_inset)
|
||||
|
@ -532,26 +539,31 @@ class FillStitch(EmbroideryElement):
|
|||
else:
|
||||
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
|
||||
if not self.auto_fill or self.fill_method == 3:
|
||||
return self.do_legacy_fill()
|
||||
else:
|
||||
stitch_groups = []
|
||||
start = self.get_starting_point(last_patch)
|
||||
end = self.get_ending_point()
|
||||
|
||||
for shape in self.shape.geoms:
|
||||
start = self.get_starting_point(last_patch)
|
||||
try:
|
||||
if self.fill_underlay:
|
||||
underlay_stitch_groups, start = self.do_underlay(shape, start)
|
||||
underlay_shapes = self.underlay_shape(shape)
|
||||
for underlay_shape in underlay_shapes.geoms:
|
||||
underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
|
||||
stitch_groups.extend(underlay_stitch_groups)
|
||||
|
||||
fill_shapes = self.fill_shape(shape)
|
||||
for fill_shape in fill_shapes.geoms:
|
||||
if self.fill_method == 0:
|
||||
stitch_groups.extend(self.do_auto_fill(shape, last_patch, start, end))
|
||||
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(self.fill_shape(shape), last_patch, start))
|
||||
stitch_groups.extend(self.do_contour_fill(fill_shape, last_patch, start))
|
||||
elif self.fill_method == 2:
|
||||
stitch_groups.extend(self.do_guided_fill(shape, last_patch, start, end))
|
||||
stitch_groups.extend(self.do_guided_fill(fill_shape, last_patch, start, end))
|
||||
except Exception:
|
||||
self.fatal_fill_error()
|
||||
last_patch = stitch_groups[-1]
|
||||
|
@ -576,7 +588,7 @@ class FillStitch(EmbroideryElement):
|
|||
color=self.color,
|
||||
tags=("auto_fill", "auto_fill_underlay"),
|
||||
stitches=auto_fill(
|
||||
self.underlay_shape(shape),
|
||||
shape,
|
||||
self.fill_underlay_angle[i],
|
||||
self.fill_underlay_row_spacing,
|
||||
self.fill_underlay_row_spacing,
|
||||
|
@ -597,7 +609,7 @@ class FillStitch(EmbroideryElement):
|
|||
color=self.color,
|
||||
tags=("auto_fill", "auto_fill_top"),
|
||||
stitches=auto_fill(
|
||||
self.fill_shape(shape),
|
||||
shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
|
@ -663,7 +675,7 @@ class FillStitch(EmbroideryElement):
|
|||
color=self.color,
|
||||
tags=("guided_fill", "auto_fill_top"),
|
||||
stitches=guided_fill(
|
||||
self.fill_shape(shape),
|
||||
shape,
|
||||
guide_line.geoms[0],
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
|
|
|
@ -150,7 +150,7 @@ class Stroke(EmbroideryElement):
|
|||
return max(self.get_int_param("line_count", 10), 1)
|
||||
|
||||
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
|
||||
|
||||
|
@ -246,7 +246,7 @@ class Stroke(EmbroideryElement):
|
|||
type='dropdown',
|
||||
default=0,
|
||||
# 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)],
|
||||
sort_index=12)
|
||||
def scale_axis(self):
|
||||
|
@ -286,6 +286,19 @@ class Stroke(EmbroideryElement):
|
|||
def rotate_ripples(self):
|
||||
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
|
||||
@cache
|
||||
def is_closed(self):
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from ..utils.geometry import Point
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
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,
|
||||
tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None):
|
||||
|
||||
base_stitch = None
|
||||
if isinstance(x, Stitch):
|
||||
# Allow creating a Stitch from another Stitch. Attributes passed as
|
||||
# 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):
|
||||
# Allow creating a Stitch from a Point
|
||||
point = x
|
||||
|
@ -24,17 +27,19 @@ class Stitch(Point):
|
|||
else:
|
||||
Point.__init__(self, x, y)
|
||||
|
||||
self.color = color
|
||||
self.jump = jump
|
||||
self.trim = trim
|
||||
self.stop = stop
|
||||
self.color_change = color_change
|
||||
self.force_lock_stitches = force_lock_stitches
|
||||
self.tie_modus = tie_modus
|
||||
self.no_ties = no_ties
|
||||
self.tags = set()
|
||||
self._set('color', color, base_stitch)
|
||||
self._set('jump', jump, base_stitch)
|
||||
self._set('trim', trim, base_stitch)
|
||||
self._set('stop', stop, base_stitch)
|
||||
self._set('color_change', color_change, base_stitch)
|
||||
self._set('force_lock_stitches', force_lock_stitches, base_stitch)
|
||||
self._set('tie_modus', tie_modus, base_stitch)
|
||||
self._set('no_ties', no_ties, base_stitch)
|
||||
|
||||
self.tags = set()
|
||||
self.add_tags(tags or [])
|
||||
if base_stitch is not None:
|
||||
self.add_tags(base_stitch.tags)
|
||||
|
||||
def __repr__(self):
|
||||
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 " ",
|
||||
"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):
|
||||
for tag in tags:
|
||||
self.add_tag(tag)
|
||||
|
|
|
@ -59,13 +59,13 @@ def auto_fill(shape,
|
|||
starting_point,
|
||||
ending_point=None,
|
||||
underpath=True):
|
||||
try:
|
||||
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)
|
||||
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)
|
||||
|
||||
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
|
||||
return fallback(shape, running_stitch_length, running_stitch_tolerance)
|
||||
|
|
|
@ -132,7 +132,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
|
|||
start -= (start + normal * center) % row_spacing
|
||||
|
||||
current_row_y = start
|
||||
|
||||
rows = []
|
||||
while current_row_y < end:
|
||||
p0 = 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 = [tuple(reversed(run)) for run in runs]
|
||||
|
||||
yield runs
|
||||
rows.append(runs)
|
||||
|
||||
if end_row_spacing:
|
||||
current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height)
|
||||
else:
|
||||
current_row_y += row_spacing
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last):
|
||||
stitches = []
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
from math import atan2, copysign
|
||||
from random import random
|
||||
|
||||
import numpy as np
|
||||
import shapely.prepared
|
||||
from shapely import geometry as shgeo
|
||||
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 ..i18n import _
|
||||
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,
|
||||
|
@ -27,10 +32,15 @@ def guided_fill(shape,
|
|||
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)
|
||||
|
||||
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)
|
||||
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
|
||||
|
@ -39,6 +49,15 @@ def guided_fill(shape,
|
|||
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):
|
||||
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):
|
||||
(minx, miny, maxx, maxy) = shape.bounds
|
||||
|
||||
line = line.simplify(0.01, False)
|
||||
|
||||
upper_left = InkstitchPoint(minx, miny)
|
||||
lower_right = InkstitchPoint(maxx, maxy)
|
||||
length = (upper_left - lower_right).length()
|
||||
|
||||
point1 = InkstitchPoint(*line.coords[0])
|
||||
point2 = InkstitchPoint(*line.coords[1])
|
||||
new_starting_point = point1 - (point2 - point1).unit() * length
|
||||
start_point = InkstitchPoint.from_tuple(line.coords[0])
|
||||
end_point = InkstitchPoint.from_tuple(line.coords[-1])
|
||||
direction = (end_point - start_point).unit()
|
||||
|
||||
point3 = InkstitchPoint(*line.coords[-2])
|
||||
point4 = InkstitchPoint(*line.coords[-1])
|
||||
new_ending_point = point4 + (point4 - point3).unit() * length
|
||||
new_start_point = start_point - direction * length
|
||||
new_end_point = end_point + direction * length
|
||||
|
||||
return shgeo.LineString([new_starting_point.as_tuple()] +
|
||||
line.coords[1:-1] + [new_ending_point.as_tuple()])
|
||||
# without this, we seem especially likely to run into this libgeos bug:
|
||||
# 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):
|
||||
|
@ -114,8 +134,8 @@ def repair_non_simple_line(line):
|
|||
repaired = unary_union(linemerge(line_segments))
|
||||
counter += 1
|
||||
if repaired.geom_type != 'LineString':
|
||||
raise ValueError(
|
||||
_("Guide line (or offset copy) is self crossing!"))
|
||||
# They gave us a line with complicated self-intersections. Use a fallback.
|
||||
return shgeo.LineString((line.coords[0], line.coords[-1]))
|
||||
else:
|
||||
return repaired
|
||||
|
||||
|
@ -158,7 +178,12 @@ def prepare_guide_line(line, shape):
|
|||
if line.geom_type != 'LineString' or not line.is_simple:
|
||||
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)
|
||||
|
||||
return line
|
||||
|
@ -176,24 +201,41 @@ def clean_offset_line(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):
|
||||
line = prepare_guide_line(line, shape)
|
||||
|
||||
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)
|
||||
shape_envelope = shapely.prepared.prep(shape.convex_hull)
|
||||
|
||||
row = 0
|
||||
start_row = _get_start_row(line, shape, row_spacing, translate_direction)
|
||||
row = start_row
|
||||
direction = 1
|
||||
offset_line = None
|
||||
rows = []
|
||||
while True:
|
||||
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)
|
||||
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)
|
||||
|
||||
|
@ -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
|
||||
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)
|
||||
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:
|
||||
direction = -1
|
||||
row = 1
|
||||
row = start_row - 1
|
||||
else:
|
||||
break
|
||||
else:
|
||||
for segment in take_only_line_strings(intersection).geoms:
|
||||
yield segment.coords[:]
|
||||
row += 1
|
||||
|
||||
return rows
|
||||
|
|
|
@ -63,6 +63,8 @@ def _get_helper_lines(stroke):
|
|||
|
||||
if stroke.is_closed:
|
||||
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:
|
||||
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
|
||||
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 = []
|
||||
for point0, point1 in zip(*rail_points):
|
||||
|
@ -83,24 +85,39 @@ def _get_satin_ripple_helper_lines(stroke):
|
|||
for step in steps:
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
def _converge_helper_line_points(helper_lines, point_edge=False):
|
||||
num_lines = len(helper_lines)
|
||||
steps = _get_steps(num_lines)
|
||||
for i, line in enumerate(helper_lines):
|
||||
points = []
|
||||
for j in range(len(line) - 1):
|
||||
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
|
||||
|
||||
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):
|
||||
guide_line = stroke.get_guide_line()
|
||||
max_dist = stroke.grid_size or stroke.running_stitch_length
|
||||
|
@ -124,10 +141,25 @@ def _target_point_helper_lines(stroke, outline):
|
|||
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):
|
||||
for i, helper in enumerate(helper_lines):
|
||||
helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines)
|
||||
start = stroke.get_skip_start()
|
||||
end = len(helper) - stroke.get_skip_end()
|
||||
skip_end = stroke.get_skip_end()
|
||||
if stroke.reverse:
|
||||
start, skip_end = skip_end, start
|
||||
for i, helper in enumerate(helper_lines):
|
||||
end = len(helper) - skip_end
|
||||
points = helper[start:end]
|
||||
if i % 2 == 0:
|
||||
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):
|
||||
# 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)
|
||||
outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))
|
||||
|
||||
|
|
|
@ -64,17 +64,7 @@ inkstitch_attribs = [
|
|||
'join_style',
|
||||
'avoid_self_crossing',
|
||||
'clockwise',
|
||||
'line_count',
|
||||
'skip_start',
|
||||
'skip_end',
|
||||
'grid_size',
|
||||
'reverse',
|
||||
'exponent',
|
||||
'flip_exponent',
|
||||
'scale_axis',
|
||||
'scale_start',
|
||||
'scale_end',
|
||||
'rotate_ripples',
|
||||
'expand_mm',
|
||||
'fill_underlay',
|
||||
'fill_underlay_angle',
|
||||
|
@ -97,6 +87,17 @@ inkstitch_attribs = [
|
|||
'repeats',
|
||||
'running_stitch_length_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',
|
||||
'short_stitch_distance_mm',
|
||||
|
|
|
@ -129,6 +129,10 @@ class Point:
|
|||
def from_shapely_point(cls, point):
|
||||
return cls(point.x, point.y)
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, point):
|
||||
return cls(point[0], point[1])
|
||||
|
||||
def __json__(self):
|
||||
return vars(self)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue