diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index 188c2ad7b..c539cf7e6 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -596,6 +596,37 @@ class FillStitch(EmbroideryElement):
     def herringbone_width(self):
         return self.get_float_param('herringbone_width_mm', 0)
 
+    @property
+    @param(
+        'pull_compensation_mm',
+        _('Pull compensation'),
+        tooltip=_('Fill stitch can pull the fabric together, resulting in a shape narrower than you draw in Inkscape. '
+                  'This setting expands each row of stitches outward from the center of the row by a fixed length. '
+                  'Two values separated by a space may be used for an asymmetric effect.'),
+        select_items=[('fill_method', 'auto_fill')],
+        unit=_('mm (each side)'),
+        type='float',
+        default=0,
+        sort_index=26)
+    @cache
+    def pull_compensation_px(self):
+        return np.maximum(self.get_split_mm_param_as_px("pull_compensation_mm", (0, 0)), 0)
+
+    @property
+    @param(
+        'pull_compensation_percent',
+        _('Pull compensation percentage'),
+        tooltip=_('Additional pull compensation which varies as a percentage of row width. '
+                  'Two values separated by a space may be used for an asymmetric effect.'),
+        select_items=[('fill_method', 'auto_fill')],
+        unit=_('% (each side)'),
+        type='float',
+        default=0,
+        sort_index=27)
+    @cache
+    def pull_compensation_percent(self):
+        return np.maximum(self.get_split_float_param("pull_compensation_percent", (0, 0)), 0)
+
     @property
     def color(self):
         # SVG spec says the default fill is black
@@ -1027,6 +1058,8 @@ class FillStitch(EmbroideryElement):
                 self.enable_random_stitch_length,
                 self.random_stitch_length_jitter,
                 self.random_seed,
+                self.pull_compensation_px,
+                self.pull_compensation_percent / 100,
             )
         )
         return [stitch_group]
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index c031f229e..dd04b507d 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -20,7 +20,7 @@ from ..metadata import InkStitchMetadata
 from ..stitch_plan import StitchGroup
 from ..stitches import running_stitch
 from ..svg import line_strings_to_csp, point_lists_to_csp
-from ..utils import Point, cache, cut, cut_multiple, prng
+from ..utils import Point, cache, cut, cut_multiple, offset_points, prng
 from ..utils.param import ParamOption
 from ..utils.threading import check_stop_flag
 from .element import PIXELS_PER_MM, EmbroideryElement, param
@@ -260,7 +260,7 @@ class SatinColumn(EmbroideryElement):
         'pull_compensation_percent',
         _('Pull compensation percentage'),
         tooltip=_('Additional pull compensation which varies as a percentage of stitch width. '
-                  'Two values separated by a space may be used for an aysmmetric effect.'),
+                  'Two values separated by a space may be used for an asymmetric effect.'),
         unit=_('% (each side)'),
         type='float',
         default=0,
@@ -276,7 +276,7 @@ class SatinColumn(EmbroideryElement):
         _('Pull compensation'),
         tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. '
                   'This setting expands each pair of needle penetrations outward from the center of the satin column by a fixed length. '
-                  'Two values separated by a space may be used for an aysmmetric effect.'),
+                  'Two values separated by a space may be used for an asymmetric effect.'),
         unit=_('mm (each side)'),
         type='float',
         default=0,
@@ -1015,34 +1015,6 @@ class SatinColumn(EmbroideryElement):
             center_walk = [center_walk[0], center_walk[0]]
         return shgeo.LineString(center_walk)
 
-    def offset_points(self, pos1, pos2, offset_px, offset_proportional):
-        # Expand or contract two points about their midpoint.  This is
-        # useful for pull compensation and insetting underlay.
-
-        distance = (pos1 - pos2).length()
-
-        if distance < 0.0001:
-            # if they're the same point, we don't know which direction
-            # to offset in, so we have to just return the points
-            return pos1, pos2
-
-        # calculate the offset for each side
-        offset_a = offset_px[0] + (distance * offset_proportional[0])
-        offset_b = offset_px[1] + (distance * offset_proportional[1])
-        offset_total = offset_a + offset_b
-
-        # don't contract beyond the midpoint, or we'll start expanding
-        if offset_total < -distance:
-            scale = -distance / offset_total
-            offset_a = offset_a * scale
-            offset_b = offset_b * scale
-
-        # convert offset to float before using because it may be a numpy.float64
-        out1 = pos1 + (pos1 - pos2).unit() * float(offset_a)
-        out2 = pos2 + (pos2 - pos1).unit() * float(offset_b)
-
-        return out1, out2
-
     def _stitch_distance(self, pos0, pos1, previous_pos0, previous_pos1):
         """Return the distance from one stitch to the next."""
 
