kopia lustrzana https://github.com/inkstitch/inkstitch
add vertical travel edges for less jagged travel paths
rodzic
90a16fb7f9
commit
513850c975
15
lib/debug.py
15
lib/debug.py
|
@ -1,4 +1,5 @@
|
|||
import atexit
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
import os
|
||||
import socket
|
||||
|
@ -80,7 +81,8 @@ class Debug(object):
|
|||
def add_layer(self, name="Debug"):
|
||||
layer = etree.Element("g", {
|
||||
INKSCAPE_GROUPMODE: "layer",
|
||||
INKSCAPE_LABEL: name
|
||||
INKSCAPE_LABEL: name,
|
||||
"style": "display: none"
|
||||
})
|
||||
self.svg.append(layer)
|
||||
self.current_layer = layer
|
||||
|
@ -172,6 +174,17 @@ class Debug(object):
|
|||
|
||||
self.close_group()
|
||||
|
||||
@contextmanager
|
||||
def time_this(self, label="code block"):
|
||||
if self.enabled:
|
||||
start = time.time()
|
||||
self.raw_log("begin %s", label)
|
||||
|
||||
yield
|
||||
|
||||
if self.enabled:
|
||||
self.raw_log("completed %s, duration = %s", label, time.time() - start)
|
||||
|
||||
|
||||
debug = Debug()
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import math
|
|||
|
||||
import networkx
|
||||
from shapely import geometry as shgeo
|
||||
from shapely.ops import snap
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
from ..debug import debug
|
||||
|
@ -146,21 +147,7 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing):
|
|||
graph.add_edge(*segment, key="segment", underpath_edges=[])
|
||||
|
||||
tag_nodes_with_outline_and_projection(graph, shape, graph.nodes())
|
||||
|
||||
for node in graph.nodes():
|
||||
outline_index = which_outline(shape, node)
|
||||
outline_projection = project(shape, node, outline_index)
|
||||
|
||||
# Tag each node with its index and projection.
|
||||
graph.add_node(node, index=outline_index, projection=outline_projection)
|
||||
|
||||
add_edges_between_outline_nodes(graph)
|
||||
|
||||
for node1, node2, key, data in graph.edges(keys=True, data=True):
|
||||
if key == "outline":
|
||||
# duplicate every other edge
|
||||
if data['index'] % 2 == 0:
|
||||
graph.add_edge(node1, node2, key="extra")
|
||||
add_edges_between_outline_nodes(graph, duplicate_every_other=True)
|
||||
|
||||
debug.log_graph(graph, "graph")
|
||||
|
||||
|
@ -175,7 +162,7 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes):
|
|||
graph.add_node(node, outline=outline_index, projection=outline_projection)
|
||||
|
||||
|
||||
def add_edges_between_outline_nodes(graph):
|
||||
def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
|
||||
"""Add edges around the outlines of the graph, connecting sequential nodes.
|
||||
|
||||
This function assumes that all nodes in the graph are on the outline of the
|
||||
|
@ -197,6 +184,9 @@ def add_edges_between_outline_nodes(graph):
|
|||
data = dict(outline=outline_index, index=i)
|
||||
graph.add_edge(node1, node2, key="outline", **data)
|
||||
|
||||
if i % 2 == 0:
|
||||
graph.add_edge(node1, node2, key="extra", **data)
|
||||
|
||||
|
||||
@debug.time
|
||||
def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
||||
|
@ -210,14 +200,15 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
|||
edges.
|
||||
|
||||
If underpath is True, we'll also allow travel inside the shape. We'll
|
||||
fill the shape with a cross-hatched grid of lines 2mm apart, at ±45
|
||||
degrees from the fill stitch angle. This will ensure that travel stitches
|
||||
won't be visible and won't disrupt the fill stitch.
|
||||
fill the shape with a cross-hatched grid of lines. We'll construct a
|
||||
graph from them and use a shortest path algorithm to construct travel
|
||||
stitch paths in travel().
|
||||
|
||||
When underpathing, we "encourage" the travel() function to travel inside
|
||||
the shape rather than on the boundary. We do this by weighting the
|
||||
boundary edges extra so that they're more "expensive" in the shortest path
|
||||
calculation.
|
||||
calculation. We also weight the interior edges extra proportional to
|
||||
how close they are to the boundary.
|
||||
"""
|
||||
|
||||
graph = networkx.MultiGraph()
|
||||
|
@ -228,33 +219,23 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
|||
graph.add_nodes_from(fill_stitch_graph.nodes(data=True))
|
||||
|
||||
if underpath:
|
||||
# These two MultiLineStrings will make up the cross-hatched grid.
|
||||
grating1 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, fill_stitch_angle + math.pi / 4, 2 * PIXELS_PER_MM))))
|
||||
grating2 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, fill_stitch_angle - math.pi / 4, 2 * PIXELS_PER_MM))))
|
||||
boundary_points, travel_edges = build_travel_edges(shape, fill_stitch_angle)
|
||||
|
||||
debug.add_layer("auto-fill travel")
|
||||
debug.log_line_strings(grating1, "grating1")
|
||||
debug.log_line_strings(grating2, "grating2")
|
||||
|
||||
# We'll add the endpoints of the crosshatch grating lines too These
|
||||
# will all be on the outline of the shape. This will ensure that a
|
||||
# path traveling inside the shape can reach its target on the outline,
|
||||
# which will be one of the points added above.
|
||||
endpoints = [coord for mls in (grating1, grating2)
|
||||
for ls in mls
|
||||
for coord in ls.coords]
|
||||
tag_nodes_with_outline_and_projection(graph, shape, endpoints)
|
||||
# This will ensure that a path traveling inside the shape can reach its
|
||||
# target on the outline, which will be one of the points added above.
|
||||
tag_nodes_with_outline_and_projection(graph, shape, boundary_points)
|
||||
|
||||
add_edges_between_outline_nodes(graph)
|
||||
for start, end, key in graph.edges:
|
||||
p1 = InkstitchPoint(*start)
|
||||
p2 = InkstitchPoint(*end)
|
||||
|
||||
# Set the weight equal to 10x the edge length, to encourage travel()
|
||||
# to avoid them when underpathing is enabled.
|
||||
graph[start][end][key]["weight"] = 10 * p1.distance(p2)
|
||||
|
||||
if underpath:
|
||||
for start, end, key in graph.edges:
|
||||
p1 = InkstitchPoint(*start)
|
||||
p2 = InkstitchPoint(*end)
|
||||
|
||||
# Set the weight equal to 10x the edge length, to encourage travel()
|
||||
# to avoid them.
|
||||
graph[start][end][key]["weight"] = 10 * p1.distance(p2)
|
||||
|
||||
segments = []
|
||||
for start, end, key, data in fill_stitch_graph.edges(keys=True, data=True):
|
||||
if key == 'segment':
|
||||
|
@ -264,14 +245,17 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
|||
# allows for building a set of shapes and then efficiently testing
|
||||
# the set for intersection. This allows us to do blazing-fast
|
||||
# queries of which line segments overlap each underpath edge.
|
||||
rtree = STRtree(segments)
|
||||
strtree = STRtree(segments)
|
||||
|
||||
interior_edges = grating1.symmetric_difference(grating2)
|
||||
for ls in interior_edges.geoms:
|
||||
# This makes the distance calculations below a bit faster. We're
|
||||
# not looking for high precision anyway.
|
||||
outline = shape.boundary.simplify(0.5 * PIXELS_PER_MM, preserve_topology=False)
|
||||
|
||||
for ls in travel_edges:
|
||||
p1, p2 = [InkstitchPoint(*coord) for coord in ls.coords]
|
||||
edge = (p1.as_tuple(), p2.as_tuple(), 'travel')
|
||||
|
||||
for segment in rtree.query(ls):
|
||||
for segment in strtree.query(ls):
|
||||
start, end = segment.coords
|
||||
fill_stitch_graph[start][end]['segment']['underpath_edges'].append(edge)
|
||||
|
||||
|
@ -282,20 +266,67 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
|||
# This includes the outer outline and the outlines of the holes.
|
||||
# The result is that travel stitching will tend to hug the center
|
||||
# of the shape.
|
||||
weight /= ls.distance(shape.boundary) + 0.1
|
||||
weight /= ls.distance(outline) + 0.1
|
||||
|
||||
graph.add_edge(*edge, weight=weight)
|
||||
|
||||
# otherwise we sometimes get exceptions like this:
|
||||
# without this, we sometimes get exceptions like this:
|
||||
# Exception AttributeError: "'NoneType' object has no attribute 'GEOSSTRtree_destroy'" in
|
||||
# <bound method STRtree.__del__ of <shapely.strtree.STRtree instance at 0x0D2BFD50>> ignored
|
||||
del rtree
|
||||
del strtree
|
||||
|
||||
debug.log_graph(graph, "travel graph")
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
def build_travel_edges(shape, fill_angle):
|
||||
"""Given a graph, compute the interior travel edges.
|
||||
|
||||
We want to fill the shape with a grid of line segments that can be used for
|
||||
travel stitch routing. Our goals:
|
||||
|
||||
* not too many edges so that the shortest path algorithm is speedy
|
||||
* don't travel in the direction of the fill stitch rows so that the
|
||||
travel stitch doesn't visually disrupt the fill stitch pattern
|
||||
|
||||
To do this, we'll fill the shape with three gratings: one at +45 degrees
|
||||
from the fill stitch angle, one at -45 degrees, and one at +90 degrees.
|
||||
The pattern looks like this:
|
||||
|
||||
/|\|/|\|/|\
|
||||
\|/|\|/|\|/
|
||||
/|\|/|\|/|\
|
||||
\|/|\|/|\|/
|
||||
|
||||
Returns: (endpoints, edges)
|
||||
endpoints - the points on travel edges that intersect with the boundary
|
||||
of the shape
|
||||
edges - the line segments we can travel on, as individual LineString
|
||||
instances
|
||||
"""
|
||||
|
||||
grating1 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, fill_angle + math.pi / 4, 2 * PIXELS_PER_MM))))
|
||||
grating2 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, fill_angle - math.pi / 4, 2 * PIXELS_PER_MM))))
|
||||
grating3 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, fill_angle - math.pi / 2, math.sqrt(2) * PIXELS_PER_MM))))
|
||||
|
||||
debug.add_layer("auto-fill travel")
|
||||
debug.log_line_strings(grating1, "grating1")
|
||||
debug.log_line_strings(grating2, "grating2")
|
||||
debug.log_line_strings(grating3, "grating3")
|
||||
|
||||
endpoints = [coord for mls in (grating1, grating2, grating3)
|
||||
for ls in mls
|
||||
for coord in ls.coords]
|
||||
|
||||
diagonal_edges = grating1.symmetric_difference(grating2)
|
||||
|
||||
# without this, floating point inaccuracies prevent the intersection points from lining up perfectly.
|
||||
vertical_edges = snap(grating3.difference(grating1), diagonal_edges, 0.005)
|
||||
|
||||
return endpoints, chain(diagonal_edges, vertical_edges)
|
||||
|
||||
|
||||
def check_graph(graph, shape, max_stitch_length):
|
||||
if networkx.is_empty(graph) or not networkx.is_eulerian(graph):
|
||||
if shape.area < max_stitch_length ** 2:
|
||||
|
|
Ładowanie…
Reference in New Issue