kopia lustrzana https://github.com/inkstitch/inkstitch
refactor, step 4
rodzic
0dc3ffefe4
commit
a47073e3ee
907
embroider.py
907
embroider.py
|
@ -14,882 +14,18 @@ import sys
|
|||
import traceback
|
||||
sys.path.append("/usr/share/inkscape/extensions")
|
||||
import os
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
import time
|
||||
from itertools import chain, izip, groupby
|
||||
from collections import deque
|
||||
import inkex
|
||||
import simplepath
|
||||
import simplestyle
|
||||
import simpletransform
|
||||
from bezmisc import bezierlength, beziertatlength, bezierpointatt
|
||||
from cspsubdiv import cspsubdiv
|
||||
import cubicsuperpath
|
||||
import math
|
||||
import lxml.etree as etree
|
||||
import shapely.geometry as shgeo
|
||||
import shapely.affinity as affinity
|
||||
import shapely.ops
|
||||
import networkx
|
||||
from pprint import pformat
|
||||
|
||||
import inkex
|
||||
import inkstitch
|
||||
from inkstitch import _, cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform
|
||||
from inkstitch.stitches import running_stitch, auto_fill, legacy_fill
|
||||
from inkstitch import _, PIXELS_PER_MM
|
||||
from inkstitch.extensions import InkstitchExtension
|
||||
from inkstitch.stitch_plan import patches_to_stitch_plan
|
||||
from inkstitch.svg import render_stitch_plan
|
||||
|
||||
class Fill(EmbroideryElement):
|
||||
element_name = _("Fill")
|
||||
|
||||
class Embroider(InkstitchExtension):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Fill, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True)
|
||||
def auto_fill(self):
|
||||
return self.get_boolean_param('auto_fill', True)
|
||||
|
||||
@property
|
||||
@param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0)
|
||||
@cache
|
||||
def angle(self):
|
||||
return math.radians(self.get_float_param('angle', 0))
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("fill")
|
||||
|
||||
@property
|
||||
@param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False)
|
||||
def flip(self):
|
||||
return self.get_boolean_param("flip", False)
|
||||
|
||||
@property
|
||||
@param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25)
|
||||
def row_spacing(self):
|
||||
return max(self.get_float_param("row_spacing_mm", 0.25), 0.01)
|
||||
|
||||
@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'), unit='mm', type='float', default=3.0)
|
||||
def max_stitch_length(self):
|
||||
return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01)
|
||||
|
||||
@property
|
||||
@param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4)
|
||||
def staggers(self):
|
||||
return self.get_int_param("staggers", 4)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def paths(self):
|
||||
return self.flatten(self.parse_path())
|
||||
|
||||
@property
|
||||
@cache
|
||||
def shape(self):
|
||||
poly_ary = []
|
||||
for sub_path in self.paths:
|
||||
point_ary = []
|
||||
last_pt = None
|
||||
for pt in sub_path:
|
||||
if (last_pt is not None):
|
||||
vp = (pt[0] - last_pt[0], pt[1] - last_pt[1])
|
||||
dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0))
|
||||
# dbg.write("dp %s\n" % dp)
|
||||
if (dp > 0.01):
|
||||
# I think too-close points confuse shapely.
|
||||
point_ary.append(pt)
|
||||
last_pt = pt
|
||||
else:
|
||||
last_pt = pt
|
||||
if point_ary:
|
||||
poly_ary.append(point_ary)
|
||||
|
||||
# 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.
|
||||
# TODO: actually figure out which things are holes and which are shells
|
||||
poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True)
|
||||
|
||||
polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])])
|
||||
# print >> sys.stderr, "polygon valid:", polygon.is_valid
|
||||
return polygon
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
stitch_lists = legacy_fill(self.shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.max_stitch_length,
|
||||
self.flip,
|
||||
self.staggers)
|
||||
return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]
|
||||
|
||||
rows_of_segments = fill.intersect_region_with_grating(self.shape, self.angle, self.row_spacing, self.end_row_spacing, self.flip)
|
||||
groups_of_segments = fill.pull_runs(rows_of_segments)
|
||||
|
||||
return [fill.section_to_patch(group) for group in groups_of_segments]
|
||||
|
||||
|
||||
class AutoFill(Fill):
|
||||
element_name = _("Auto-Fill")
|
||||
|
||||
@property
|
||||
@param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True)
|
||||
def auto_fill(self):
|
||||
return self.get_boolean_param('auto_fill', True)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def outline(self):
|
||||
return self.shape.boundary[0]
|
||||
|
||||
@property
|
||||
@cache
|
||||
def outline_length(self):
|
||||
return self.outline.length
|
||||
|
||||
@property
|
||||
def flip(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False)
|
||||
def fill_underlay(self):
|
||||
return self.get_boolean_param("fill_underlay", default=False)
|
||||
|
||||
@property
|
||||
@param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float')
|
||||
@cache
|
||||
def fill_underlay_angle(self):
|
||||
underlay_angle = self.get_float_param("fill_underlay_angle")
|
||||
|
||||
if underlay_angle:
|
||||
return math.radians(underlay_angle)
|
||||
else:
|
||||
return self.angle + math.pi / 2.0
|
||||
|
||||
@property
|
||||
@param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float')
|
||||
@cache
|
||||
def fill_underlay_row_spacing(self):
|
||||
return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3
|
||||
|
||||
@property
|
||||
@param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float')
|
||||
@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
|
||||
@param('fill_underlay_inset_mm', _('Inset'), unit='mm', group=_('AutoFill Underlay'), type='float', default=0)
|
||||
def fill_underlay_inset(self):
|
||||
return self.get_float_param('fill_underlay_inset_mm', 0)
|
||||
|
||||
@property
|
||||
def underlay_shape(self):
|
||||
if self.fill_underlay_inset:
|
||||
shape = self.shape.buffer(-self.fill_underlay_inset)
|
||||
if not isinstance(shape, shgeo.MultiPolygon):
|
||||
shape = shgeo.MultiPolygon([shape])
|
||||
return shape
|
||||
else:
|
||||
return self.shape
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
stitches = []
|
||||
|
||||
if last_patch is None:
|
||||
starting_point = None
|
||||
else:
|
||||
starting_point = last_patch.stitches[-1]
|
||||
|
||||
if self.fill_underlay:
|
||||
stitches.extend(auto_fill(self.underlay_shape,
|
||||
self.fill_underlay_angle,
|
||||
self.fill_underlay_row_spacing,
|
||||
self.fill_underlay_row_spacing,
|
||||
self.fill_underlay_max_stitch_length,
|
||||
self.running_stitch_length,
|
||||
self.staggers,
|
||||
starting_point))
|
||||
starting_point = stitches[-1]
|
||||
|
||||
stitches.extend(auto_fill(self.shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.max_stitch_length,
|
||||
self.running_stitch_length,
|
||||
self.staggers,
|
||||
starting_point))
|
||||
|
||||
return [Patch(stitches=stitches, color=self.color)]
|
||||
|
||||
|
||||
class Stroke(EmbroideryElement):
|
||||
element_name = "Stroke"
|
||||
|
||||
@property
|
||||
@param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True)
|
||||
def satin_column(self):
|
||||
return self.get_boolean_param("satin_column")
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("stroke")
|
||||
|
||||
@property
|
||||
@cache
|
||||
def width(self):
|
||||
stroke_width = self.get_style("stroke-width")
|
||||
|
||||
if stroke_width.endswith("px"):
|
||||
stroke_width = stroke_width[:-2]
|
||||
|
||||
return float(stroke_width)
|
||||
|
||||
@property
|
||||
def dashed(self):
|
||||
return self.get_style("stroke-dasharray") is not None
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4)
|
||||
@cache
|
||||
def zigzag_spacing(self):
|
||||
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
|
||||
|
||||
@property
|
||||
@param('repeats', _('Repeats'), type='int', default="1")
|
||||
def repeats(self):
|
||||
return self.get_int_param("repeats", 1)
|
||||
|
||||
@property
|
||||
def paths(self):
|
||||
return self.flatten(self.parse_path())
|
||||
|
||||
def is_running_stitch(self):
|
||||
# stroke width <= 0.5 pixels is deprecated in favor of dashed lines
|
||||
return self.dashed or self.width <= 0.5
|
||||
|
||||
def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width):
|
||||
patch = Patch(color=self.color)
|
||||
p0 = emb_point_list[0]
|
||||
rho = 0.0
|
||||
side = 1
|
||||
last_segment_direction = None
|
||||
|
||||
for repeat in xrange(self.repeats):
|
||||
if repeat % 2 == 0:
|
||||
order = range(1, len(emb_point_list))
|
||||
else:
|
||||
order = range(-2, -len(emb_point_list) - 1, -1)
|
||||
|
||||
for segi in order:
|
||||
p1 = emb_point_list[segi]
|
||||
|
||||
# how far we have to go along segment
|
||||
seg_len = (p1 - p0).length()
|
||||
if (seg_len == 0):
|
||||
continue
|
||||
|
||||
# vector pointing along segment
|
||||
along = (p1 - p0).unit()
|
||||
|
||||
# vector pointing to edge of stroke width
|
||||
perp = along.rotate_left() * (stroke_width * 0.5)
|
||||
|
||||
if stroke_width == 0.0 and last_segment_direction is not None:
|
||||
if abs(1.0 - along * last_segment_direction) > 0.5:
|
||||
# if greater than 45 degree angle, stitch the corner
|
||||
rho = zigzag_spacing
|
||||
patch.add_stitch(p0)
|
||||
|
||||
# iteration variable: how far we are along segment
|
||||
while (rho <= seg_len):
|
||||
left_pt = p0 + along * rho + perp * side
|
||||
patch.add_stitch(left_pt)
|
||||
rho += zigzag_spacing
|
||||
side = -side
|
||||
|
||||
p0 = p1
|
||||
last_segment_direction = along
|
||||
rho -= seg_len
|
||||
|
||||
if (p0 - patch.stitches[-1]).length() > 0.1:
|
||||
patch.add_stitch(p0)
|
||||
|
||||
return patch
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
patches = []
|
||||
|
||||
for path in self.paths:
|
||||
path = [inkstitch.Point(x, y) for x, y in path]
|
||||
if self.is_running_stitch():
|
||||
patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0)
|
||||
else:
|
||||
patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.width)
|
||||
|
||||
patches.append(patch)
|
||||
|
||||
return patches
|
||||
|
||||
|
||||
class SatinColumn(EmbroideryElement):
|
||||
element_name = _("Satin Column")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SatinColumn, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@param('satin_column', _('Custom satin column'), type='toggle')
|
||||
def satin_column(self):
|
||||
return self.get_boolean_param("satin_column")
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("stroke")
|
||||
|
||||
@property
|
||||
@param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4)
|
||||
def zigzag_spacing(self):
|
||||
# peak-to-peak distance between zigzags
|
||||
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
|
||||
|
||||
@property
|
||||
@param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float')
|
||||
def pull_compensation(self):
|
||||
# In satin stitch, the stitches have a tendency to pull together and
|
||||
# narrow the entire column. We can compensate for this by stitching
|
||||
# wider than we desire the column to end up.
|
||||
return self.get_float_param("pull_compensation_mm", 0)
|
||||
|
||||
@property
|
||||
@param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay'))
|
||||
def contour_underlay(self):
|
||||
# "Contour underlay" is stitching just inside the rectangular shape
|
||||
# of the satin column; that is, up one side and down the other.
|
||||
return self.get_boolean_param("contour_underlay")
|
||||
|
||||
@property
|
||||
@param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5)
|
||||
def contour_underlay_stitch_length(self):
|
||||
return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4)
|
||||
def contour_underlay_inset(self):
|
||||
# how far inside the edge of the column to stitch the underlay
|
||||
return self.get_float_param("contour_underlay_inset_mm", 0.4)
|
||||
|
||||
@property
|
||||
@param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay'))
|
||||
def center_walk_underlay(self):
|
||||
# "Center walk underlay" is stitching down and back in the centerline
|
||||
# between the two sides of the satin column.
|
||||
return self.get_boolean_param("center_walk_underlay")
|
||||
|
||||
@property
|
||||
@param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5)
|
||||
def center_walk_underlay_stitch_length(self):
|
||||
return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay'))
|
||||
def zigzag_underlay(self):
|
||||
return self.get_boolean_param("zigzag_underlay")
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3)
|
||||
def zigzag_underlay_spacing(self):
|
||||
return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float')
|
||||
def zigzag_underlay_inset(self):
|
||||
# how far in from the edge of the satin the points in the zigzags
|
||||
# should be
|
||||
|
||||
# Default to half of the contour underlay inset. That is, if we're
|
||||
# doing both contour underlay and zigzag underlay, make sure the
|
||||
# points of the zigzag fall outside the contour underlay but inside
|
||||
# the edges of the satin column.
|
||||
return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0
|
||||
|
||||
@property
|
||||
@cache
|
||||
def csp(self):
|
||||
return self.parse_path()
|
||||
|
||||
@property
|
||||
@cache
|
||||
def flattened_beziers(self):
|
||||
if len(self.csp) == 2:
|
||||
return self.simple_flatten_beziers()
|
||||
else:
|
||||
return self.flatten_beziers_with_rungs()
|
||||
|
||||
|
||||
def flatten_beziers_with_rungs(self):
|
||||
input_paths = [self.flatten([path]) for path in self.csp]
|
||||
input_paths = [shgeo.LineString(path[0]) for path in input_paths]
|
||||
|
||||
paths = input_paths[:]
|
||||
paths.sort(key=lambda path: path.length, reverse=True)
|
||||
|
||||
# Imagine a satin column as a curvy ladder.
|
||||
# The two long paths are the "rails" of the ladder. The remainder are
|
||||
# the "rungs".
|
||||
rails = paths[:2]
|
||||
rungs = shgeo.MultiLineString(paths[2:])
|
||||
|
||||
# The rails should stay in the order they were in the original CSP.
|
||||
# (this lets the user control where the satin starts and ends)
|
||||
rails.sort(key=lambda rail: input_paths.index(rail))
|
||||
|
||||
result = []
|
||||
|
||||
for rail in rails:
|
||||
if not rail.is_simple:
|
||||
self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns."))
|
||||
|
||||
# handle null intersections here?
|
||||
linestrings = shapely.ops.split(rail, rungs)
|
||||
|
||||
print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs]
|
||||
if len(linestrings.geoms) < len(rungs.geoms) + 1:
|
||||
self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once."))
|
||||
elif len(linestrings.geoms) > len(rungs.geoms) + 1:
|
||||
self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once."))
|
||||
|
||||
paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
|
||||
result.append(paths)
|
||||
|
||||
return zip(*result)
|
||||
|
||||
|
||||
def simple_flatten_beziers(self):
|
||||
# Given a pair of paths made up of bezier segments, flatten
|
||||
# each individual bezier segment into line segments that approximate
|
||||
# the curves. Retain the divisions between beziers -- we'll use those
|
||||
# later.
|
||||
|
||||
paths = []
|
||||
|
||||
for path in self.csp:
|
||||
# See the documentation in the parent class for parse_path() for a
|
||||
# description of the format of the CSP. Each bezier is constructed
|
||||
# using two neighboring 3-tuples in the list.
|
||||
|
||||
flattened_path = []
|
||||
|
||||
# iterate over pairs of 3-tuples
|
||||
for prev, current in zip(path[:-1], path[1:]):
|
||||
flattened_segment = self.flatten([[prev, current]])
|
||||
flattened_segment = [inkstitch.Point(x, y) for x, y in flattened_segment[0]]
|
||||
flattened_path.append(flattened_segment)
|
||||
|
||||
paths.append(flattened_path)
|
||||
|
||||
return zip(*paths)
|
||||
|
||||
def validate_satin_column(self):
|
||||
# The node should have exactly two paths with no fill. Each
|
||||
# path should have the same number of points, meaning that they
|
||||
# will both be made up of the same number of bezier curves.
|
||||
|
||||
node_id = self.node.get("id")
|
||||
|
||||
if self.get_style("fill") is not None:
|
||||
self.fatal(_("satin column: object %s has a fill (but should not)") % node_id)
|
||||
|
||||
if len(self.csp) == 2:
|
||||
if len(self.csp[0]) != len(self.csp[1]):
|
||||
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.csp[0]), length2=len(self.csp[1])))
|
||||
|
||||
def offset_points(self, pos1, pos2, offset_px):
|
||||
# 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
|
||||
|
||||
# don't contract beyond the midpoint, or we'll start expanding
|
||||
if offset_px < -distance / 2.0:
|
||||
offset_px = -distance / 2.0
|
||||
|
||||
pos1 = pos1 + (pos1 - pos2).unit() * offset_px
|
||||
pos2 = pos2 + (pos2 - pos1).unit() * offset_px
|
||||
|
||||
return pos1, pos2
|
||||
|
||||
def walk(self, path, start_pos, start_index, distance):
|
||||
# Move <distance> pixels along <path>, which is a sequence of line
|
||||
# segments defined by points.
|
||||
|
||||
# <start_index> is the index of the line segment in <path> that
|
||||
# we're currently on. <start_pos> is where along that line
|
||||
# segment we are. Return a new position and index.
|
||||
|
||||
# print >> dbg, "walk", start_pos, start_index, distance
|
||||
|
||||
pos = start_pos
|
||||
index = start_index
|
||||
last_index = len(path) - 1
|
||||
distance_remaining = distance
|
||||
|
||||
while True:
|
||||
if index >= last_index:
|
||||
return pos, index
|
||||
|
||||
segment_end = path[index + 1]
|
||||
segment = segment_end - pos
|
||||
segment_length = segment.length()
|
||||
|
||||
if segment_length > distance_remaining:
|
||||
# our walk ends partway along this segment
|
||||
return pos + segment.unit() * distance_remaining, index
|
||||
else:
|
||||
# our walk goes past the end of this segment, so advance
|
||||
# one point
|
||||
index += 1
|
||||
distance_remaining -= segment_length
|
||||
pos = segment_end
|
||||
|
||||
def walk_paths(self, spacing, offset):
|
||||
# Take a bezier segment from each path in turn, and plot out an
|
||||
# equal number of points on each bezier. Return the points plotted.
|
||||
# The points will be contracted or expanded by offset using
|
||||
# offset_points().
|
||||
|
||||
points = [[], []]
|
||||
|
||||
def add_pair(pos1, pos2):
|
||||
pos1, pos2 = self.offset_points(pos1, pos2, offset)
|
||||
points[0].append(pos1)
|
||||
points[1].append(pos2)
|
||||
|
||||
# We may not be able to fit an even number of zigzags in each pair of
|
||||
# beziers. We'll store the remaining bit of the beziers after handling
|
||||
# each section.
|
||||
remainder_path1 = []
|
||||
remainder_path2 = []
|
||||
|
||||
for segment1, segment2 in self.flattened_beziers:
|
||||
subpath1 = remainder_path1 + segment1
|
||||
subpath2 = remainder_path2 + segment2
|
||||
|
||||
len1 = shgeo.LineString(subpath1).length
|
||||
len2 = shgeo.LineString(subpath2).length
|
||||
|
||||
# Base the number of stitches in each section on the _longest_ of
|
||||
# the two beziers. Otherwise, things could get too sparse when one
|
||||
# side is significantly longer (e.g. when going around a corner).
|
||||
# The risk here is that we poke a hole in the fabric if we try to
|
||||
# cram too many stitches on the short bezier. The user will need
|
||||
# to avoid this through careful construction of paths.
|
||||
#
|
||||
# TODO: some commercial machine embroidery software compensates by
|
||||
# pulling in some of the "inner" stitches toward the center a bit.
|
||||
|
||||
# note, this rounds down using integer-division
|
||||
num_points = max(len1, len2) / spacing
|
||||
|
||||
spacing1 = len1 / num_points
|
||||
spacing2 = len2 / num_points
|
||||
|
||||
pos1 = subpath1[0]
|
||||
index1 = 0
|
||||
|
||||
pos2 = subpath2[0]
|
||||
index2 = 0
|
||||
|
||||
for i in xrange(int(num_points)):
|
||||
add_pair(pos1, pos2)
|
||||
|
||||
pos1, index1 = self.walk(subpath1, pos1, index1, spacing1)
|
||||
pos2, index2 = self.walk(subpath2, pos2, index2, spacing2)
|
||||
|
||||
if index1 < len(subpath1) - 1:
|
||||
remainder_path1 = [pos1] + subpath1[index1 + 1:]
|
||||
else:
|
||||
remainder_path1 = []
|
||||
|
||||
if index2 < len(subpath2) - 1:
|
||||
remainder_path2 = [pos2] + subpath2[index2 + 1:]
|
||||
else:
|
||||
remainder_path2 = []
|
||||
|
||||
# We're off by one in the algorithm above, so we need one more
|
||||
# pair of points. We also want to add points at the very end to
|
||||
# make sure we match the vectors on screen as best as possible.
|
||||
# Try to avoid doing both if they're going to stack up too
|
||||
# closely.
|
||||
|
||||
end1 = remainder_path1[-1]
|
||||
end2 = remainder_path2[-1]
|
||||
|
||||
if (end1 - pos1).length() > 0.3 * spacing:
|
||||
add_pair(pos1, pos2)
|
||||
|
||||
add_pair(end1, end2)
|
||||
|
||||
return points
|
||||
|
||||
def do_contour_underlay(self):
|
||||
# "contour walk" underlay: do stitches up one side and down the
|
||||
# other.
|
||||
forward, back = self.walk_paths(self.contour_underlay_stitch_length,
|
||||
-self.contour_underlay_inset)
|
||||
return Patch(color=self.color, stitches=(forward + list(reversed(back))))
|
||||
|
||||
def do_center_walk(self):
|
||||
# Center walk underlay is just a running stitch down and back on the
|
||||
# center line between the bezier curves.
|
||||
|
||||
# Do it like contour underlay, but inset all the way to the center.
|
||||
forward, back = self.walk_paths(self.center_walk_underlay_stitch_length,
|
||||
-100000)
|
||||
return Patch(color=self.color, stitches=(forward + list(reversed(back))))
|
||||
|
||||
def do_zigzag_underlay(self):
|
||||
# zigzag underlay, usually done at a much lower density than the
|
||||
# satin itself. It looks like this:
|
||||
#
|
||||
# \/\/\/\/\/\/\/\/\/\/|
|
||||
# /\/\/\/\/\/\/\/\/\/\|
|
||||
#
|
||||
# In combination with the "contour walk" underlay, this is the
|
||||
# "German underlay" described here:
|
||||
# http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/
|
||||
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0,
|
||||
-self.zigzag_underlay_inset)
|
||||
|
||||
# This organizes the points in each side in the order that they'll be
|
||||
# visited.
|
||||
sides = [sides[0][::2] + list(reversed(sides[0][1::2])),
|
||||
sides[1][1::2] + list(reversed(sides[1][::2]))]
|
||||
|
||||
# This fancy bit of iterable magic just repeatedly takes a point
|
||||
# from each side in turn.
|
||||
for point in chain.from_iterable(izip(*sides)):
|
||||
patch.add_stitch(point)
|
||||
|
||||
return patch
|
||||
|
||||
def do_satin(self):
|
||||
# satin: do a zigzag pattern, alternating between the paths. The
|
||||
# zigzag looks like this to make the satin stitches look perpendicular
|
||||
# to the column:
|
||||
#
|
||||
# /|/|/|/|/|/|/|/|
|
||||
|
||||
# print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation
|
||||
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation)
|
||||
|
||||
# Like in zigzag_underlay(): take a point from each side in turn.
|
||||
for point in chain.from_iterable(izip(*sides)):
|
||||
patch.add_stitch(point)
|
||||
|
||||
return patch
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
# Stitch a variable-width satin column, zig-zagging between two paths.
|
||||
|
||||
# The algorithm will draw zigzags between each consecutive pair of
|
||||
# beziers. The boundary points between beziers serve as "checkpoints",
|
||||
# allowing the user to control how the zigzags flow around corners.
|
||||
|
||||
# First, verify that we have valid paths.
|
||||
self.validate_satin_column()
|
||||
|
||||
patches = []
|
||||
|
||||
if self.center_walk_underlay:
|
||||
patches.append(self.do_center_walk())
|
||||
|
||||
if self.contour_underlay:
|
||||
patches.append(self.do_contour_underlay())
|
||||
|
||||
if self.zigzag_underlay:
|
||||
# zigzag underlay comes after contour walk underlay, so that the
|
||||
# zigzags sit on the contour walk underlay like rail ties on rails.
|
||||
patches.append(self.do_zigzag_underlay())
|
||||
|
||||
patches.append(self.do_satin())
|
||||
|
||||
return patches
|
||||
|
||||
|
||||
class Polyline(EmbroideryElement):
|
||||
# Handle a <polyline> element, which is treated as a set of points to
|
||||
# stitch exactly.
|
||||
#
|
||||
# <polyline> elements are pretty rare in SVG, from what I can tell.
|
||||
# Anything you can do with a <polyline> can also be done with a <p>, and
|
||||
# much more.
|
||||
#
|
||||
# Notably, EmbroiderModder2 uses <polyline> elements when converting from
|
||||
# common machine embroidery file formats to SVG. Handling those here lets
|
||||
# users use File -> Import to pull in existing designs they may have
|
||||
# obtained, for example purchased fonts.
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
# example: "1,2 0,0 1.5,3 4,2"
|
||||
|
||||
points = self.node.get('points')
|
||||
points = points.split(" ")
|
||||
points = [[float(coord) for coord in point.split(",")] for point in points]
|
||||
|
||||
return points
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
# A polyline is a series of connected line segments described by their
|
||||
# points. In order to make use of the existing logic for incorporating
|
||||
# svg transforms that is in our superclass, we'll convert the polyline
|
||||
# to a degenerate cubic superpath in which the bezier handles are on
|
||||
# the segment endpoints.
|
||||
|
||||
path = [[[point[:], point[:], point[:]] for point in self.points]]
|
||||
|
||||
return path
|
||||
|
||||
@property
|
||||
@cache
|
||||
def csp(self):
|
||||
csp = self.parse_path()
|
||||
|
||||
return csp
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
# EmbroiderModder2 likes to use the `stroke` property directly instead
|
||||
# of CSS.
|
||||
return self.get_style("stroke") or self.node.get("stroke")
|
||||
|
||||
@property
|
||||
def stitches(self):
|
||||
# For a <polyline>, we'll stitch the points exactly as they exist in
|
||||
# the SVG, with no stitch spacing interpolation, flattening, etc.
|
||||
|
||||
# See the comments in the parent class's parse_path method for a
|
||||
# description of the CSP data structure.
|
||||
|
||||
stitches = [point for handle_before, point, handle_after in self.csp[0]]
|
||||
|
||||
return stitches
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
for stitch in self.stitches:
|
||||
patch.add_stitch(inkstitch.Point(*stitch))
|
||||
|
||||
return [patch]
|
||||
|
||||
def detect_classes(node):
|
||||
if node.tag == SVG_POLYLINE_TAG:
|
||||
return [Polyline]
|
||||
else:
|
||||
element = EmbroideryElement(node)
|
||||
|
||||
if element.get_boolean_param("satin_column"):
|
||||
return [SatinColumn]
|
||||
else:
|
||||
classes = []
|
||||
|
||||
if element.get_style("fill"):
|
||||
if element.get_boolean_param("auto_fill", True):
|
||||
classes.append(AutoFill)
|
||||
else:
|
||||
classes.append(Fill)
|
||||
|
||||
if element.get_style("stroke"):
|
||||
classes.append(Stroke)
|
||||
|
||||
if element.get_boolean_param("stroke_first", False):
|
||||
classes.reverse()
|
||||
|
||||
return classes
|
||||
|
||||
|
||||
class Patch:
|
||||
# TODO: merge this with inkstitch.stitch_plan.ColorBlock
|
||||
def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False):
|
||||
self.color = color
|
||||
self.stitches = stitches or []
|
||||
self.trim_after = trim_after
|
||||
self.stop_after = stop_after
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Patch):
|
||||
return Patch(self.color, self.stitches + other.stitches)
|
||||
else:
|
||||
raise TypeError("Patch can only be added to another Patch")
|
||||
|
||||
def add_stitch(self, stitch):
|
||||
self.stitches.append(stitch)
|
||||
|
||||
def reverse(self):
|
||||
return Patch(self.color, self.stitches[::-1])
|
||||
|
||||
def get_elements(effect):
|
||||
elements = []
|
||||
nodes = get_nodes(effect)
|
||||
|
||||
for node in nodes:
|
||||
classes = detect_classes(node)
|
||||
elements.extend(cls(node) for cls in classes)
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
def elements_to_patches(elements):
|
||||
patches = []
|
||||
for element in elements:
|
||||
if patches:
|
||||
last_patch = patches[-1]
|
||||
else:
|
||||
last_patch = None
|
||||
|
||||
patches.extend(element.embroider(last_patch))
|
||||
|
||||
return patches
|
||||
|
||||
class Embroider(inkex.Effect):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
inkex.Effect.__init__(self)
|
||||
InkstitchExtension.__init__(self)
|
||||
self.OptionParser.add_option("-c", "--collapse_len_mm",
|
||||
action="store", type="float",
|
||||
dest="collapse_length_mm", default=3.0,
|
||||
|
@ -951,37 +87,18 @@ class Embroider(inkex.Effect):
|
|||
|
||||
return output_path
|
||||
|
||||
def hide_layers(self):
|
||||
for g in self.document.getroot().findall(SVG_GROUP_TAG):
|
||||
if g.get(inkex.addNS("groupmode", "inkscape")) == "layer":
|
||||
g.set("style", "display:none")
|
||||
|
||||
def effect(self):
|
||||
# Printing anything other than a valid SVG on stdout blows inkscape up.
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = sys.stderr
|
||||
|
||||
self.patch_list = []
|
||||
|
||||
self.elements = get_elements(self)
|
||||
|
||||
if not self.elements:
|
||||
if self.selected:
|
||||
inkex.errormsg(_("No embroiderable paths selected."))
|
||||
else:
|
||||
inkex.errormsg(_("No embroiderable paths found in document."))
|
||||
inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering."))
|
||||
if not self.get_elements():
|
||||
return
|
||||
|
||||
if self.options.hide_layers:
|
||||
self.hide_layers()
|
||||
self.hide_all_layers()
|
||||
|
||||
patches = elements_to_patches(self.elements)
|
||||
patches = self.elements_to_patches(self.elements)
|
||||
stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
|
||||
inkstitch.write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot())
|
||||
render_stitch_plan(self.document.getroot(), stitch_plan)
|
||||
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.setrecursionlimit(100000)
|
||||
|
@ -990,8 +107,6 @@ if __name__ == '__main__':
|
|||
try:
|
||||
e.affect()
|
||||
except KeyboardInterrupt:
|
||||
print >> dbg, "interrupted!"
|
||||
|
||||
print >> dbg, traceback.format_exc()
|
||||
|
||||
dbg.flush()
|
||||
# for use at the command prompt for debugging
|
||||
print >> sys.stderr, "interrupted!"
|
||||
print >> sys.stderr, traceback.format_exc()
|
||||
|
|
|
@ -12,12 +12,13 @@ from cStringIO import StringIO
|
|||
import wx
|
||||
from wx.lib.scrolledpanel import ScrolledPanel
|
||||
from collections import defaultdict
|
||||
import inkex
|
||||
import inkstitch
|
||||
from inkstitch import _, Param, EmbroideryElement, get_nodes
|
||||
from embroider import Fill, AutoFill, Stroke, SatinColumn
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from inkstitch import _
|
||||
from inkstitch.extensions import InkstitchExtension
|
||||
from inkstitch.stitch_plan import patches_to_stitch_plan
|
||||
from inkstitch.elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
|
||||
from embroider_simulate import EmbroiderySimulator
|
||||
|
||||
|
||||
|
@ -412,9 +413,10 @@ class SettingsFrame(wx.Frame):
|
|||
wx.CallAfter(self.refresh_simulator, patches)
|
||||
|
||||
def refresh_simulator(self, patches):
|
||||
stitch_plan = patches_to_stitch_plan(patches)
|
||||
if self.simulate_window:
|
||||
self.simulate_window.stop()
|
||||
self.simulate_window.load(patches=patches)
|
||||
self.simulate_window.load(stitch_plan=stitch_plan)
|
||||
else:
|
||||
my_rect = self.GetRect()
|
||||
simulator_pos = my_rect.GetTopRight()
|
||||
|
@ -428,7 +430,7 @@ class SettingsFrame(wx.Frame):
|
|||
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
|
||||
simulator_pos,
|
||||
size=(300, 300),
|
||||
patches=patches,
|
||||
stitch_plan=stitch_plan,
|
||||
on_close=self.simulate_window_closed,
|
||||
target_duration=5,
|
||||
max_width=max_width,
|
||||
|
@ -631,10 +633,10 @@ class SettingsFrame(wx.Frame):
|
|||
self.Layout()
|
||||
# end wxGlade
|
||||
|
||||
class EmbroiderParams(inkex.Effect):
|
||||
class EmbroiderParams(InkstitchExtension):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cancelled = False
|
||||
inkex.Effect.__init__(self, *args, **kwargs)
|
||||
InkstitchExtension.__init__(self, *args, **kwargs)
|
||||
|
||||
def embroidery_classes(self, node):
|
||||
element = EmbroideryElement(node)
|
||||
|
@ -653,7 +655,7 @@ class EmbroiderParams(inkex.Effect):
|
|||
return classes
|
||||
|
||||
def get_nodes_by_class(self):
|
||||
nodes = get_nodes(self)
|
||||
nodes = self.get_nodes()
|
||||
nodes_by_class = defaultdict(list)
|
||||
|
||||
for z, node in enumerate(nodes):
|
||||
|
|
|
@ -8,15 +8,15 @@ import colorsys
|
|||
from itertools import izip
|
||||
|
||||
import inkstitch
|
||||
from inkstitch.extensions import InkstitchExtension
|
||||
from inkstitch import PIXELS_PER_MM
|
||||
from embroider import _, patches_to_stitch_plan, get_elements, elements_to_patches
|
||||
from inkstitch.stitch_plan import patches_to_stitch_plan
|
||||
from inkstitch.svg import color_block_to_point_lists
|
||||
|
||||
|
||||
class EmbroiderySimulator(wx.Frame):
|
||||
def __init__(self, *args, **kwargs):
|
||||
stitch_file = kwargs.pop('stitch_file', None)
|
||||
patches = kwargs.pop('patches', None)
|
||||
stitch_plan = kwargs.pop('stitch_plan', None)
|
||||
self.on_close_hook = kwargs.pop('on_close', None)
|
||||
self.frame_period = kwargs.pop('frame_period', 80)
|
||||
self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1)
|
||||
|
@ -34,7 +34,7 @@ class EmbroiderySimulator(wx.Frame):
|
|||
self.panel = wx.Panel(self, wx.ID_ANY)
|
||||
self.panel.SetFocus()
|
||||
|
||||
self.load(stitch_file, patches)
|
||||
self.load(stitch_plan)
|
||||
|
||||
if self.target_duration:
|
||||
self.adjust_speed(self.target_duration)
|
||||
|
@ -56,13 +56,10 @@ class EmbroiderySimulator(wx.Frame):
|
|||
|
||||
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||||
|
||||
def load(self, stitch_file=None, patches=None):
|
||||
if stitch_file:
|
||||
self.mirror = True
|
||||
self.segments = self._parse_stitch_file(stitch_file)
|
||||
elif patches:
|
||||
def load(self, stitch_plan=None):
|
||||
if stitch_plan:
|
||||
self.mirror = False
|
||||
self.segments = self._patches_to_segments(patches)
|
||||
self.segments = self._stitch_plan_to_segments(stitch_plan)
|
||||
else:
|
||||
return
|
||||
|
||||
|
@ -135,19 +132,13 @@ class EmbroiderySimulator(wx.Frame):
|
|||
|
||||
return wx.Pen(color)
|
||||
|
||||
def _patches_to_segments(self, patches):
|
||||
stitch_plan = patches_to_stitch_plan(patches)
|
||||
|
||||
def _stitch_plan_to_segments(self, stitch_plan):
|
||||
segments = []
|
||||
|
||||
for color_block in stitch_plan:
|
||||
pen = self.color_to_pen(color_block.color)
|
||||
|
||||
for point_list in color_block_to_point_lists(color_block):
|
||||
# The polyline is made up of a set of points denoting the
|
||||
# vertices along the path. We need to break this up into
|
||||
# individual line segments to be drawn on the screen.
|
||||
|
||||
# if there's only one point, there's nothing to do, so skip
|
||||
if len(point_list) < 2:
|
||||
continue
|
||||
|
@ -283,18 +274,22 @@ class EmbroiderySimulator(wx.Frame):
|
|||
except IndexError:
|
||||
self.timer.Stop()
|
||||
|
||||
class SimulateEffect(inkex.Effect):
|
||||
class SimulateEffect(InkstitchExtension):
|
||||
def __init__(self):
|
||||
inkex.Effect.__init__(self)
|
||||
InkstitchExtension.__init__(self)
|
||||
self.OptionParser.add_option("-P", "--path",
|
||||
action="store", type="string",
|
||||
dest="path", default=".",
|
||||
help="Directory in which to store output file")
|
||||
|
||||
def effect(self):
|
||||
patches = elements_to_patches(get_elements(self))
|
||||
if not self.get_elements():
|
||||
return
|
||||
|
||||
patches = self.elements_to_patches(self.elements)
|
||||
stitch_plan = patches_to_stitch_plan(patches)
|
||||
app = wx.App()
|
||||
frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), patches=patches)
|
||||
frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), stitch_plan=stitch_plan)
|
||||
app.SetTopWindow(frame)
|
||||
frame.Show()
|
||||
wx.CallAfter(frame.go)
|
||||
|
|
|
@ -7,6 +7,9 @@ import gettext
|
|||
from copy import deepcopy
|
||||
import math
|
||||
import libembroidery
|
||||
from inkstitch.utils import cache
|
||||
from inkstitch.utils.geometry import Point
|
||||
|
||||
import inkex
|
||||
import simplepath
|
||||
import simplestyle
|
||||
|
@ -17,11 +20,6 @@ import cubicsuperpath
|
|||
from shapely import geometry as shgeo
|
||||
|
||||
|
||||
try:
|
||||
from functools import lru_cache
|
||||
except ImportError:
|
||||
from backports.functools_lru_cache import lru_cache
|
||||
|
||||
# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
|
||||
PIXELS_PER_MM = 96 / 25.4
|
||||
|
||||
|
@ -38,9 +36,6 @@ dbg = open(os.devnull, "w")
|
|||
|
||||
_ = lambda message: message
|
||||
|
||||
# simplify use of lru_cache decorator
|
||||
def cache(*args, **kwargs):
|
||||
return lru_cache(maxsize=None)(*args, **kwargs)
|
||||
|
||||
def localize():
|
||||
if getattr(sys, 'frozen', False):
|
||||
|
@ -157,277 +152,6 @@ def get_viewbox_transform(node):
|
|||
|
||||
return transform
|
||||
|
||||
class Param(object):
|
||||
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.values = values or [""]
|
||||
self.type = type
|
||||
self.group = group
|
||||
self.inverse = inverse
|
||||
self.default = default
|
||||
self.tooltip = tooltip
|
||||
self.sort_index = sort_index
|
||||
|
||||
def __repr__(self):
|
||||
return "Param(%s)" % vars(self)
|
||||
|
||||
|
||||
# Decorate a member function or property with information about
|
||||
# the embroidery parameter it corresponds to
|
||||
def param(*args, **kwargs):
|
||||
p = Param(*args, **kwargs)
|
||||
|
||||
def decorator(func):
|
||||
func.param = p
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class EmbroideryElement(object):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.node.get('id')
|
||||
|
||||
@classmethod
|
||||
def get_params(cls):
|
||||
params = []
|
||||
for attr in dir(cls):
|
||||
prop = getattr(cls, attr)
|
||||
if isinstance(prop, property):
|
||||
# The 'param' attribute is set by the 'param' decorator defined above.
|
||||
if hasattr(prop.fget, 'param'):
|
||||
params.append(prop.fget.param)
|
||||
|
||||
return params
|
||||
|
||||
@cache
|
||||
def get_param(self, param, default):
|
||||
value = self.node.get("embroider_" + param, "").strip()
|
||||
|
||||
return value or default
|
||||
|
||||
@cache
|
||||
def get_boolean_param(self, param, default=None):
|
||||
value = self.get_param(param, default)
|
||||
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
else:
|
||||
return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
|
||||
|
||||
@cache
|
||||
def get_float_param(self, param, default=None):
|
||||
try:
|
||||
value = float(self.get_param(param, default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if param.endswith('_mm'):
|
||||
value = value * PIXELS_PER_MM
|
||||
|
||||
return value
|
||||
|
||||
@cache
|
||||
def get_int_param(self, param, default=None):
|
||||
try:
|
||||
value = int(self.get_param(param, default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if param.endswith('_mm'):
|
||||
value = int(value * PIXELS_PER_MM)
|
||||
|
||||
return value
|
||||
|
||||
def set_param(self, name, value):
|
||||
self.node.set("embroider_%s" % name, str(value))
|
||||
|
||||
@cache
|
||||
def get_style(self, style_name):
|
||||
style = simplestyle.parseStyle(self.node.get("style"))
|
||||
if (style_name not in style):
|
||||
return None
|
||||
value = style[style_name]
|
||||
if value == 'none':
|
||||
return None
|
||||
return value
|
||||
|
||||
@cache
|
||||
def has_style(self, style_name):
|
||||
style = simplestyle.parseStyle(self.node.get("style"))
|
||||
return style_name in style
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return cubicsuperpath.parsePath(self.node.get("d"))
|
||||
|
||||
|
||||
@cache
|
||||
def parse_path(self):
|
||||
# A CSP is a "cubic superpath".
|
||||
#
|
||||
# A "path" is a sequence of strung-together bezier curves.
|
||||
#
|
||||
# A "superpath" is a collection of paths that are all in one object.
|
||||
#
|
||||
# The "cubic" bit in "cubic superpath" is because the bezier curves
|
||||
# inkscape uses involve cubic polynomials.
|
||||
#
|
||||
# Each path is a collection of tuples, each of the form:
|
||||
#
|
||||
# (control_before, point, control_after)
|
||||
#
|
||||
# A bezier curve segment is defined by an endpoint, a control point,
|
||||
# a second control point, and a final endpoint. A path is a bunch of
|
||||
# bezier curves strung together. One could represent a path as a set
|
||||
# of four-tuples, but there would be redundancy because the ending
|
||||
# point of one bezier is the starting point of the next. Instead, a
|
||||
# path is a set of 3-tuples as shown above, and one must construct
|
||||
# each bezier curve by taking the appropriate endpoints and control
|
||||
# points. Bleh. It should be noted that a straight segment is
|
||||
# represented by having the control point on each end equal to that
|
||||
# end's point.
|
||||
#
|
||||
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
|
||||
# Tuples all the way down. Hasn't anyone heard of using classes?
|
||||
|
||||
path = self.path
|
||||
|
||||
# start with the identity transform
|
||||
transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||
|
||||
# combine this node's transform with all parent groups' transforms
|
||||
transform = simpletransform.composeParents(self.node, transform)
|
||||
|
||||
# add in the transform implied by the viewBox
|
||||
viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
|
||||
transform = simpletransform.composeTransform(viewbox_transform, transform)
|
||||
|
||||
# apply the combined transform to this node's path
|
||||
simpletransform.applyTransformToPath(transform, path)
|
||||
|
||||
|
||||
return path
|
||||
|
||||
def flatten(self, path):
|
||||
"""approximate a path containing beziers with a series of points"""
|
||||
|
||||
path = deepcopy(path)
|
||||
|
||||
cspsubdiv(path, 0.1)
|
||||
|
||||
flattened = []
|
||||
|
||||
for comp in path:
|
||||
vertices = []
|
||||
for ctl in comp:
|
||||
vertices.append((ctl[1][0], ctl[1][1]))
|
||||
flattened.append(vertices)
|
||||
|
||||
return flattened
|
||||
|
||||
@property
|
||||
@param('trim_after',
|
||||
_('TRIM after'),
|
||||
tooltip=_('Trim thread after this object (for supported machines and file formats)'),
|
||||
type='boolean',
|
||||
default=False,
|
||||
sort_index=1000)
|
||||
def trim_after(self):
|
||||
return self.get_boolean_param('trim_after', False)
|
||||
|
||||
@property
|
||||
@param('stop_after',
|
||||
_('STOP after'),
|
||||
tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'),
|
||||
type='boolean',
|
||||
default=False,
|
||||
sort_index=1000)
|
||||
def stop_after(self):
|
||||
return self.get_boolean_param('stop_after', False)
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
|
||||
|
||||
def embroider(self, last_patch):
|
||||
patches = self.to_patches(last_patch)
|
||||
|
||||
if patches:
|
||||
patches[-1].trim_after = self.trim_after
|
||||
patches[-1].stop_after = self.stop_after
|
||||
|
||||
return patches
|
||||
|
||||
def fatal(self, message):
|
||||
print >> sys.stderr, "error:", message
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __add__(self, other):
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other):
|
||||
return Point(self.x - other.x, self.y - other.y)
|
||||
|
||||
def mul(self, scalar):
|
||||
return Point(self.x * scalar, self.y * scalar)
|
||||
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Point):
|
||||
# dot product
|
||||
return self.x * other.x + self.y * other.y
|
||||
elif isinstance(other, (int, float)):
|
||||
return Point(self.x * other, self.y * other)
|
||||
else:
|
||||
raise ValueError("cannot multiply Point by %s" % type(other))
|
||||
|
||||
def __rmul__(self, other):
|
||||
if isinstance(other, (int, float)):
|
||||
return self.__mul__(other)
|
||||
else:
|
||||
raise ValueError("cannot multiply Point by %s" % type(other))
|
||||
|
||||
def __repr__(self):
|
||||
return "Point(%s,%s)" % (self.x, self.y)
|
||||
|
||||
def length(self):
|
||||
return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
|
||||
|
||||
def unit(self):
|
||||
return self.mul(1.0 / self.length())
|
||||
|
||||
def rotate_left(self):
|
||||
return Point(-self.y, self.x)
|
||||
|
||||
def rotate(self, angle):
|
||||
return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
|
||||
|
||||
def as_int(self):
|
||||
return Point(int(round(self.x)), int(round(self.y)))
|
||||
|
||||
def as_tuple(self):
|
||||
return (self.x, self.y)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.as_tuple(), other.as_tuple())
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.as_tuple()[item]
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
|
||||
class Stitch(Point):
|
||||
def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
|
||||
|
@ -442,42 +166,6 @@ class Stitch(Point):
|
|||
return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
|
||||
|
||||
|
||||
def descendants(node):
|
||||
nodes = []
|
||||
element = EmbroideryElement(node)
|
||||
|
||||
if element.has_style('display') and element.get_style('display') is None:
|
||||
return []
|
||||
|
||||
if node.tag == SVG_DEFS_TAG:
|
||||
return []
|
||||
|
||||
for child in node:
|
||||
nodes.extend(descendants(child))
|
||||
|
||||
if node.tag in EMBROIDERABLE_TAGS:
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def get_nodes(effect):
|
||||
"""Get all XML nodes, or just those selected
|
||||
|
||||
effect is an instance of a subclass of inkex.Effect.
|
||||
"""
|
||||
|
||||
if effect.selected:
|
||||
nodes = []
|
||||
for node in effect.document.getroot().iter():
|
||||
if node.get("id") in effect.selected:
|
||||
nodes.extend(descendants(node))
|
||||
else:
|
||||
nodes = descendants(effect.document.getroot())
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def make_thread(color):
|
||||
# strip off the leading "#"
|
||||
if color.startswith("#"):
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
from auto_fill import AutoFill
|
||||
from fill import Fill
|
||||
from stroke import Stroke
|
||||
from satin_column import SatinColumn
|
||||
from element import EmbroideryElement
|
|
@ -0,0 +1,107 @@
|
|||
from .. import _
|
||||
from .element import param, Patch
|
||||
from ..utils import cache
|
||||
from .fill import Fill
|
||||
from shapely import geometry as shgeo
|
||||
from ..stitches import auto_fill
|
||||
|
||||
|
||||
class AutoFill(Fill):
|
||||
element_name = _("Auto-Fill")
|
||||
|
||||
@property
|
||||
@param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True)
|
||||
def auto_fill(self):
|
||||
return self.get_boolean_param('auto_fill', True)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def outline(self):
|
||||
return self.shape.boundary[0]
|
||||
|
||||
@property
|
||||
@cache
|
||||
def outline_length(self):
|
||||
return self.outline.length
|
||||
|
||||
@property
|
||||
def flip(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False)
|
||||
def fill_underlay(self):
|
||||
return self.get_boolean_param("fill_underlay", default=False)
|
||||
|
||||
@property
|
||||
@param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float')
|
||||
@cache
|
||||
def fill_underlay_angle(self):
|
||||
underlay_angle = self.get_float_param("fill_underlay_angle")
|
||||
|
||||
if underlay_angle:
|
||||
return math.radians(underlay_angle)
|
||||
else:
|
||||
return self.angle + math.pi / 2.0
|
||||
|
||||
@property
|
||||
@param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float')
|
||||
@cache
|
||||
def fill_underlay_row_spacing(self):
|
||||
return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3
|
||||
|
||||
@property
|
||||
@param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float')
|
||||
@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
|
||||
@param('fill_underlay_inset_mm', _('Inset'), unit='mm', group=_('AutoFill Underlay'), type='float', default=0)
|
||||
def fill_underlay_inset(self):
|
||||
return self.get_float_param('fill_underlay_inset_mm', 0)
|
||||
|
||||
@property
|
||||
def underlay_shape(self):
|
||||
if self.fill_underlay_inset:
|
||||
shape = self.shape.buffer(-self.fill_underlay_inset)
|
||||
if not isinstance(shape, shgeo.MultiPolygon):
|
||||
shape = shgeo.MultiPolygon([shape])
|
||||
return shape
|
||||
else:
|
||||
return self.shape
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
stitches = []
|
||||
|
||||
if last_patch is None:
|
||||
starting_point = None
|
||||
else:
|
||||
starting_point = last_patch.stitches[-1]
|
||||
|
||||
if self.fill_underlay:
|
||||
stitches.extend(auto_fill(self.underlay_shape,
|
||||
self.fill_underlay_angle,
|
||||
self.fill_underlay_row_spacing,
|
||||
self.fill_underlay_row_spacing,
|
||||
self.fill_underlay_max_stitch_length,
|
||||
self.running_stitch_length,
|
||||
self.staggers,
|
||||
starting_point))
|
||||
starting_point = stitches[-1]
|
||||
|
||||
stitches.extend(auto_fill(self.shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.max_stitch_length,
|
||||
self.running_stitch_length,
|
||||
self.staggers,
|
||||
starting_point))
|
||||
|
||||
return [Patch(stitches=stitches, color=self.color)]
|
|
@ -0,0 +1,245 @@
|
|||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
from ..utils import cache
|
||||
from shapely import geometry as shgeo
|
||||
from .. import _, PIXELS_PER_MM, get_viewbox_transform
|
||||
|
||||
# inkscape-provided utilities
|
||||
import simpletransform
|
||||
import simplestyle
|
||||
import cubicsuperpath
|
||||
from cspsubdiv import cspsubdiv
|
||||
|
||||
class Patch:
|
||||
# TODO: merge this with inkstitch.stitch_plan.ColorBlock
|
||||
def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False):
|
||||
self.color = color
|
||||
self.stitches = stitches or []
|
||||
self.trim_after = trim_after
|
||||
self.stop_after = stop_after
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Patch):
|
||||
return Patch(self.color, self.stitches + other.stitches)
|
||||
else:
|
||||
raise TypeError("Patch can only be added to another Patch")
|
||||
|
||||
def add_stitch(self, stitch):
|
||||
self.stitches.append(stitch)
|
||||
|
||||
def reverse(self):
|
||||
return Patch(self.color, self.stitches[::-1])
|
||||
|
||||
|
||||
|
||||
class Param(object):
|
||||
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.values = values or [""]
|
||||
self.type = type
|
||||
self.group = group
|
||||
self.inverse = inverse
|
||||
self.default = default
|
||||
self.tooltip = tooltip
|
||||
self.sort_index = sort_index
|
||||
|
||||
def __repr__(self):
|
||||
return "Param(%s)" % vars(self)
|
||||
|
||||
|
||||
# Decorate a member function or property with information about
|
||||
# the embroidery parameter it corresponds to
|
||||
def param(*args, **kwargs):
|
||||
p = Param(*args, **kwargs)
|
||||
|
||||
def decorator(func):
|
||||
func.param = p
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class EmbroideryElement(object):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.node.get('id')
|
||||
|
||||
@classmethod
|
||||
def get_params(cls):
|
||||
params = []
|
||||
for attr in dir(cls):
|
||||
prop = getattr(cls, attr)
|
||||
if isinstance(prop, property):
|
||||
# The 'param' attribute is set by the 'param' decorator defined above.
|
||||
if hasattr(prop.fget, 'param'):
|
||||
params.append(prop.fget.param)
|
||||
|
||||
return params
|
||||
|
||||
@cache
|
||||
def get_param(self, param, default):
|
||||
value = self.node.get("embroider_" + param, "").strip()
|
||||
|
||||
return value or default
|
||||
|
||||
@cache
|
||||
def get_boolean_param(self, param, default=None):
|
||||
value = self.get_param(param, default)
|
||||
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
else:
|
||||
return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
|
||||
|
||||
@cache
|
||||
def get_float_param(self, param, default=None):
|
||||
try:
|
||||
value = float(self.get_param(param, default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if param.endswith('_mm'):
|
||||
value = value * PIXELS_PER_MM
|
||||
|
||||
return value
|
||||
|
||||
@cache
|
||||
def get_int_param(self, param, default=None):
|
||||
try:
|
||||
value = int(self.get_param(param, default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if param.endswith('_mm'):
|
||||
value = int(value * PIXELS_PER_MM)
|
||||
|
||||
return value
|
||||
|
||||
def set_param(self, name, value):
|
||||
self.node.set("embroider_%s" % name, str(value))
|
||||
|
||||
@cache
|
||||
def get_style(self, style_name):
|
||||
style = simplestyle.parseStyle(self.node.get("style"))
|
||||
if (style_name not in style):
|
||||
return None
|
||||
value = style[style_name]
|
||||
if value == 'none':
|
||||
return None
|
||||
return value
|
||||
|
||||
@cache
|
||||
def has_style(self, style_name):
|
||||
style = simplestyle.parseStyle(self.node.get("style"))
|
||||
return style_name in style
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return cubicsuperpath.parsePath(self.node.get("d"))
|
||||
|
||||
|
||||
@cache
|
||||
def parse_path(self):
|
||||
# A CSP is a "cubic superpath".
|
||||
#
|
||||
# A "path" is a sequence of strung-together bezier curves.
|
||||
#
|
||||
# A "superpath" is a collection of paths that are all in one object.
|
||||
#
|
||||
# The "cubic" bit in "cubic superpath" is because the bezier curves
|
||||
# inkscape uses involve cubic polynomials.
|
||||
#
|
||||
# Each path is a collection of tuples, each of the form:
|
||||
#
|
||||
# (control_before, point, control_after)
|
||||
#
|
||||
# A bezier curve segment is defined by an endpoint, a control point,
|
||||
# a second control point, and a final endpoint. A path is a bunch of
|
||||
# bezier curves strung together. One could represent a path as a set
|
||||
# of four-tuples, but there would be redundancy because the ending
|
||||
# point of one bezier is the starting point of the next. Instead, a
|
||||
# path is a set of 3-tuples as shown above, and one must construct
|
||||
# each bezier curve by taking the appropriate endpoints and control
|
||||
# points. Bleh. It should be noted that a straight segment is
|
||||
# represented by having the control point on each end equal to that
|
||||
# end's point.
|
||||
#
|
||||
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
|
||||
# Tuples all the way down. Hasn't anyone heard of using classes?
|
||||
|
||||
path = self.path
|
||||
|
||||
# start with the identity transform
|
||||
transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||
|
||||
# combine this node's transform with all parent groups' transforms
|
||||
transform = simpletransform.composeParents(self.node, transform)
|
||||
|
||||
# add in the transform implied by the viewBox
|
||||
viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
|
||||
transform = simpletransform.composeTransform(viewbox_transform, transform)
|
||||
|
||||
# apply the combined transform to this node's path
|
||||
simpletransform.applyTransformToPath(transform, path)
|
||||
|
||||
|
||||
return path
|
||||
|
||||
def flatten(self, path):
|
||||
"""approximate a path containing beziers with a series of points"""
|
||||
|
||||
path = deepcopy(path)
|
||||
|
||||
cspsubdiv(path, 0.1)
|
||||
|
||||
flattened = []
|
||||
|
||||
for comp in path:
|
||||
vertices = []
|
||||
for ctl in comp:
|
||||
vertices.append((ctl[1][0], ctl[1][1]))
|
||||
flattened.append(vertices)
|
||||
|
||||
return flattened
|
||||
|
||||
@property
|
||||
@param('trim_after',
|
||||
_('TRIM after'),
|
||||
tooltip=_('Trim thread after this object (for supported machines and file formats)'),
|
||||
type='boolean',
|
||||
default=False,
|
||||
sort_index=1000)
|
||||
def trim_after(self):
|
||||
return self.get_boolean_param('trim_after', False)
|
||||
|
||||
@property
|
||||
@param('stop_after',
|
||||
_('STOP after'),
|
||||
tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'),
|
||||
type='boolean',
|
||||
default=False,
|
||||
sort_index=1000)
|
||||
def stop_after(self):
|
||||
return self.get_boolean_param('stop_after', False)
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
|
||||
|
||||
def embroider(self, last_patch):
|
||||
patches = self.to_patches(last_patch)
|
||||
|
||||
if patches:
|
||||
patches[-1].trim_after = self.trim_after
|
||||
patches[-1].stop_after = self.stop_after
|
||||
|
||||
return patches
|
||||
|
||||
def fatal(self, message):
|
||||
print >> sys.stderr, "error:", message
|
||||
sys.exit(1)
|
|
@ -0,0 +1,97 @@
|
|||
from .. import _
|
||||
from .element import param, EmbroideryElement, Patch
|
||||
from ..utils import cache
|
||||
from shapely import geometry as shgeo
|
||||
import math
|
||||
from ..stitches import running_stitch, auto_fill, legacy_fill
|
||||
|
||||
class Fill(EmbroideryElement):
|
||||
element_name = _("Fill")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Fill, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True)
|
||||
def auto_fill(self):
|
||||
return self.get_boolean_param('auto_fill', True)
|
||||
|
||||
@property
|
||||
@param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0)
|
||||
@cache
|
||||
def angle(self):
|
||||
return math.radians(self.get_float_param('angle', 0))
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("fill")
|
||||
|
||||
@property
|
||||
@param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False)
|
||||
def flip(self):
|
||||
return self.get_boolean_param("flip", False)
|
||||
|
||||
@property
|
||||
@param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25)
|
||||
def row_spacing(self):
|
||||
return max(self.get_float_param("row_spacing_mm", 0.25), 0.01)
|
||||
|
||||
@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'), unit='mm', type='float', default=3.0)
|
||||
def max_stitch_length(self):
|
||||
return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01)
|
||||
|
||||
@property
|
||||
@param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4)
|
||||
def staggers(self):
|
||||
return self.get_int_param("staggers", 4)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def paths(self):
|
||||
return self.flatten(self.parse_path())
|
||||
|
||||
@property
|
||||
@cache
|
||||
def shape(self):
|
||||
poly_ary = []
|
||||
for sub_path in self.paths:
|
||||
point_ary = []
|
||||
last_pt = None
|
||||
for pt in sub_path:
|
||||
if (last_pt is not None):
|
||||
vp = (pt[0] - last_pt[0], pt[1] - last_pt[1])
|
||||
dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0))
|
||||
# dbg.write("dp %s\n" % dp)
|
||||
if (dp > 0.01):
|
||||
# I think too-close points confuse shapely.
|
||||
point_ary.append(pt)
|
||||
last_pt = pt
|
||||
else:
|
||||
last_pt = pt
|
||||
if point_ary:
|
||||
poly_ary.append(point_ary)
|
||||
|
||||
# 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.
|
||||
# TODO: actually figure out which things are holes and which are shells
|
||||
poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True)
|
||||
|
||||
polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])])
|
||||
# print >> sys.stderr, "polygon valid:", polygon.is_valid
|
||||
return polygon
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
stitch_lists = legacy_fill(self.shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.max_stitch_length,
|
||||
self.flip,
|
||||
self.staggers)
|
||||
return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]
|
|
@ -0,0 +1,72 @@
|
|||
from .. import _, Point
|
||||
from .element import param, EmbroideryElement, Patch
|
||||
from ..utils import cache
|
||||
|
||||
|
||||
class Polyline(EmbroideryElement):
|
||||
# Handle a <polyline> element, which is treated as a set of points to
|
||||
# stitch exactly.
|
||||
#
|
||||
# <polyline> elements are pretty rare in SVG, from what I can tell.
|
||||
# Anything you can do with a <polyline> can also be done with a <p>, and
|
||||
# much more.
|
||||
#
|
||||
# Notably, EmbroiderModder2 uses <polyline> elements when converting from
|
||||
# common machine embroidery file formats to SVG. Handling those here lets
|
||||
# users use File -> Import to pull in existing designs they may have
|
||||
# obtained, for example purchased fonts.
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
# example: "1,2 0,0 1.5,3 4,2"
|
||||
|
||||
points = self.node.get('points')
|
||||
points = points.split(" ")
|
||||
points = [[float(coord) for coord in point.split(",")] for point in points]
|
||||
|
||||
return points
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
# A polyline is a series of connected line segments described by their
|
||||
# points. In order to make use of the existing logic for incorporating
|
||||
# svg transforms that is in our superclass, we'll convert the polyline
|
||||
# to a degenerate cubic superpath in which the bezier handles are on
|
||||
# the segment endpoints.
|
||||
|
||||
path = [[[point[:], point[:], point[:]] for point in self.points]]
|
||||
|
||||
return path
|
||||
|
||||
@property
|
||||
@cache
|
||||
def csp(self):
|
||||
csp = self.parse_path()
|
||||
|
||||
return csp
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
# EmbroiderModder2 likes to use the `stroke` property directly instead
|
||||
# of CSS.
|
||||
return self.get_style("stroke") or self.node.get("stroke")
|
||||
|
||||
@property
|
||||
def stitches(self):
|
||||
# For a <polyline>, we'll stitch the points exactly as they exist in
|
||||
# the SVG, with no stitch spacing interpolation, flattening, etc.
|
||||
|
||||
# See the comments in the parent class's parse_path method for a
|
||||
# description of the CSP data structure.
|
||||
|
||||
stitches = [point for handle_before, point, handle_after in self.csp[0]]
|
||||
|
||||
return stitches
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
for stitch in self.stitches:
|
||||
patch.add_stitch(Point(*stitch))
|
||||
|
||||
return [patch]
|
|
@ -0,0 +1,403 @@
|
|||
from itertools import chain, izip
|
||||
|
||||
from .. import _, Point
|
||||
from .element import param, EmbroideryElement, Patch
|
||||
from ..utils import cache
|
||||
from shapely import geometry as shgeo, ops as shops
|
||||
|
||||
|
||||
class SatinColumn(EmbroideryElement):
|
||||
element_name = _("Satin Column")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SatinColumn, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@param('satin_column', _('Custom satin column'), type='toggle')
|
||||
def satin_column(self):
|
||||
return self.get_boolean_param("satin_column")
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("stroke")
|
||||
|
||||
@property
|
||||
@param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4)
|
||||
def zigzag_spacing(self):
|
||||
# peak-to-peak distance between zigzags
|
||||
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
|
||||
|
||||
@property
|
||||
@param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float')
|
||||
def pull_compensation(self):
|
||||
# In satin stitch, the stitches have a tendency to pull together and
|
||||
# narrow the entire column. We can compensate for this by stitching
|
||||
# wider than we desire the column to end up.
|
||||
return self.get_float_param("pull_compensation_mm", 0)
|
||||
|
||||
@property
|
||||
@param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay'))
|
||||
def contour_underlay(self):
|
||||
# "Contour underlay" is stitching just inside the rectangular shape
|
||||
# of the satin column; that is, up one side and down the other.
|
||||
return self.get_boolean_param("contour_underlay")
|
||||
|
||||
@property
|
||||
@param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5)
|
||||
def contour_underlay_stitch_length(self):
|
||||
return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4)
|
||||
def contour_underlay_inset(self):
|
||||
# how far inside the edge of the column to stitch the underlay
|
||||
return self.get_float_param("contour_underlay_inset_mm", 0.4)
|
||||
|
||||
@property
|
||||
@param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay'))
|
||||
def center_walk_underlay(self):
|
||||
# "Center walk underlay" is stitching down and back in the centerline
|
||||
# between the two sides of the satin column.
|
||||
return self.get_boolean_param("center_walk_underlay")
|
||||
|
||||
@property
|
||||
@param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5)
|
||||
def center_walk_underlay_stitch_length(self):
|
||||
return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay'))
|
||||
def zigzag_underlay(self):
|
||||
return self.get_boolean_param("zigzag_underlay")
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3)
|
||||
def zigzag_underlay_spacing(self):
|
||||
return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float')
|
||||
def zigzag_underlay_inset(self):
|
||||
# how far in from the edge of the satin the points in the zigzags
|
||||
# should be
|
||||
|
||||
# Default to half of the contour underlay inset. That is, if we're
|
||||
# doing both contour underlay and zigzag underlay, make sure the
|
||||
# points of the zigzag fall outside the contour underlay but inside
|
||||
# the edges of the satin column.
|
||||
return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0
|
||||
|
||||
@property
|
||||
@cache
|
||||
def csp(self):
|
||||
return self.parse_path()
|
||||
|
||||
@property
|
||||
@cache
|
||||
def flattened_beziers(self):
|
||||
if len(self.csp) == 2:
|
||||
return self.simple_flatten_beziers()
|
||||
else:
|
||||
return self.flatten_beziers_with_rungs()
|
||||
|
||||
|
||||
def flatten_beziers_with_rungs(self):
|
||||
input_paths = [self.flatten([path]) for path in self.csp]
|
||||
input_paths = [shgeo.LineString(path[0]) for path in input_paths]
|
||||
|
||||
paths = input_paths[:]
|
||||
paths.sort(key=lambda path: path.length, reverse=True)
|
||||
|
||||
# Imagine a satin column as a curvy ladder.
|
||||
# The two long paths are the "rails" of the ladder. The remainder are
|
||||
# the "rungs".
|
||||
rails = paths[:2]
|
||||
rungs = shgeo.MultiLineString(paths[2:])
|
||||
|
||||
# The rails should stay in the order they were in the original CSP.
|
||||
# (this lets the user control where the satin starts and ends)
|
||||
rails.sort(key=lambda rail: input_paths.index(rail))
|
||||
|
||||
result = []
|
||||
|
||||
for rail in rails:
|
||||
if not rail.is_simple:
|
||||
self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns."))
|
||||
|
||||
# handle null intersections here?
|
||||
linestrings = shops.split(rail, rungs)
|
||||
|
||||
#print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs]
|
||||
if len(linestrings.geoms) < len(rungs.geoms) + 1:
|
||||
self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once."))
|
||||
elif len(linestrings.geoms) > len(rungs.geoms) + 1:
|
||||
self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once."))
|
||||
|
||||
paths = [[Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
|
||||
result.append(paths)
|
||||
|
||||
return zip(*result)
|
||||
|
||||
|
||||
def simple_flatten_beziers(self):
|
||||
# Given a pair of paths made up of bezier segments, flatten
|
||||
# each individual bezier segment into line segments that approximate
|
||||
# the curves. Retain the divisions between beziers -- we'll use those
|
||||
# later.
|
||||
|
||||
paths = []
|
||||
|
||||
for path in self.csp:
|
||||
# See the documentation in the parent class for parse_path() for a
|
||||
# description of the format of the CSP. Each bezier is constructed
|
||||
# using two neighboring 3-tuples in the list.
|
||||
|
||||
flattened_path = []
|
||||
|
||||
# iterate over pairs of 3-tuples
|
||||
for prev, current in zip(path[:-1], path[1:]):
|
||||
flattened_segment = self.flatten([[prev, current]])
|
||||
flattened_segment = [Point(x, y) for x, y in flattened_segment[0]]
|
||||
flattened_path.append(flattened_segment)
|
||||
|
||||
paths.append(flattened_path)
|
||||
|
||||
return zip(*paths)
|
||||
|
||||
def validate_satin_column(self):
|
||||
# The node should have exactly two paths with no fill. Each
|
||||
# path should have the same number of points, meaning that they
|
||||
# will both be made up of the same number of bezier curves.
|
||||
|
||||
node_id = self.node.get("id")
|
||||
|
||||
if self.get_style("fill") is not None:
|
||||
self.fatal(_("satin column: object %s has a fill (but should not)") % node_id)
|
||||
|
||||
if len(self.csp) == 2:
|
||||
if len(self.csp[0]) != len(self.csp[1]):
|
||||
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.csp[0]), length2=len(self.csp[1])))
|
||||
|
||||
def offset_points(self, pos1, pos2, offset_px):
|
||||
# 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
|
||||
|
||||
# don't contract beyond the midpoint, or we'll start expanding
|
||||
if offset_px < -distance / 2.0:
|
||||
offset_px = -distance / 2.0
|
||||
|
||||
pos1 = pos1 + (pos1 - pos2).unit() * offset_px
|
||||
pos2 = pos2 + (pos2 - pos1).unit() * offset_px
|
||||
|
||||
return pos1, pos2
|
||||
|
||||
def walk(self, path, start_pos, start_index, distance):
|
||||
# Move <distance> pixels along <path>, which is a sequence of line
|
||||
# segments defined by points.
|
||||
|
||||
# <start_index> is the index of the line segment in <path> that
|
||||
# we're currently on. <start_pos> is where along that line
|
||||
# segment we are. Return a new position and index.
|
||||
|
||||
# print >> dbg, "walk", start_pos, start_index, distance
|
||||
|
||||
pos = start_pos
|
||||
index = start_index
|
||||
last_index = len(path) - 1
|
||||
distance_remaining = distance
|
||||
|
||||
while True:
|
||||
if index >= last_index:
|
||||
return pos, index
|
||||
|
||||
segment_end = path[index + 1]
|
||||
segment = segment_end - pos
|
||||
segment_length = segment.length()
|
||||
|
||||
if segment_length > distance_remaining:
|
||||
# our walk ends partway along this segment
|
||||
return pos + segment.unit() * distance_remaining, index
|
||||
else:
|
||||
# our walk goes past the end of this segment, so advance
|
||||
# one point
|
||||
index += 1
|
||||
distance_remaining -= segment_length
|
||||
pos = segment_end
|
||||
|
||||
def walk_paths(self, spacing, offset):
|
||||
# Take a bezier segment from each path in turn, and plot out an
|
||||
# equal number of points on each bezier. Return the points plotted.
|
||||
# The points will be contracted or expanded by offset using
|
||||
# offset_points().
|
||||
|
||||
points = [[], []]
|
||||
|
||||
def add_pair(pos1, pos2):
|
||||
pos1, pos2 = self.offset_points(pos1, pos2, offset)
|
||||
points[0].append(pos1)
|
||||
points[1].append(pos2)
|
||||
|
||||
# We may not be able to fit an even number of zigzags in each pair of
|
||||
# beziers. We'll store the remaining bit of the beziers after handling
|
||||
# each section.
|
||||
remainder_path1 = []
|
||||
remainder_path2 = []
|
||||
|
||||
for segment1, segment2 in self.flattened_beziers:
|
||||
subpath1 = remainder_path1 + segment1
|
||||
subpath2 = remainder_path2 + segment2
|
||||
|
||||
len1 = shgeo.LineString(subpath1).length
|
||||
len2 = shgeo.LineString(subpath2).length
|
||||
|
||||
# Base the number of stitches in each section on the _longest_ of
|
||||
# the two beziers. Otherwise, things could get too sparse when one
|
||||
# side is significantly longer (e.g. when going around a corner).
|
||||
# The risk here is that we poke a hole in the fabric if we try to
|
||||
# cram too many stitches on the short bezier. The user will need
|
||||
# to avoid this through careful construction of paths.
|
||||
#
|
||||
# TODO: some commercial machine embroidery software compensates by
|
||||
# pulling in some of the "inner" stitches toward the center a bit.
|
||||
|
||||
# note, this rounds down using integer-division
|
||||
num_points = max(len1, len2) / spacing
|
||||
|
||||
spacing1 = len1 / num_points
|
||||
spacing2 = len2 / num_points
|
||||
|
||||
pos1 = subpath1[0]
|
||||
index1 = 0
|
||||
|
||||
pos2 = subpath2[0]
|
||||
index2 = 0
|
||||
|
||||
for i in xrange(int(num_points)):
|
||||
add_pair(pos1, pos2)
|
||||
|
||||
pos1, index1 = self.walk(subpath1, pos1, index1, spacing1)
|
||||
pos2, index2 = self.walk(subpath2, pos2, index2, spacing2)
|
||||
|
||||
if index1 < len(subpath1) - 1:
|
||||
remainder_path1 = [pos1] + subpath1[index1 + 1:]
|
||||
else:
|
||||
remainder_path1 = []
|
||||
|
||||
if index2 < len(subpath2) - 1:
|
||||
remainder_path2 = [pos2] + subpath2[index2 + 1:]
|
||||
else:
|
||||
remainder_path2 = []
|
||||
|
||||
# We're off by one in the algorithm above, so we need one more
|
||||
# pair of points. We also want to add points at the very end to
|
||||
# make sure we match the vectors on screen as best as possible.
|
||||
# Try to avoid doing both if they're going to stack up too
|
||||
# closely.
|
||||
|
||||
end1 = remainder_path1[-1]
|
||||
end2 = remainder_path2[-1]
|
||||
|
||||
if (end1 - pos1).length() > 0.3 * spacing:
|
||||
add_pair(pos1, pos2)
|
||||
|
||||
add_pair(end1, end2)
|
||||
|
||||
return points
|
||||
|
||||
def do_contour_underlay(self):
|
||||
# "contour walk" underlay: do stitches up one side and down the
|
||||
# other.
|
||||
forward, back = self.walk_paths(self.contour_underlay_stitch_length,
|
||||
-self.contour_underlay_inset)
|
||||
return Patch(color=self.color, stitches=(forward + list(reversed(back))))
|
||||
|
||||
def do_center_walk(self):
|
||||
# Center walk underlay is just a running stitch down and back on the
|
||||
# center line between the bezier curves.
|
||||
|
||||
# Do it like contour underlay, but inset all the way to the center.
|
||||
forward, back = self.walk_paths(self.center_walk_underlay_stitch_length,
|
||||
-100000)
|
||||
return Patch(color=self.color, stitches=(forward + list(reversed(back))))
|
||||
|
||||
def do_zigzag_underlay(self):
|
||||
# zigzag underlay, usually done at a much lower density than the
|
||||
# satin itself. It looks like this:
|
||||
#
|
||||
# \/\/\/\/\/\/\/\/\/\/|
|
||||
# /\/\/\/\/\/\/\/\/\/\|
|
||||
#
|
||||
# In combination with the "contour walk" underlay, this is the
|
||||
# "German underlay" described here:
|
||||
# http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/
|
||||
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0,
|
||||
-self.zigzag_underlay_inset)
|
||||
|
||||
# This organizes the points in each side in the order that they'll be
|
||||
# visited.
|
||||
sides = [sides[0][::2] + list(reversed(sides[0][1::2])),
|
||||
sides[1][1::2] + list(reversed(sides[1][::2]))]
|
||||
|
||||
# This fancy bit of iterable magic just repeatedly takes a point
|
||||
# from each side in turn.
|
||||
for point in chain.from_iterable(izip(*sides)):
|
||||
patch.add_stitch(point)
|
||||
|
||||
return patch
|
||||
|
||||
def do_satin(self):
|
||||
# satin: do a zigzag pattern, alternating between the paths. The
|
||||
# zigzag looks like this to make the satin stitches look perpendicular
|
||||
# to the column:
|
||||
#
|
||||
# /|/|/|/|/|/|/|/|
|
||||
|
||||
# print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation
|
||||
|
||||
patch = Patch(color=self.color)
|
||||
|
||||
sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation)
|
||||
|
||||
# Like in zigzag_underlay(): take a point from each side in turn.
|
||||
for point in chain.from_iterable(izip(*sides)):
|
||||
patch.add_stitch(point)
|
||||
|
||||
return patch
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
# Stitch a variable-width satin column, zig-zagging between two paths.
|
||||
|
||||
# The algorithm will draw zigzags between each consecutive pair of
|
||||
# beziers. The boundary points between beziers serve as "checkpoints",
|
||||
# allowing the user to control how the zigzags flow around corners.
|
||||
|
||||
# First, verify that we have valid paths.
|
||||
self.validate_satin_column()
|
||||
|
||||
patches = []
|
||||
|
||||
if self.center_walk_underlay:
|
||||
patches.append(self.do_center_walk())
|
||||
|
||||
if self.contour_underlay:
|
||||
patches.append(self.do_contour_underlay())
|
||||
|
||||
if self.zigzag_underlay:
|
||||
# zigzag underlay comes after contour walk underlay, so that the
|
||||
# zigzags sit on the contour walk underlay like rail ties on rails.
|
||||
patches.append(self.do_zigzag_underlay())
|
||||
|
||||
patches.append(self.do_satin())
|
||||
|
||||
return patches
|
|
@ -0,0 +1,119 @@
|
|||
from .. import _, Point
|
||||
from .element import param, EmbroideryElement, Patch
|
||||
from ..utils import cache
|
||||
|
||||
|
||||
class Stroke(EmbroideryElement):
|
||||
element_name = "Stroke"
|
||||
|
||||
@property
|
||||
@param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True)
|
||||
def satin_column(self):
|
||||
return self.get_boolean_param("satin_column")
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.get_style("stroke")
|
||||
|
||||
@property
|
||||
@cache
|
||||
def width(self):
|
||||
stroke_width = self.get_style("stroke-width")
|
||||
|
||||
if stroke_width.endswith("px"):
|
||||
stroke_width = stroke_width[:-2]
|
||||
|
||||
return float(stroke_width)
|
||||
|
||||
@property
|
||||
def dashed(self):
|
||||
return self.get_style("stroke-dasharray") is not None
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
|
||||
|
||||
@property
|
||||
@param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4)
|
||||
@cache
|
||||
def zigzag_spacing(self):
|
||||
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
|
||||
|
||||
@property
|
||||
@param('repeats', _('Repeats'), type='int', default="1")
|
||||
def repeats(self):
|
||||
return self.get_int_param("repeats", 1)
|
||||
|
||||
@property
|
||||
def paths(self):
|
||||
return self.flatten(self.parse_path())
|
||||
|
||||
def is_running_stitch(self):
|
||||
# stroke width <= 0.5 pixels is deprecated in favor of dashed lines
|
||||
return self.dashed or self.width <= 0.5
|
||||
|
||||
def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width):
|
||||
# TODO: use inkstitch.stitches.running_stitch
|
||||
|
||||
patch = Patch(color=self.color)
|
||||
p0 = emb_point_list[0]
|
||||
rho = 0.0
|
||||
side = 1
|
||||
last_segment_direction = None
|
||||
|
||||
for repeat in xrange(self.repeats):
|
||||
if repeat % 2 == 0:
|
||||
order = range(1, len(emb_point_list))
|
||||
else:
|
||||
order = range(-2, -len(emb_point_list) - 1, -1)
|
||||
|
||||
for segi in order:
|
||||
p1 = emb_point_list[segi]
|
||||
|
||||
# how far we have to go along segment
|
||||
seg_len = (p1 - p0).length()
|
||||
if (seg_len == 0):
|
||||
continue
|
||||
|
||||
# vector pointing along segment
|
||||
along = (p1 - p0).unit()
|
||||
|
||||
# vector pointing to edge of stroke width
|
||||
perp = along.rotate_left() * (stroke_width * 0.5)
|
||||
|
||||
if stroke_width == 0.0 and last_segment_direction is not None:
|
||||
if abs(1.0 - along * last_segment_direction) > 0.5:
|
||||
# if greater than 45 degree angle, stitch the corner
|
||||
rho = zigzag_spacing
|
||||
patch.add_stitch(p0)
|
||||
|
||||
# iteration variable: how far we are along segment
|
||||
while (rho <= seg_len):
|
||||
left_pt = p0 + along * rho + perp * side
|
||||
patch.add_stitch(left_pt)
|
||||
rho += zigzag_spacing
|
||||
side = -side
|
||||
|
||||
p0 = p1
|
||||
last_segment_direction = along
|
||||
rho -= seg_len
|
||||
|
||||
if (p0 - patch.stitches[-1]).length() > 0.1:
|
||||
patch.add_stitch(p0)
|
||||
|
||||
return patch
|
||||
|
||||
def to_patches(self, last_patch):
|
||||
patches = []
|
||||
|
||||
for path in self.paths:
|
||||
path = [Point(x, y) for x, y in path]
|
||||
if self.is_running_stitch():
|
||||
patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0)
|
||||
else:
|
||||
patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.width)
|
||||
|
||||
patches.append(patch)
|
||||
|
||||
return patches
|
|
@ -0,0 +1,103 @@
|
|||
import inkex
|
||||
from .elements import AutoFill, Fill, Stroke, SatinColumn, EmbroideryElement
|
||||
from . import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM
|
||||
|
||||
|
||||
class InkstitchExtension(inkex.Effect):
|
||||
"""Base class for Inkstitch extensions. Not intended for direct use."""
|
||||
|
||||
def hide_all_layers(self):
|
||||
for g in self.document.getroot().findall(SVG_GROUP_TAG):
|
||||
if g.get(INKSCAPE_GROUPMODE) == "layer":
|
||||
g.set("style", "display:none")
|
||||
|
||||
def no_elements_error(self):
|
||||
if self.selected:
|
||||
inkex.errormsg(_("No embroiderable paths selected."))
|
||||
else:
|
||||
inkex.errormsg(_("No embroiderable paths found in document."))
|
||||
inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering."))
|
||||
|
||||
def descendants(self, node):
|
||||
nodes = []
|
||||
element = EmbroideryElement(node)
|
||||
|
||||
if element.has_style('display') and element.get_style('display') is None:
|
||||
return []
|
||||
|
||||
if node.tag == SVG_DEFS_TAG:
|
||||
return []
|
||||
|
||||
for child in node:
|
||||
nodes.extend(self.descendants(child))
|
||||
|
||||
if node.tag in EMBROIDERABLE_TAGS:
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
def get_nodes(self):
|
||||
"""Get all XML nodes, or just those selected
|
||||
|
||||
effect is an instance of a subclass of inkex.Effect.
|
||||
"""
|
||||
|
||||
if self.selected:
|
||||
nodes = []
|
||||
for node in self.document.getroot().iter():
|
||||
if node.get("id") in self.selected:
|
||||
nodes.extend(self.descendants(node))
|
||||
else:
|
||||
nodes = self.descendants(self.document.getroot())
|
||||
|
||||
return nodes
|
||||
|
||||
def detect_classes(self, node):
|
||||
if node.tag == SVG_POLYLINE_TAG:
|
||||
return [Polyline]
|
||||
else:
|
||||
element = EmbroideryElement(node)
|
||||
|
||||
if element.get_boolean_param("satin_column"):
|
||||
return [SatinColumn]
|
||||
else:
|
||||
classes = []
|
||||
|
||||
if element.get_style("fill"):
|
||||
if element.get_boolean_param("auto_fill", True):
|
||||
classes.append(AutoFill)
|
||||
else:
|
||||
classes.append(Fill)
|
||||
|
||||
if element.get_style("stroke"):
|
||||
classes.append(Stroke)
|
||||
|
||||
if element.get_boolean_param("stroke_first", False):
|
||||
classes.reverse()
|
||||
|
||||
return classes
|
||||
|
||||
|
||||
def get_elements(self):
|
||||
self.elements = []
|
||||
for node in self.get_nodes():
|
||||
classes = self.detect_classes(node)
|
||||
self.elements.extend(cls(node) for cls in classes)
|
||||
|
||||
if self.elements:
|
||||
return True
|
||||
else:
|
||||
self.no_elements_error()
|
||||
return False
|
||||
|
||||
def elements_to_patches(self, elements):
|
||||
patches = []
|
||||
for element in elements:
|
||||
if patches:
|
||||
last_patch = patches[-1]
|
||||
else:
|
||||
last_patch = None
|
||||
|
||||
patches.extend(element.embroider(last_patch))
|
||||
|
||||
return patches
|
|
@ -1,4 +1,5 @@
|
|||
from .. import Stitch, Point, PIXELS_PER_MM
|
||||
from .. import Stitch, PIXELS_PER_MM
|
||||
from ..utils.geometry import Point
|
||||
from .stop import process_stop
|
||||
from .trim import process_trim
|
||||
from .ties import add_ties
|
||||
|
|
|
@ -40,4 +40,7 @@ def add_ties(stitch_plan):
|
|||
else:
|
||||
new_stitches.append(stitch)
|
||||
|
||||
if not need_tie_in:
|
||||
add_tie_off(new_stitches)
|
||||
|
||||
color_block.replace_stitches(new_stitches)
|
||||
|
|
|
@ -416,7 +416,7 @@ def connect_points(shape, start, end, running_stitch_length):
|
|||
#print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction
|
||||
#dbg.flush()
|
||||
|
||||
stitches = [InkstitchPoint(*start)]
|
||||
stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])]
|
||||
|
||||
for i in xrange(num_stitches):
|
||||
pos = (pos + one_stitch) % outline.length
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .. import Point as InkstitchPoint, cache, PIXELS_PER_MM
|
||||
from .. import PIXELS_PER_MM
|
||||
from ..utils import cache, Point as InkstitchPoint
|
||||
import shapely
|
||||
import math
|
||||
import sys
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from geometry import *
|
||||
from cache import cache
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
try:
|
||||
from functools import lru_cache
|
||||
except ImportError:
|
||||
from backports.functools_lru_cache import lru_cache
|
||||
|
||||
# simplify use of lru_cache decorator
|
||||
def cache(*args, **kwargs):
|
||||
return lru_cache(maxsize=None)(*args, **kwargs)
|
|
@ -1,5 +1,6 @@
|
|||
from .. import Point as InkstitchPoint
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
import math
|
||||
|
||||
|
||||
def cut(line, distance):
|
||||
""" Cuts a LineString in two at a distance from its starting point.
|
||||
|
@ -38,5 +39,64 @@ def cut_path(points, length):
|
|||
path = LineString(points)
|
||||
subpath, rest = cut(path, length)
|
||||
|
||||
return [InkstitchPoint(*point) for point in subpath.coords]
|
||||
return [Point(*point) for point in subpath.coords]
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __add__(self, other):
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other):
|
||||
return Point(self.x - other.x, self.y - other.y)
|
||||
|
||||
def mul(self, scalar):
|
||||
return Point(self.x * scalar, self.y * scalar)
|
||||
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Point):
|
||||
# dot product
|
||||
return self.x * other.x + self.y * other.y
|
||||
elif isinstance(other, (int, float)):
|
||||
return Point(self.x * other, self.y * other)
|
||||
else:
|
||||
raise ValueError("cannot multiply Point by %s" % type(other))
|
||||
|
||||
def __rmul__(self, other):
|
||||
if isinstance(other, (int, float)):
|
||||
return self.__mul__(other)
|
||||
else:
|
||||
raise ValueError("cannot multiply Point by %s" % type(other))
|
||||
|
||||
def __repr__(self):
|
||||
return "Point(%s,%s)" % (self.x, self.y)
|
||||
|
||||
def length(self):
|
||||
return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
|
||||
|
||||
def unit(self):
|
||||
return self.mul(1.0 / self.length())
|
||||
|
||||
def rotate_left(self):
|
||||
return Point(-self.y, self.x)
|
||||
|
||||
def rotate(self, angle):
|
||||
return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
|
||||
|
||||
def as_int(self):
|
||||
return Point(int(round(self.x)), int(round(self.y)))
|
||||
|
||||
def as_tuple(self):
|
||||
return (self.x, self.y)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.as_tuple(), other.as_tuple())
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.as_tuple()[item]
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
|
150
messages.po
150
messages.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-03-10 22:11-0500\n"
|
||||
"POT-Creation-Date: 2018-03-11 21:59-0400\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,146 +17,12 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Fill"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manually routed fill stitching"
|
||||
msgstr ""
|
||||
|
||||
msgid "Angle of lines of stitches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Flip fill (start right-to-left)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Spacing between rows"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum fill stitch length"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stagger rows this many times before repeating"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto-Fill"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically routed fill stitching"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running stitch length (traversal between sections)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "AutoFill Underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fill angle (default: fill angle + 90 deg)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Row spacing (default: 3x fill row spacing)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Max stitch length"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inset"
|
||||
msgstr ""
|
||||
|
||||
msgid "Satin stitch along paths"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running stitch length"
|
||||
msgstr ""
|
||||
|
||||
msgid "Zig-zag spacing (peak-to-peak)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Repeats"
|
||||
msgstr ""
|
||||
|
||||
msgid "Satin Column"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom satin column"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pull compensation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contour underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contour Underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stitch length"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contour underlay inset amount"
|
||||
msgstr ""
|
||||
|
||||
msgid "Center-walk underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Center-Walk Underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Zig-zag underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Zig-zag Underlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Zig-Zag spacing (peak-to-peak)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inset amount (default: half of contour underlay inset)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"One or more rails crosses itself, and this is not allowed. Please split "
|
||||
"into multiple satin columns."
|
||||
msgstr ""
|
||||
|
||||
msgid "satin column: One or more of the rungs doesn't intersect both rails."
|
||||
msgstr ""
|
||||
|
||||
msgid "Each rail should intersect both rungs once."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"satin column: One or more of the rungs intersects the rails more than once."
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "satin column: object %s has a fill (but should not)"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"satin column: object %(id)s has two paths with an unequal number of points "
|
||||
"(%(length1)d and %(length2)d)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"\n"
|
||||
"\n"
|
||||
"Seeing a 'no such option' message? Please restart Inkscape to fix."
|
||||
msgstr ""
|
||||
|
||||
msgid "No embroiderable paths selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "No embroiderable paths found in document."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Tip: use Path -> Object to Path to convert non-paths before embroidering."
|
||||
msgstr ""
|
||||
|
||||
msgid "These settings will be applied to 1 object."
|
||||
msgstr ""
|
||||
|
||||
|
@ -241,17 +107,3 @@ msgstr ""
|
|||
#, python-format
|
||||
msgid "Unknown unit: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "TRIM after"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trim thread after this object (for supported machines and file formats)"
|
||||
msgstr ""
|
||||
|
||||
msgid "STOP after"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Add STOP instruction after this object (for supported machines and file "
|
||||
"formats)"
|
||||
msgstr ""
|
||||
|
|
Ładowanie…
Reference in New Issue