@@ -1567,7 +1539,7 @@ class SatinColumn(EmbroideryElement):
                 offset_px[0] = -inset_px
             if b.distance(pairs[i-1][1]) < min_dist:
                 offset_px[1] = -inset_px
-            shortened.append(self.offset_points(a, b, offset_px, (0, 0)))
+            shortened.append(offset_points(a, b, offset_px, (0, 0)))
         return shortened
 
     def _get_inset_point(self, point1, point2, distance_fraction):
@@ -1656,7 +1628,7 @@ class SatinProcessor:
         else:
             offset_prop = self.offset_proportional
 
-        a, b = self.satin.offset_points(pos0, pos1, self.offset_px, offset_prop)
+        a, b = offset_points(pos0, pos1, self.offset_px, offset_prop)
         return a, b
 
     def get_stitch_spacing_multiple(self):
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index d3db978a2..84832fc8f 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -11,8 +11,8 @@ from typing import Iterator
 
 import networkx
 from shapely import geometry as shgeo
-from shapely import segmentize
-from shapely.ops import snap
+from shapely import make_valid, segmentize
+from shapely.ops import snap, unary_union
 from shapely.strtree import STRtree
 
 from ..debug.debug import debug
@@ -22,7 +22,8 @@ from ..utils import cache
 from ..utils.clamp_path import clamp_path_to_polygon
 from ..utils.geometry import Point as InkstitchPoint
 from ..utils.geometry import (ensure_multi_line_string,
-                              line_string_to_point_list)
+                              line_string_to_point_list, offset_points)
+from ..utils.list import is_all_zeroes
 from ..utils.prng import join_args
 from ..utils.smoothing import smooth_path
 from ..utils.threading import check_stop_flag
@@ -82,12 +83,18 @@ def auto_fill(shape,
               gap_fill_rows=0,
               enable_random_stitch_length=False,
               random_sigma=0.0,
-              random_seed=""):
+              random_seed="",
+              pull_compensation_px=(0, 0),
+              pull_compensation_percent=(0, 0)):
+    has_pull_compensation = not is_all_zeroes(pull_compensation_px) or not is_all_zeroes(pull_compensation_percent)
+    if has_pull_compensation:
+        spacing = min(row_spacing, end_row_spacing or row_spacing)
+        shape = adjust_shape_for_pull_compensation(shape, angle, spacing, pull_compensation_px, pull_compensation_percent)
+
     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)
 
@@ -120,6 +127,34 @@ def round_to_multiple_of_2(number):
         return number
 
 
+@debug.time
+def adjust_shape_for_pull_compensation(shape, angle, row_spacing, pull_compensation_px, pull_compensation_percent):
+    rows = intersect_region_with_grating(shape, angle, row_spacing)
+    if not rows:
+        return shape
+    segments = [segment for row in rows for segment in row]
+    segments = apply_pull_compensation(segments, pull_compensation_px, pull_compensation_percent)
+
+    lines = [shgeo.LineString((start, end)) for start, end in segments]
+    buffer_amount = row_spacing/2 + 0.01
+    buffered_lines = [line.buffer(buffer_amount) for line in lines]
+
+    polygon = unary_union(buffered_lines)
+    return make_valid(polygon)
+
+
+def apply_pull_compensation(segments, pull_compensation_px, pull_compensation_percent):
+    new_segments = []
+    for segment in segments:
+        start = InkstitchPoint.from_tuple(segment[0])
+        end = InkstitchPoint.from_tuple(segment[1])
+        end = InkstitchPoint.from_tuple(segment[1])
+        new_start, new_end = offset_points(start, end, pull_compensation_px, pull_compensation_percent)
+        new_segments.append((new_start.as_tuple(), new_end.as_tuple()))
+
+    return new_segments
+
+
 def which_outline(shape, coords):
     """return the index of the outline on which the point resides
 
@@ -374,7 +409,6 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
     calculation.  We also weight the interior edges extra proportional to
     how close they are to the boundary.
     """
