2021-03-12 04:17:19 +00:00
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
2022-01-29 08:53:50 +00:00
import logging
2018-03-31 00:37:11 +00:00
import math
2022-01-29 08:53:50 +00:00
import re
2019-03-10 22:24:10 +00:00
import sys
2019-02-16 01:51:10 +00:00
import traceback
2018-12-13 01:26:22 +00:00
2018-03-31 00:37:11 +00:00
from shapely import geometry as shgeo
2022-06-21 17:59:26 +00:00
from shapely . errors import TopologicalError
from shapely . validation import explain_validity , make_valid
2022-01-29 08:53:50 +00:00
2018-05-02 01:21:07 +00:00
from . . i18n import _
2022-01-29 08:53:50 +00:00
from . . marker import get_marker_elements
2021-08-07 15:21:13 +00:00
from . . stitch_plan import StitchGroup
2022-06-21 17:59:26 +00:00
from . . stitches import auto_fill , contour_fill , guided_fill , legacy_fill
2021-10-21 14:24:40 +00:00
from . . svg import PIXELS_PER_MM
from . . svg . tags import INKSCAPE_LABEL
2022-01-29 08:53:50 +00:00
from . . utils import cache , version
from . element import EmbroideryElement , param
2022-01-30 14:48:51 +00:00
from . validation import ValidationError , ValidationWarning
2019-08-06 02:42:48 +00:00
2021-10-29 14:18:22 +00:00
2019-08-06 02:42:48 +00:00
class SmallShapeWarning ( ValidationWarning ) :
name = _ ( " Small Fill " )
description = _ ( " This fill object is so small that it would probably look better as running stitch or satin column. "
" For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around "
" the outline instead. " )
2018-03-31 00:37:11 +00:00
2021-03-04 17:40:53 +00:00
class ExpandWarning ( ValidationWarning ) :
name = _ ( " Expand " )
description = _ ( " The expand parameter for this fill object cannot be applied. "
" Ink/Stitch will ignore it and will use original size instead. " )
class UnderlayInsetWarning ( ValidationWarning ) :
name = _ ( " Inset " )
description = _ ( " The underlay inset parameter for this fill object cannot be applied. "
" Ink/Stitch will ignore it and will use the original size instead. " )
2022-02-01 18:47:19 +00:00
2022-01-30 14:48:51 +00:00
class MissingGuideLineWarning ( ValidationWarning ) :
name = _ ( " Missing Guideline " )
2022-02-18 14:36:01 +00:00
description = _ ( ' This object is set to " Guided Fill " , but has no guide line. ' )
2022-01-30 14:48:51 +00:00
steps_to_solve = [
_ ( ' * Create a stroke object ' ) ,
_ ( ' * Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line ' )
]
2022-02-01 18:47:19 +00:00
2022-01-30 14:48:51 +00:00
class DisjointGuideLineWarning ( ValidationWarning ) :
name = _ ( " Disjointed Guide Line " )
description = _ ( " The guide line of this object isn ' t within the object borders. "
" The guide line works best, if it is within the target element. " )
steps_to_solve = [
_ ( ' * Move the guide line into the element ' )
]
2022-02-01 18:47:19 +00:00
2022-01-30 14:48:51 +00:00
class MultipleGuideLineWarning ( ValidationWarning ) :
name = _ ( " Multiple Guide Lines " )
2022-02-02 20:19:31 +00:00
description = _ ( " This object has multiple guide lines, but only the first one will be used. " )
2022-01-30 14:48:51 +00:00
steps_to_solve = [
_ ( " * Remove all guide lines, except for one. " )
]
2022-02-01 18:47:19 +00:00
2022-06-21 17:59:26 +00:00
class UnconnectedWarning ( ValidationWarning ) :
2022-01-30 14:48:51 +00:00
name = _ ( " Unconnected " )
2022-06-21 17:59:26 +00:00
description = _ ( " Fill: This object is made up of unconnected shapes. "
2022-01-30 14:48:51 +00:00
" Ink/Stitch doesn ' t know what order to stitch them in. Please break this "
" object up into separate shapes. " )
steps_to_solve = [
_ ( ' * Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects ' ) ,
]
2022-06-21 17:59:26 +00:00
class BorderCrossWarning ( ValidationWarning ) :
2022-01-30 14:48:51 +00:00
name = _ ( " Border crosses itself " )
2022-06-21 17:59:26 +00:00
description = _ ( " Fill: The border crosses over itself. This may lead into unconnected shapes. "
" Please break this object into separate shapes to indicate in which order it should be stitched in. " )
steps_to_solve = [
_ ( ' * Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects ' )
]
class InvalidShapeError ( ValidationError ) :
name = _ ( " This shape is invalid " )
description = _ ( ' Fill: This shape cannot be stitched out. Please try to repair it with the " Break Apart Fill Objects " extension. ' )
2022-01-30 14:48:51 +00:00
steps_to_solve = [
_ ( ' * Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects ' )
]
class FillStitch ( EmbroideryElement ) :
element_name = _ ( " FillStitch " )
2018-03-31 00:37:11 +00:00
@property
2021-10-29 14:18:22 +00:00
@param ( ' auto_fill ' , _ ( ' Automatically routed fill stitching ' ) , type = ' toggle ' , default = True , sort_index = 1 )
2022-01-30 14:48:51 +00:00
def auto_fill ( self ) :
2021-10-29 14:18:22 +00:00
return self . get_boolean_param ( ' auto_fill ' , True )
2021-10-21 14:24:40 +00:00
@property
2021-10-29 14:18:22 +00:00
@param ( ' fill_method ' , _ ( ' Fill method ' ) , type = ' dropdown ' , default = 0 ,
2022-05-06 02:53:31 +00:00
options = [ _ ( " Auto Fill " ) , _ ( " Contour Fill " ) , _ ( " Guided Fill " ) , _ ( " Legacy Fill " ) ] , sort_index = 2 )
2021-10-21 14:24:40 +00:00
def fill_method ( self ) :
return self . get_int_param ( ' fill_method ' , 0 )
2022-06-22 14:11:12 +00:00
@property
@param ( ' guided_fill_strategy ' , _ ( ' Guided Fill Strategy ' ) , type = ' dropdown ' , default = 0 ,
options = [ _ ( " Copy " ) , _ ( " Parallel Offset " ) ] , select_items = [ ( ' fill_method ' , 2 ) ] , sort_index = 3 ,
tooltip = _ ( ' Copy (the default) will fill the shape with shifted copies of the line. ' +
' Parallel offset will ensure that each line is always a consistent distance from its neighbor. ' +
' Sharp corners may be introduced. ' ) )
def guided_fill_strategy ( self ) :
return self . get_int_param ( ' guided_fill_strategy ' , 0 )
2021-10-21 14:24:40 +00:00
@property
2022-05-08 03:14:55 +00:00
@param ( ' contour_strategy ' , _ ( ' Contour Fill Strategy ' ) , type = ' dropdown ' , default = 0 ,
2022-05-06 02:53:31 +00:00
options = [ _ ( " Inner to Outer " ) , _ ( " Single spiral " ) , _ ( " Double spiral " ) ] , select_items = [ ( ' fill_method ' , 1 ) ] , sort_index = 3 )
def contour_strategy ( self ) :
2022-05-08 03:14:55 +00:00
return self . get_int_param ( ' contour_strategy ' , 0 )
2021-10-21 14:24:40 +00:00
@property
2021-10-29 14:18:22 +00:00
@param ( ' join_style ' , _ ( ' Join Style ' ) , type = ' dropdown ' , default = 0 ,
2022-05-06 02:53:31 +00:00
options = [ _ ( " Round " ) , _ ( " Mitered " ) , _ ( " Beveled " ) ] , select_items = [ ( ' fill_method ' , 1 ) ] , sort_index = 4 )
2021-10-21 14:24:40 +00:00
def join_style ( self ) :
return self . get_int_param ( ' join_style ' , 0 )
@property
2022-05-06 02:53:31 +00:00
@param ( ' avoid_self_crossing ' , _ ( ' Avoid self-crossing ' ) , type = ' boolean ' , default = False , select_items = [ ( ' fill_method ' , 1 ) ] , sort_index = 5 )
2022-05-02 19:00:52 +00:00
def avoid_self_crossing ( self ) :
return self . get_boolean_param ( ' avoid_self_crossing ' , False )
2022-05-03 03:48:46 +00:00
@property
2022-05-06 02:53:31 +00:00
@param ( ' clockwise ' , _ ( ' Clockwise ' ) , type = ' boolean ' , default = True , select_items = [ ( ' fill_method ' , 1 ) ] , sort_index = 5 )
2022-05-03 03:48:46 +00:00
def clockwise ( self ) :
return self . get_boolean_param ( ' clockwise ' , True )
2021-10-21 14:24:40 +00:00
@property
@param ( ' angle ' ,
_ ( ' Angle of lines of stitches ' ) ,
2022-02-02 20:19:31 +00:00
tooltip = _ ( ' The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed. ' ) ,
2021-10-21 14:24:40 +00:00
unit = ' deg ' ,
type = ' float ' ,
2022-05-06 02:53:31 +00:00
sort_index = 6 ,
2022-01-28 21:31:55 +00:00
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 3 ) ] ,
2021-10-21 14:24:40 +00:00
default = 0 )
@cache
def angle ( self ) :
return math . radians ( self . get_float_param ( ' angle ' , 0 ) )
@property
def color ( self ) :
# SVG spec says the default fill is black
return self . get_style ( " fill " , " #000000 " )
@property
@param (
' skip_last ' ,
_ ( ' Skip last stitch in each row ' ) ,
tooltip = _ ( ' The last stitch in each row is quite close to the first stitch in the next row. '
' Skipping it decreases stitch count and density. ' ) ,
type = ' boolean ' ,
2022-05-06 02:53:31 +00:00
sort_index = 6 ,
2022-01-28 21:31:55 +00:00
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 2 ) ,
( ' fill_method ' , 3 ) ] ,
2021-10-21 14:24:40 +00:00
default = False )
def skip_last ( self ) :
return self . get_boolean_param ( " skip_last " , False )
@property
@param (
' flip ' ,
_ ( ' Flip fill (start right-to-left) ' ) ,
tooltip = _ ( ' The flip option can help you with routing your stitch path. '
' When you enable flip, stitching goes from right-to-left instead of left-to-right. ' ) ,
type = ' boolean ' ,
2022-05-06 02:53:31 +00:00
sort_index = 7 ,
2022-05-17 15:33:10 +00:00
select_items = [ ( ' fill_method ' , 3 ) ] ,
2021-10-21 14:24:40 +00:00
default = False )
def flip ( self ) :
return self . get_boolean_param ( " flip " , False )
@property
@param ( ' row_spacing_mm ' ,
_ ( ' Spacing between rows ' ) ,
tooltip = _ ( ' Distance between rows of stitches. ' ) ,
unit = ' mm ' ,
2022-05-06 02:53:31 +00:00
sort_index = 6 ,
2021-10-21 14:24:40 +00:00
type = ' float ' ,
default = 0.25 )
def row_spacing ( self ) :
return max ( self . get_float_param ( " row_spacing_mm " , 0.25 ) , 0.1 * PIXELS_PER_MM )
@property
def end_row_spacing ( self ) :
return self . get_float_param ( " end_row_spacing_mm " )
@property
@param ( ' max_stitch_length_mm ' ,
_ ( ' Maximum fill stitch length ' ) ,
2021-10-29 14:18:22 +00:00
tooltip = _ (
' The length of each stitch in a row. Shorter stitch may be used at the start or end of a row. ' ) ,
2021-10-21 14:24:40 +00:00
unit = ' mm ' ,
2022-05-06 02:53:31 +00:00
sort_index = 6 ,
2021-10-21 14:24:40 +00:00
type = ' float ' ,
default = 3.0 )
def max_stitch_length ( self ) :
return max ( self . get_float_param ( " max_stitch_length_mm " , 3.0 ) , 0.1 * PIXELS_PER_MM )
@property
@param ( ' staggers ' ,
_ ( ' Stagger rows this many times before repeating ' ) ,
2022-10-23 04:54:35 +00:00
tooltip = _ ( ' Length of the cycle by which successive stitch rows are staggered. Fractional values are allowed and can have less visible diagonals than integer values. ' ) ,
2021-10-21 14:24:40 +00:00
type = ' int ' ,
2022-05-06 02:53:31 +00:00
sort_index = 6 ,
2022-06-22 14:11:12 +00:00
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 2 ) , ( ' fill_method ' , 3 ) ] ,
2021-10-21 14:24:40 +00:00
default = 4 )
def staggers ( self ) :
2022-10-23 04:54:35 +00:00
return self . get_float_param ( " staggers " , 4 )
2021-10-21 14:24:40 +00:00
@property
@cache
def paths ( self ) :
paths = self . flatten ( self . parse_path ( ) )
# ensure path length
for i , path in enumerate ( paths ) :
if len ( path ) < 3 :
2022-05-07 20:20:15 +00:00
paths [ i ] = [ ( path [ 0 ] [ 0 ] , path [ 0 ] [ 1 ] ) , ( path [ 0 ] [ 0 ] + 1.0 , path [ 0 ] [ 1 ] ) , ( path [ 0 ] [ 0 ] , path [ 0 ] [ 1 ] + 1.0 ) ]
2021-10-21 14:24:40 +00:00
return paths
2022-01-30 14:48:51 +00:00
@property
@cache
2022-06-21 17:59:26 +00:00
def original_shape ( self ) :
2022-01-30 14:48:51 +00:00
# shapely's idea of "holes" are to subtract everything in the second set
# from the first. So let's at least make sure the "first" thing is the
# biggest path.
paths = self . paths
2022-06-21 17:59:26 +00:00
paths . sort ( key = lambda point_list : shgeo . Polygon ( point_list ) . area , reverse = True )
2022-01-30 14:48:51 +00:00
# Very small holes will cause a shape to be rendered as an outline only
# they are too small to be rendered and only confuse the auto_fill algorithm.
# So let's ignore them
if shgeo . Polygon ( paths [ 0 ] ) . area > 5 and shgeo . Polygon ( paths [ - 1 ] ) . area < 5 :
paths = [ path for path in paths if shgeo . Polygon ( path ) . area > 3 ]
2022-06-21 17:59:26 +00:00
return shgeo . MultiPolygon ( [ ( paths [ 0 ] , paths [ 1 : ] ) ] )
@property
@cache
def shape ( self ) :
shape = self . _get_clipped_path ( )
if self . shape_is_valid ( shape ) :
return shape
# Repair not valid shapes
logger = logging . getLogger ( ' shapely.geos ' )
level = logger . level
logger . setLevel ( logging . CRITICAL )
valid_shape = make_valid ( shape )
logger . setLevel ( level )
2022-06-30 17:22:33 +00:00
if isinstance ( valid_shape , shgeo . Polygon ) :
return shgeo . MultiPolygon ( [ valid_shape ] )
2022-07-14 14:23:46 +00:00
if isinstance ( valid_shape , shgeo . LineString ) :
return shgeo . MultiPolygon ( [ ] )
2022-06-30 17:22:33 +00:00
2022-06-21 17:59:26 +00:00
polygons = [ ]
for polygon in valid_shape . geoms :
if isinstance ( polygon , shgeo . Polygon ) :
polygons . append ( polygon )
if isinstance ( polygon , shgeo . MultiPolygon ) :
polygons . extend ( polygon . geoms )
return shgeo . MultiPolygon ( polygons )
def _get_clipped_path ( self ) :
if self . node . clip is None :
return self . original_shape
from . element import EmbroideryElement
clip_element = EmbroideryElement ( self . node . clip )
clip_element . paths . sort ( key = lambda point_list : shgeo . Polygon ( point_list ) . area , reverse = True )
polygon = shgeo . MultiPolygon ( [ ( clip_element . paths [ 0 ] , clip_element . paths [ 1 : ] ) ] )
try :
intersection = polygon . intersection ( self . original_shape )
except TopologicalError :
return self . original_shape
if isinstance ( intersection , shgeo . Polygon ) :
return shgeo . MultiPolygon ( [ intersection ] )
if isinstance ( intersection , shgeo . MultiPolygon ) :
return intersection
polygons = [ ]
if isinstance ( intersection , shgeo . GeometryCollection ) :
for geom in intersection . geoms :
if isinstance ( geom , shgeo . Polygon ) :
polygons . append ( geom )
return shgeo . MultiPolygon ( [ polygons ] )
2022-01-30 14:48:51 +00:00
def shape_is_valid ( self , shape ) :
# Shapely will log to stdout to complain about the shape unless we make
# it shut up.
logger = logging . getLogger ( ' shapely.geos ' )
level = logger . level
logger . setLevel ( logging . CRITICAL )
valid = shape . is_valid
logger . setLevel ( level )
return valid
def validation_errors ( self ) :
if not self . shape_is_valid ( self . shape ) :
why = explain_validity ( self . shape )
message , x , y = re . findall ( r " .+?(?= \ [)|-? \ d+(?: \ . \ d+)? " , why )
2022-06-21 17:59:26 +00:00
yield InvalidShapeError ( ( x , y ) )
2022-01-30 14:48:51 +00:00
2022-06-21 17:59:26 +00:00
def validation_warnings ( self ) : # noqa: C901
if not self . shape_is_valid ( self . original_shape ) :
why = explain_validity ( self . original_shape )
message , x , y = re . findall ( r " .+?(?= \ [)|-? \ d+(?: \ . \ d+)? " , why )
2022-01-30 14:48:51 +00:00
if " Hole lies outside shell " in message :
2022-06-21 17:59:26 +00:00
yield UnconnectedWarning ( ( x , y ) )
2022-01-30 14:48:51 +00:00
else :
2022-06-21 17:59:26 +00:00
yield BorderCrossWarning ( ( x , y ) )
2022-01-30 14:48:51 +00:00
2022-06-21 17:59:26 +00:00
for shape in self . shape . geoms :
if self . shape . area < 20 :
label = self . node . get ( INKSCAPE_LABEL ) or self . node . get ( " id " )
yield SmallShapeWarning ( shape . centroid , label )
2022-01-30 14:48:51 +00:00
2022-06-21 17:59:26 +00:00
if self . shrink_or_grow_shape ( shape , self . expand , True ) . is_empty :
yield ExpandWarning ( shape . centroid )
2022-01-30 14:48:51 +00:00
2022-06-21 17:59:26 +00:00
if self . shrink_or_grow_shape ( shape , - self . fill_underlay_inset , True ) . is_empty :
yield UnderlayInsetWarning ( shape . centroid )
2022-01-30 14:48:51 +00:00
# guided fill warnings
if self . fill_method == 2 :
guide_lines = self . _get_guide_lines ( True )
if not guide_lines or guide_lines [ 0 ] . is_empty :
yield MissingGuideLineWarning ( self . shape . centroid )
elif len ( guide_lines ) > 1 :
yield MultipleGuideLineWarning ( self . shape . centroid )
elif guide_lines [ 0 ] . disjoint ( self . shape ) :
yield DisjointGuideLineWarning ( self . shape . centroid )
return None
for warning in super ( FillStitch , self ) . validation_warnings ( ) :
yield warning
2018-03-31 00:37:11 +00:00
@property
@cache
def outline ( self ) :
return self . shape . boundary [ 0 ]
@property
@cache
def outline_length ( self ) :
return self . outline . length
@property
2018-08-09 18:32:41 +00:00
@param ( ' running_stitch_length_mm ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Running stitch length (traversal between sections) ' ) ,
2022-02-02 20:19:31 +00:00
tooltip = _ ( ' Length of stitches around the outline of the fill region used when moving from section to section. ' ) ,
2018-08-22 00:32:50 +00:00
unit = ' mm ' ,
type = ' float ' ,
2021-10-21 14:24:40 +00:00
default = 1.5 ,
2021-10-29 14:18:22 +00:00
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 2 ) ] ,
2022-05-06 02:53:31 +00:00
sort_index = 6 )
2018-03-31 00:37:11 +00:00
def running_stitch_length ( self ) :
return max ( self . get_float_param ( " running_stitch_length_mm " , 1.5 ) , 0.01 )
2022-06-22 13:26:37 +00:00
@property
@param ( ' running_stitch_tolerance_mm ' ,
_ ( ' Running stitch tolerance ' ) ,
tooltip = _ ( ' All stitches must be within this distance of the path. ' +
' A lower tolerance means stitches will be closer together. ' +
' A higher tolerance means sharp corners may be rounded. ' ) ,
unit = ' mm ' ,
type = ' float ' ,
default = 0.2 ,
sort_index = 6 )
def running_stitch_tolerance ( self ) :
return max ( self . get_float_param ( " running_stitch_tolerance_mm " , 0.2 ) , 0.01 )
2018-03-31 00:37:11 +00:00
@property
2022-05-17 15:33:10 +00:00
@param ( ' fill_underlay ' , _ ( ' Underlay ' ) , type = ' toggle ' , group = _ ( ' Fill Underlay ' ) , default = True )
2018-03-31 00:37:11 +00:00
def fill_underlay ( self ) :
2020-04-25 12:45:27 +00:00
return self . get_boolean_param ( " fill_underlay " , default = True )
2018-03-31 00:37:11 +00:00
@property
2018-08-09 18:32:41 +00:00
@param ( ' fill_underlay_angle ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Fill angle ' ) ,
2022-02-02 20:19:31 +00:00
tooltip = _ ( ' Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers. ' ) ,
2018-08-22 00:32:50 +00:00
unit = ' deg ' ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) ,
2018-08-22 00:32:50 +00:00
type = ' float ' )
2018-03-31 00:37:11 +00:00
@cache
def fill_underlay_angle ( self ) :
2020-03-16 17:38:10 +00:00
underlay_angles = self . get_param ( ' fill_underlay_angle ' , None )
default_value = [ self . angle + math . pi / 2.0 ]
if underlay_angles is not None :
underlay_angles = underlay_angles . strip ( ) . split ( ' , ' )
try :
2021-10-29 14:18:22 +00:00
underlay_angles = [ math . radians (
float ( angle ) ) for angle in underlay_angles ]
2020-03-16 17:38:10 +00:00
except ( TypeError , ValueError ) :
return default_value
2018-03-31 00:37:11 +00:00
else :
2020-03-16 17:38:10 +00:00
underlay_angles = default_value
return underlay_angles
2018-03-31 00:37:11 +00:00
@property
2018-08-09 18:32:41 +00:00
@param ( ' fill_underlay_row_spacing_mm ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Row spacing ' ) ,
tooltip = _ ( ' default: 3x fill row spacing ' ) ,
unit = ' mm ' ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) ,
2018-08-22 00:32:50 +00:00
type = ' float ' )
2018-03-31 00:37:11 +00:00
@cache
def fill_underlay_row_spacing ( self ) :
return self . get_float_param ( " fill_underlay_row_spacing_mm " ) or self . row_spacing * 3
@property
2018-08-09 18:32:41 +00:00
@param ( ' fill_underlay_max_stitch_length_mm ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Max stitch length ' ) ,
tooltip = _ ( ' default: equal to fill max stitch length ' ) ,
unit = ' mm ' ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) , type = ' float ' )
2018-03-31 00:37:11 +00:00
@cache
def fill_underlay_max_stitch_length ( self ) :
return self . get_float_param ( " fill_underlay_max_stitch_length_mm " ) or self . max_stitch_length
@property
2018-06-02 00:34:27 +00:00
@param ( ' fill_underlay_inset_mm ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Inset ' ) ,
2022-02-02 20:19:31 +00:00
tooltip = _ ( ' Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill. ' ) ,
2018-08-22 00:32:50 +00:00
unit = ' mm ' ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) ,
2018-08-22 00:32:50 +00:00
type = ' float ' ,
default = 0 )
2018-03-31 00:37:11 +00:00
def fill_underlay_inset ( self ) :
return self . get_float_param ( ' fill_underlay_inset_mm ' , 0 )
2018-12-13 01:26:22 +00:00
@property
@param (
' fill_underlay_skip_last ' ,
_ ( ' Skip last stitch in each row ' ) ,
tooltip = _ ( ' The last stitch in each row is quite close to the first stitch in the next row. '
' Skipping it decreases stitch count and density. ' ) ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) ,
2018-12-13 01:26:22 +00:00
type = ' boolean ' ,
default = False )
def fill_underlay_skip_last ( self ) :
return self . get_boolean_param ( " fill_underlay_skip_last " , False )
2018-03-31 00:37:11 +00:00
@property
2018-06-02 00:34:27 +00:00
@param ( ' expand_mm ' ,
2018-08-22 00:32:50 +00:00
_ ( ' Expand ' ) ,
2022-02-02 20:19:31 +00:00
tooltip = _ ( ' Expand the shape before fill stitching, to compensate for gaps between shapes. ' ) ,
2018-08-22 00:32:50 +00:00
unit = ' mm ' ,
type = ' float ' ,
2021-10-21 14:24:40 +00:00
default = 0 ,
2021-10-29 14:18:22 +00:00
sort_index = 5 ,
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 2 ) ] )
2018-06-02 00:34:27 +00:00
def expand ( self ) :
return self . get_float_param ( ' expand_mm ' , 0 )
2019-03-20 02:30:07 +00:00
@property
@param ( ' underpath ' ,
_ ( ' Underpath ' ) ,
tooltip = _ ( ' Travel inside the shape when moving from section to section. Underpath '
' stitches avoid traveling in the direction of the row angle so that they '
' are not visible. This gives them a jagged appearance. ' ) ,
type = ' boolean ' ,
2021-10-21 14:24:40 +00:00
default = True ,
2021-10-29 14:18:22 +00:00
select_items = [ ( ' fill_method ' , 0 ) , ( ' fill_method ' , 2 ) ] ,
sort_index = 6 )
2019-03-20 02:30:07 +00:00
def underpath ( self ) :
return self . get_boolean_param ( ' underpath ' , True )
@property
2019-03-22 01:07:48 +00:00
@param (
' underlay_underpath ' ,
_ ( ' Underpath ' ) ,
tooltip = _ ( ' Travel inside the shape when moving from section to section. Underpath '
' stitches avoid traveling in the direction of the row angle so that they '
' are not visible. This gives them a jagged appearance. ' ) ,
2022-05-17 15:33:10 +00:00
group = _ ( ' Fill Underlay ' ) ,
2019-03-22 01:07:48 +00:00
type = ' boolean ' ,
2019-03-22 01:25:14 +00:00
default = True )
2019-03-20 02:30:07 +00:00
def underlay_underpath ( self ) :
2019-03-31 01:56:39 +00:00
return self . get_boolean_param ( ' underlay_underpath ' , True )
2019-03-20 02:30:07 +00:00
2022-06-21 17:59:26 +00:00
def shrink_or_grow_shape ( self , shape , amount , validate = False ) :
2022-06-30 17:22:33 +00:00
new_shape = shape
2018-06-02 00:34:27 +00:00
if amount :
2022-06-30 17:22:33 +00:00
new_shape = shape . buffer ( amount )
2021-03-04 17:40:53 +00:00
# changing the size can empty the shape
# in this case we want to use the original shape rather than returning an error
2022-06-30 17:22:33 +00:00
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
2018-03-31 00:37:11 +00:00
2022-06-21 17:59:26 +00:00
def underlay_shape ( self , shape ) :
return self . shrink_or_grow_shape ( shape , - self . fill_underlay_inset )
2018-06-02 00:34:27 +00:00
2022-06-21 17:59:26 +00:00
def fill_shape ( self , shape ) :
return self . shrink_or_grow_shape ( shape , self . expand )
2018-06-02 00:34:27 +00:00
2018-06-23 02:29:23 +00:00
def get_starting_point ( self , last_patch ) :
# If there is a "fill_start" Command, then use that; otherwise pick
# the point closest to the end of the last patch.
if self . get_command ( ' fill_start ' ) :
return self . get_command ( ' fill_start ' ) . target_point
elif last_patch :
return last_patch . stitches [ - 1 ]
else :
return None
2018-06-23 02:31:42 +00:00
def get_ending_point ( self ) :
if self . get_command ( ' fill_end ' ) :
return self . get_command ( ' fill_end ' ) . target_point
else :
return None
2022-06-30 17:22:33 +00:00
def to_stitch_groups ( self , last_patch ) : # noqa: C901
2022-01-30 14:48:51 +00:00
# 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 = [ ]
end = self . get_ending_point ( )
2018-03-31 00:37:11 +00:00
2022-06-21 17:59:26 +00:00
for shape in self . shape . geoms :
2022-06-30 17:22:33 +00:00
start = self . get_starting_point ( last_patch )
2022-06-21 17:59:26 +00:00
try :
if self . fill_underlay :
2022-06-30 17:22:33 +00:00
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 ( fill_shape , last_patch , start , end ) )
if self . fill_method == 1 :
stitch_groups . extend ( self . do_contour_fill ( fill_shape , last_patch , start ) )
elif self . fill_method == 2 :
stitch_groups . extend ( self . do_guided_fill ( fill_shape , last_patch , start , end ) )
2022-06-21 17:59:26 +00:00
except Exception :
self . fatal_fill_error ( )
last_patch = stitch_groups [ - 1 ]
2022-01-30 14:48:51 +00:00
return stitch_groups
def do_legacy_fill ( self ) :
stitch_lists = legacy_fill ( self . shape ,
self . angle ,
self . row_spacing ,
self . end_row_spacing ,
self . max_stitch_length ,
self . flip ,
self . staggers ,
self . skip_last )
return [ StitchGroup ( stitches = stitch_list , color = self . color ) for stitch_list in stitch_lists ]
2022-06-21 17:59:26 +00:00
def do_underlay ( self , shape , starting_point ) :
2022-01-30 14:48:51 +00:00
stitch_groups = [ ]
for i in range ( len ( self . fill_underlay_angle ) ) :
underlay = StitchGroup (
color = self . color ,
tags = ( " auto_fill " , " auto_fill_underlay " ) ,
stitches = auto_fill (
2022-06-30 17:22:33 +00:00
shape ,
2022-01-30 14:48:51 +00:00
self . fill_underlay_angle [ i ] ,
self . fill_underlay_row_spacing ,
self . fill_underlay_row_spacing ,
self . fill_underlay_max_stitch_length ,
self . running_stitch_length ,
2022-06-22 13:26:37 +00:00
self . running_stitch_tolerance ,
2022-01-30 14:48:51 +00:00
self . staggers ,
self . fill_underlay_skip_last ,
starting_point ,
underpath = self . underlay_underpath ) )
stitch_groups . append ( underlay )
starting_point = underlay . stitches [ - 1 ]
return [ stitch_groups , starting_point ]
2022-06-21 17:59:26 +00:00
def do_auto_fill ( self , shape , last_patch , starting_point , ending_point ) :
2022-01-30 14:48:51 +00:00
stitch_group = StitchGroup (
color = self . color ,
tags = ( " auto_fill " , " auto_fill_top " ) ,
stitches = auto_fill (
2022-06-30 17:22:33 +00:00
shape ,
2022-01-30 14:48:51 +00:00
self . angle ,
self . row_spacing ,
self . end_row_spacing ,
self . max_stitch_length ,
self . running_stitch_length ,
2022-06-22 13:26:37 +00:00
self . running_stitch_tolerance ,
2022-01-30 14:48:51 +00:00
self . staggers ,
self . skip_last ,
starting_point ,
ending_point ,
self . underpath ) )
return [ stitch_group ]
2022-06-21 17:59:26 +00:00
def do_contour_fill ( self , polygon , last_patch , starting_point ) :
2022-01-30 14:48:51 +00:00
if not starting_point :
starting_point = ( 0 , 0 )
2022-05-04 01:08:48 +00:00
starting_point = shgeo . Point ( starting_point )
stitch_groups = [ ]
2022-06-21 17:59:26 +00:00
tree = contour_fill . offset_polygon ( polygon , self . row_spacing , self . join_style + 1 , self . clockwise )
stitches = [ ]
if self . contour_strategy == 0 :
stitches = contour_fill . inner_to_outer (
tree ,
self . row_spacing ,
self . max_stitch_length ,
2022-06-22 13:26:37 +00:00
self . running_stitch_tolerance ,
2022-06-21 17:59:26 +00:00
starting_point ,
self . avoid_self_crossing
)
elif self . contour_strategy == 1 :
stitches = contour_fill . single_spiral (
tree ,
self . max_stitch_length ,
2022-06-22 13:26:37 +00:00
self . running_stitch_tolerance ,
2022-06-21 17:59:26 +00:00
starting_point
)
elif self . contour_strategy == 2 :
stitches = contour_fill . double_spiral (
tree ,
self . max_stitch_length ,
2022-06-22 13:26:37 +00:00
self . running_stitch_tolerance ,
2022-06-21 17:59:26 +00:00
starting_point
)
stitch_group = StitchGroup (
color = self . color ,
tags = ( " auto_fill " , " auto_fill_top " ) ,
stitches = stitches )
stitch_groups . append ( stitch_group )
2018-03-31 00:37:11 +00:00
2021-08-15 21:24:59 +00:00
return stitch_groups
2019-08-06 02:42:48 +00:00
2022-06-21 17:59:26 +00:00
def do_guided_fill ( self , shape , last_patch , starting_point , ending_point ) :
2022-01-30 14:48:51 +00:00
guide_line = self . _get_guide_lines ( )
# No guide line: fallback to normal autofill
if not guide_line :
2022-06-22 14:11:12 +00:00
return self . do_auto_fill ( shape , last_patch , starting_point , ending_point )
2022-01-30 14:48:51 +00:00
stitch_group = StitchGroup (
color = self . color ,
2022-02-18 14:36:01 +00:00
tags = ( " guided_fill " , " auto_fill_top " ) ,
stitches = guided_fill (
2022-06-30 17:22:33 +00:00
shape ,
2022-01-30 14:48:51 +00:00
guide_line . geoms [ 0 ] ,
self . angle ,
self . row_spacing ,
2022-06-22 14:11:12 +00:00
self . staggers ,
2022-01-30 14:48:51 +00:00
self . max_stitch_length ,
self . running_stitch_length ,
2022-06-22 14:11:12 +00:00
self . running_stitch_tolerance ,
2022-01-30 14:48:51 +00:00
self . skip_last ,
starting_point ,
ending_point ,
2022-06-22 14:11:12 +00:00
self . underpath ,
self . guided_fill_strategy ,
) )
2022-01-30 14:48:51 +00:00
return [ stitch_group ]
2019-08-06 02:42:48 +00:00
2022-01-30 14:48:51 +00:00
@cache
def _get_guide_lines ( self , multiple = False ) :
guide_lines = get_marker_elements ( self . node , " guide-line " , False , True )
# No or empty guide line
2022-02-01 18:47:19 +00:00
if not guide_lines or not guide_lines [ ' stroke ' ] :
2022-01-30 14:48:51 +00:00
return None
2022-02-01 18:47:19 +00:00
2022-01-30 14:48:51 +00:00
if multiple :
return guide_lines [ ' stroke ' ]
else :
return guide_lines [ ' stroke ' ] [ 0 ]
def fatal_fill_error ( self ) :
if hasattr ( sys , ' gettrace ' ) and sys . gettrace ( ) :
# if we're debugging, let the exception bubble up
raise
# for an uncaught exception, give a little more info so that they can create a bug report
message = " "
message + = _ ( " Error during autofill! This means that there is a problem with Ink/Stitch. " )
message + = " \n \n "
# L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
message + = _ ( " If you ' d like to help us make Ink/Stitch better, please paste this whole message into a new issue at: " )
message + = " https://github.com/inkstitch/inkstitch/issues/new \n \n "
message + = version . get_inkstitch_version ( ) + " \n \n "
message + = traceback . format_exc ( )
self . fatal ( message )