refactor, step 4

pull/127/head
Lex Neva 2018-03-11 21:59:31 -04:00
rodzic 0dc3ffefe4
commit a47073e3ee
20 zmienionych plików z 1271 dodań i 1394 usunięć

Wyświetl plik

@ -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()

Wyświetl plik

@ -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):

Wyświetl plik

@ -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)

Wyświetl plik

@ -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("#"):

Wyświetl plik

@ -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

Wyświetl plik

@ -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)]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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]

Wyświetl plik

@ -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]

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -1 +1,2 @@
from geometry import *
from cache import cache

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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 ""