-
     graph = networkx.MultiGraph()
 
     # Add all the nodes from the main graph.  This will be all of the endpoints
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 4f3e2208c..8480ea0b3 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -113,10 +113,10 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
     # Now get a unit vector rotated to the requested angle.  I use -angle
     # because shapely rotates clockwise, but my geometry textbooks taught
     # me to consider angles as counter-clockwise from the X axis.
-    direction = InkstitchPoint(1, 0).rotate(-angle)
+    direction = InkstitchPoint(-1, 0).rotate(-angle)
 
     # and get a normal vector
-    normal = direction.rotate(math.pi / 2)
+    normal = direction.rotate(-math.pi / 2)
 
     # I'll start from the center, move in the normal direction some amount,
     # and then walk left and right half_length in each direction to create
diff --git a/lib/utils/clamp_path.py b/lib/utils/clamp_path.py
index 0f58f83c8..fcb34a4bf 100644
--- a/lib/utils/clamp_path.py
+++ b/lib/utils/clamp_path.py
@@ -1,5 +1,6 @@
 from shapely.geometry import LineString, MultiPolygon
 from shapely.geometry import Point as ShapelyPoint
+from shapely.ops import nearest_points
 from shapely.prepared import prep
 
 from .geometry import (Point, ensure_geometry_collection,
@@ -14,6 +15,9 @@ def path_to_segments(path):
 
 def segments_to_path(segments):
     """Convert a list of contiguous LineStrings into a list of Points."""
+    if not segments:
+        return []
+
     coords = [segments[0].coords[0]]
 
     for segment in segments:
@@ -68,6 +72,22 @@ def find_border(polygon, point):
         return polygon.exterior
 
 
+def clamp_fully_external_path(path, polygon):
+    """Clamp a path that lies entirely outside a polygon."""
+
+    start = ShapelyPoint(path[0])
+    end = ShapelyPoint(path[-1])
+
+    start_on_outline = nearest_points(start, polygon.exterior)[1].buffer(0.01, resolution=1)
+    end_on_outline = nearest_points(end, polygon.exterior)[1].buffer(0.01, resolution=1)
+
+    border_pieces = ensure_multi_line_string(polygon.exterior.difference(MultiPolygon((start_on_outline, end_on_outline)))).geoms
+    border_pieces = fix_starting_point(border_pieces)
+    shorter = min(border_pieces, key=lambda piece: piece.length)
+
+    return adjust_line_end(shorter, start)
+
+
 def clamp_path_to_polygon(path, polygon):
     """Constrain a path to a Polygon.
 
@@ -143,4 +163,7 @@ def clamp_path_to_polygon(path, polygon):
         else:
             was_inside = False
 
+    if not result:
+        return clamp_fully_external_path(path, polygon)
+
     return segments_to_path(result)
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 24cf8459e..541d9cbc3 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -197,6 +197,37 @@ def cut_path(points, length):
     return [Point(*point) for point in subpath.coords]
 
 
+def offset_points(pos1, pos2, offset_px, offset_proportional):
+    """Expand or contract two points about their midpoint.
+
+    This is useful for pull compensation and insetting underlay.
+    """
+
+    distance = (pos1 - pos2).length()
+
+    if distance < 0.0001:
+        # if they're the same point, we don't know which direction
+        # to offset in, so we have to just return the points
+        return pos1, pos2
+
+    # calculate the offset for each side
+    offset_a = offset_px[0] + (distance * offset_proportional[0])
+    offset_b = offset_px[1] + (distance * offset_proportional[1])
+    offset_total = offset_a + offset_b
+
+    # don't contract beyond the midpoint, or we'll start expanding
+    if offset_total < -distance:
+        scale = -distance / offset_total
+        offset_a = offset_a * scale
+        offset_b = offset_b * scale
+
+    # convert offset to float before using because it may be a numpy.float64
+    out1 = pos1 + (pos1 - pos2).unit() * float(offset_a)
+    out2 = pos2 + (pos2 - pos1).unit() * float(offset_b)
+
+    return out1, out2
+
+
 class Point:
     def __init__(self, x: typing.Union[float, numpy.float64], y: typing.Union[float, numpy.float64]):
         self.x = float(x)
diff --git a/lib/utils/list.py b/lib/utils/list.py
index efa3969eb..e5838b5a4 100644
--- a/lib/utils/list.py
+++ b/lib/utils/list.py
@@ -21,3 +21,7 @@ def poprandom(sequence, rng=_rng):
         sequence[index] = last_item
 
     return item
+
+
+def is_all_zeroes(sequence):
+    return all(item == 0 for item in sequence)