summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-rw-r--r--lib/elements/__init__.py8
-rw-r--r--lib/elements/satin_column.py122
2 files changed, 94 insertions, 36 deletions
diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py
index 7e05e19c..22603217 100644
--- a/lib/elements/__init__.py
+++ b/lib/elements/__init__.py
@@ -1,6 +1,6 @@
-from auto_fill import AutoFill
-from fill import Fill
-from stroke import Stroke
-from satin_column import SatinColumn
from element import EmbroideryElement
+from satin_column import SatinColumn
+from stroke import Stroke
from polyline import Polyline
+from fill import Fill
+from auto_fill import AutoFill
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index 705983d7..1f9854ed 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -5,8 +5,8 @@ import cubicsuperpath
from .element import param, EmbroideryElement, Patch
from ..i18n import _
-from ..utils import cache, Point, cut
-from ..svg import line_strings_to_csp, get_correction_transform
+from ..utils import cache, Point, cut, collapse_duplicate_point
+from ..svg import line_strings_to_csp, point_lists_to_csp
class SatinColumn(EmbroideryElement):
@@ -245,10 +245,17 @@ class SatinColumn(EmbroideryElement):
intersection = rail_segment.intersection(rung)
+ # If there are duplicate points in a rung-less satin, then
+ # intersection will be a GeometryCollection of multiple copies
+ # of the same point. This reduces it that to a single point.
+ intersection = collapse_duplicate_point(intersection)
+
if not intersection.is_empty:
if isinstance(intersection, shgeo.MultiLineString):
intersections += len(intersection)
break
+ elif not isinstance(intersection, shgeo.Point):
+ self.fatal("intersection is a: %s %s" % (intersection, intersection.geoms))
else:
intersections += 1
@@ -321,6 +328,35 @@ class SatinColumn(EmbroideryElement):
self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") %
dict(id=node_id, length1=len(self.rails[0]), length2=len(self.rails[1])))
+ def reverse(self):
+ """Return a new SatinColumn like this one but in the opposite direction.
+
+ The path will be flattened and the new satin will contain a new XML
+ node that is not yet in the SVG.
+ """
+ # flatten the path because you can't just reverse a CSP subpath's elements (I think)
+ point_lists = []
+
+ for rail in self.rails:
+ point_lists.append(list(reversed(self.flatten_subpath(rail))))
+
+ # reverse the order of the rails because we're sewing in the opposite direction
+ point_lists.reverse()
+
+ for rung in self.rungs:
+ point_lists.append(self.flatten_subpath(rung))
+
+ return self._csp_to_satin(point_lists_to_csp(point_lists))
+
+ def apply_transform(self):
+ """Return a new SatinColumn like this one but with transforms applied.
+
+ This node's and all ancestor nodes' transforms will be applied. The
+ new SatinColumn's node will not be in the SVG document.
+ """
+
+ return self._csp_to_satin(self.csp)
+
def split(self, split_point):
"""Split a satin into two satins at the specified point
@@ -328,6 +364,9 @@ class SatinColumn(EmbroideryElement):
ends. Finds corresponding point on the other rail (taking into account
the rungs) and breaks the rails at these points.
+ split_point can also be a noramlized projection of a distance along the
+ satin, in the range 0.0 to 1.0.
+
Returns two new SatinColumn instances: the part before and the part
after the split point. All parameters are copied over to the new
SatinColumn instances.
@@ -337,7 +376,7 @@ class SatinColumn(EmbroideryElement):
path_lists = self._cut_rails(cut_points)
self._assign_rungs_to_split_rails(path_lists)
self._add_rungs_if_necessary(path_lists)
- return self._path_lists_to_satins(path_lists)
+ return [self._path_list_to_satins(path_list) for path_list in path_lists]
def _find_cut_points(self, split_point):
"""Find the points on each satin corresponding to the split point.
@@ -347,22 +386,30 @@ class SatinColumn(EmbroideryElement):
for that rail. A corresponding cut point will be chosen on the other
rail, taking into account the satin's rungs to choose a matching point.
+ split_point can instead be a number in [0.0, 1.0] indicating a
+ a fractional distance down the satin to cut at.
+
Returns: a list of two Point objects corresponding to the selected
cut points.
"""
- split_point = Point(*split_point)
- patch = self.do_satin()
- index_of_closest_stitch = min(range(len(patch)), key=lambda index: split_point.distance(patch.stitches[index]))
+ # like in do_satin()
+ points = list(chain.from_iterable(izip(*self.walk_paths(self.zigzag_spacing, 0))))
+
+ if isinstance(split_point, float):
+ index_of_closest_stitch = int(round(len(points) * split_point))
+ else:
+ split_point = Point(*split_point)
+ index_of_closest_stitch = min(range(len(points)), key=lambda index: split_point.distance(points[index]))
if index_of_closest_stitch % 2 == 0:
# split point is on the first rail
- return (patch.stitches[index_of_closest_stitch],
- patch.stitches[index_of_closest_stitch + 1])
+ return (points[index_of_closest_stitch],
+ points[index_of_closest_stitch + 1])
else:
# split point is on the second rail
- return (patch.stitches[index_of_closest_stitch - 1],
- patch.stitches[index_of_closest_stitch])
+ return (points[index_of_closest_stitch - 1],
+ points[index_of_closest_stitch])
def _cut_rails(self, cut_points):
"""Cut the rails of this satin at the specified points.
@@ -372,7 +419,7 @@ class SatinColumn(EmbroideryElement):
Returns: A list of two elements, corresponding two the two new sets of
rails. Each element is a list of two rails of type LineString.
- """
+ """
rails = [shgeo.LineString(self.flatten_subpath(rail)) for rail in self.rails]
@@ -396,19 +443,22 @@ class SatinColumn(EmbroideryElement):
path_list.extend(rung for rung in rungs if path_list[0].intersects(rung) and path_list[1].intersects(rung))
def _add_rungs_if_necessary(self, path_lists):
- """Add an additional rung to each new satin if it ended up with none.
+ """Add an additional rung to each new satin if needed.
- If the split point is between the end and the last rung, then one of
- the satins will have no rungs. Add one to make it stitch properly.
- """
+ Case #1: If the split point is between the end and the last rung, then
+ one of the satins will have no rungs. It will be treated as an old-style
+ satin, but it may not have an equal number of points in each rail. Adding
+ a rung will make it stitch properly.
- # no need to add rungs if there weren't any in the first place
- if not self.rungs:
- return
+ Case #2: If one of the satins ends up with exactly two rungs, it's
+ ambiguous which of the subpaths are rails and which are rungs. Adding
+ another rung disambiguates this case. See rail_indices() above for more
+ information.
+ """
for path_list in path_lists:
- if len(path_list) == 2:
- # If a path has no rungs, it may be invalid. Add a rung at the start.
+ if len(path_list) in (2, 4):
+ # Add the rung just after the start of the satin.
rung_start = path_list[0].interpolate(0.1)
rung_end = path_list[1].interpolate(0.1)
rung = shgeo.LineString((rung_start, rung_end))
@@ -418,18 +468,26 @@ class SatinColumn(EmbroideryElement):
path_list.append(rung)
- def _path_lists_to_satins(self, path_lists):
- transform = get_correction_transform(self.node)
- satins = []
- for path_list in path_lists:
- node = deepcopy(self.node)
- csp = line_strings_to_csp(path_list)
- d = cubicsuperpath.formatPath(csp)
- node.set("d", d)
- node.set("transform", transform)
- satins.append(SatinColumn(node))
-
- return satins
+ def _path_list_to_satins(self, path_list):
+ return self._csp_to_satin(line_strings_to_csp(path_list))
+
+ def _csp_to_satin(self, csp):
+ node = deepcopy(self.node)
+ d = cubicsuperpath.formatPath(csp)
+ node.set("d", d)
+
+ # we've already applied the transform, so get rid of it
+ if node.get("transform"):
+ del node.attrib["transform"]
+
+ return SatinColumn(node)
+
+ @property
+ @cache
+ def center_line(self):
+ # similar technique to do_center_walk()
+ center_walk, _ = self.walk_paths(self.zigzag_spacing, -100000)
+ return shgeo.LineString(center_walk)
def offset_points(self, pos1, pos2, offset_px):
# Expand or contract two points about their midpoint. This is