diff options
Diffstat (limited to 'lib/stitches/guided_fill.py')
| -rw-r--r-- | lib/stitches/guided_fill.py | 112 |
1 files changed, 78 insertions, 34 deletions
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 7eb49e86..05de14cd 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -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() + 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 |
