kopia lustrzana https://github.com/inkstitch/inkstitch
rewrite of Embroidery Params into a full GUI app
The Embroidery Params filter now pops up a full GTK dialog. This alows it to load existing values in the selected shapes and present them to the user. The user can also load and save presets. If selected shapes have differing values for a given param, the values are presented in a dropdown so the user can select one to apply to all.pull/2/merge
rodzic
1fedbc11b5
commit
a7bfc17e7c
191
embroider.py
191
embroider.py
|
@ -50,12 +50,52 @@ SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
|
|||
SVG_GROUP_TAG = inkex.addNS('g', 'svg')
|
||||
|
||||
|
||||
class EmbroideryElement(object):
|
||||
class Param(object):
|
||||
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None):
|
||||
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
|
||||
|
||||
def __init__(self, node, options):
|
||||
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, options=None):
|
||||
self.node = node
|
||||
self.options = options
|
||||
|
||||
@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()
|
||||
|
@ -99,6 +139,9 @@ class EmbroideryElement(object):
|
|||
|
||||
return value
|
||||
|
||||
def set_param(self, name, value):
|
||||
self.node.set("embroider_%s" % name, value)
|
||||
|
||||
@cache
|
||||
def get_style(self, style_name):
|
||||
style = simplestyle.parseStyle(self.node.get("style"))
|
||||
|
@ -184,11 +227,16 @@ class EmbroideryElement(object):
|
|||
|
||||
|
||||
class Fill(EmbroideryElement):
|
||||
|
||||
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')
|
||||
@cache
|
||||
def angle(self):
|
||||
return math.radians(self.get_float_param('angle', 0))
|
||||
|
@ -198,18 +246,22 @@ class Fill(EmbroideryElement):
|
|||
return self.get_style("fill")
|
||||
|
||||
@property
|
||||
@param('flip', 'Flip fill (start right-to-left)', type='boolean')
|
||||
def flip(self):
|
||||
return self.get_boolean_param("flip", False)
|
||||
|
||||
@property
|
||||
@param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float')
|
||||
def row_spacing(self):
|
||||
return self.get_float_param("row_spacing_mm")
|
||||
|
||||
@property
|
||||
@param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float')
|
||||
def max_stitch_length(self):
|
||||
return self.get_float_param("max_stitch_length_mm")
|
||||
|
||||
@property
|
||||
@param('staggers', 'Stagger rows this many times before repeating', type='int')
|
||||
def staggers(self):
|
||||
return self.get_int_param("staggers", 4)
|
||||
|
||||
|
@ -478,6 +530,11 @@ class Fill(EmbroideryElement):
|
|||
|
||||
|
||||
class AutoFill(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):
|
||||
|
@ -493,14 +550,17 @@ class AutoFill(Fill):
|
|||
return False
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float')
|
||||
def running_stitch_length(self):
|
||||
return self.get_float_param("running_stitch_length_mm")
|
||||
|
||||
@property
|
||||
@param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay')
|
||||
def fill_underlay(self):
|
||||
return self.get_boolean_param("fill_underlay")
|
||||
|
||||
@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")
|
||||
|
@ -511,11 +571,13 @@ class AutoFill(Fill):
|
|||
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)
|
||||
|
@ -607,7 +669,7 @@ class AutoFill(Fill):
|
|||
|
||||
return self.section_to_patch(section, angle, row_spacing, max_stitch_length)
|
||||
|
||||
def auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None):
|
||||
def do_auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None):
|
||||
rows_of_segments = self.intersect_region_with_grating(angle, row_spacing)
|
||||
sections = self.pull_runs(rows_of_segments)
|
||||
|
||||
|
@ -637,15 +699,19 @@ class AutoFill(Fill):
|
|||
last_stitch = last_patch.stitches[-1]
|
||||
|
||||
if self.fill_underlay:
|
||||
patches.extend(self.auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, last_stitch))
|
||||
patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, last_stitch))
|
||||
last_stitch = patches[-1].stitches[-1]
|
||||
|
||||
patches.extend(self.auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch))
|
||||
patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch))
|
||||
|
||||
return patches
|
||||
|
||||
|
||||
class Stroke(EmbroideryElement):
|
||||
@property
|
||||
@param('satin_column', 'Satin along paths', type='toggle', inverse=True)
|
||||
def satin_column(self):
|
||||
return self.get_boolean_param("satin_column")
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
|
@ -666,15 +732,18 @@ class Stroke(EmbroideryElement):
|
|||
return self.get_style("stroke-dasharray") is not None
|
||||
|
||||
@property
|
||||
@param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float')
|
||||
def running_stitch_length(self):
|
||||
return self.get_float_param("running_stitch_length_mm")
|
||||
|
||||
@property
|
||||
@param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
|
||||
@cache
|
||||
def zigzag_spacing(self):
|
||||
return self.get_float_param("zigzag_spacing_mm")
|
||||
|
||||
@property
|
||||
@param('repeats', 'Repeats', type='int')
|
||||
def repeats(self):
|
||||
return self.get_int_param("repeats", 1)
|
||||
|
||||
|
@ -751,25 +820,26 @@ class Stroke(EmbroideryElement):
|
|||
|
||||
|
||||
class SatinColumn(EmbroideryElement):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SatinColumn, self).__init__(*args, **kwargs)
|
||||
|
||||
self.csp = self.parse_path()
|
||||
self.flattened_beziers = self.get_flattened_paths()
|
||||
|
||||
# print >> dbg, "flattened beziers", self.flattened_beziers
|
||||
@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')
|
||||
def zigzag_spacing(self):
|
||||
# peak-to-peak distance between zigzags
|
||||
return self.get_float_param("zigzag_spacing_mm")
|
||||
|
||||
@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
|
||||
|
@ -777,42 +847,50 @@ class SatinColumn(EmbroideryElement):
|
|||
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')
|
||||
def contour_underlay_stitch_length(self):
|
||||
# use "contour_underlay_stitch_length", or, if not set, default to "stitch_length"
|
||||
return self.get_float_param("contour_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
|
||||
|
||||
@property
|
||||
@param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float')
|
||||
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')
|
||||
def center_walk_underlay_stitch_length(self):
|
||||
# use "center_walk_underlay_stitch_length", or, if not set, default to "stitch_length"
|
||||
return self.get_float_param("center_walk_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
|
||||
|
||||
@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')
|
||||
def zigzag_underlay_spacing(self):
|
||||
# peak-to-peak distance between zigzags in zigzag underlay
|
||||
return self.get_float_param("zigzag_underlay_spacing_mm", 1)
|
||||
|
||||
@property
|
||||
@param('zigzag_underlay_inset', '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
|
||||
|
@ -823,7 +901,14 @@ class SatinColumn(EmbroideryElement):
|
|||
# the edges of the satin column.
|
||||
return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0
|
||||
|
||||
def get_flattened_paths(self):
|
||||
@property
|
||||
@cache
|
||||
def csp(self):
|
||||
return self.parse_path()
|
||||
|
||||
@property
|
||||
@cache
|
||||
def flattened_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
|
||||
|
@ -1088,8 +1173,48 @@ class SatinColumn(EmbroideryElement):
|
|||
return patches
|
||||
|
||||
|
||||
class Patch:
|
||||
def detect_classes(node):
|
||||
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 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 == SVG_PATH_TAG:
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
class Patch:
|
||||
def __init__(self, color=None, stitches=None):
|
||||
self.color = color
|
||||
self.stitches = stitches or []
|
||||
|
@ -1227,41 +1352,11 @@ class Embroider(inkex.Effect):
|
|||
|
||||
def handle_node(self, node):
|
||||
print >> dbg, "handling node", node.get('id'), node.get('tag')
|
||||
|
||||
element = EmbroideryElement(node, self.options)
|
||||
|
||||
if element.has_style('display') and element.get_style('display') is None:
|
||||
return
|
||||
|
||||
if node.tag == SVG_DEFS_TAG:
|
||||
return
|
||||
|
||||
for child in node:
|
||||
self.handle_node(child)
|
||||
|
||||
if node.tag != SVG_PATH_TAG:
|
||||
return
|
||||
|
||||
# dbg.write("Node: %s\n"%str((id, etree.tostring(node, pretty_print=True))))
|
||||
|
||||
if element.get_boolean_param("satin_column"):
|
||||
self.elements.append(SatinColumn(node, self.options))
|
||||
else:
|
||||
elements = []
|
||||
|
||||
if element.get_style("fill"):
|
||||
if element.get_boolean_param("auto_fill", True):
|
||||
elements.append(AutoFill(node, self.options))
|
||||
else:
|
||||
elements.append(Fill(node, self.options))
|
||||
|
||||
if element.get_style("stroke"):
|
||||
elements.append(Stroke(node, self.options))
|
||||
|
||||
if element.get_boolean_param("stroke_first", False):
|
||||
elements.reverse()
|
||||
|
||||
self.elements.extend(elements)
|
||||
nodes = descendants(node)
|
||||
for node in nodes:
|
||||
classes = detect_classes(node)
|
||||
print >> dbg, "classes:", classes
|
||||
self.elements.extend(cls(node, self.options) for cls in classes)
|
||||
|
||||
def get_output_path(self):
|
||||
svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'))
|
||||
|
|
1147
embroider.py.save
1147
embroider.py.save
Plik diff jest za duży
Load Diff
|
@ -4,28 +4,6 @@
|
|||
<id>jonh.embroider.params</id>
|
||||
<dependency type="executable" location="extensions">embroider_params.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
<param name="zigzag_spacing_mm" type="string" _gui-text="Zigzag spacing (mm)"></param>
|
||||
<param name="running_stitch_length_mm" type="string" _gui-text="Running stitch length (mm)"></param>
|
||||
<param name="row_spacing_mm" type="string" _gui-text="Row spacing (mm)"></param>
|
||||
<param name="max_stitch_length_mm" type="string" _gui-text="Maximum stitch length for fills (mm)"></param>
|
||||
<param name="repeats" type="string" _gui-text="Repeats (stroke only)"></param>
|
||||
<param name="angle" type="string" _gui-text="Angle for lines in fills (degrees)"></param>
|
||||
<param name="pull_compensation_mm" type="string" _gui-text="Pull compensation for satin column (mm)"></param>
|
||||
<param name="flip" type="string" _gui-text="Flip fill? (yes/no)"></param>
|
||||
<param name="satin_column" type="string" _gui-text="Satin Column? (yes/no)"></param>
|
||||
<param name="contour_underlay" type="string" _gui-text="Edge-walk underlay for satin Column? (yes/no)"></param>
|
||||
<param name="contour_underlay_inset_mm" type="string" _gui-text="Inset for satin colum underlay (mm)"></param>
|
||||
<param name="contour_underlay_stitch_length_mm" type="string" _gui-text="Stitch length (mm) for contour underlay (defaults to running stitch length)"></param>
|
||||
<param name="center_walk_underlay" type="string" _gui-text="Satin center walk underlay? (yes/no)"></param>
|
||||
<param name="center_walk_underlay_stitch_length_mm" type="string" _gui-text="Stitch length (mm) for center walk underlay (defaults to running stitch length)"></param>
|
||||
<param name="zigzag_underlay" type="string" _gui-text="Zig-Zag underlay for satin Column? (yes/no)"></param>
|
||||
<param name="zigzag_spacing_mm" type="string" _gui-text="Zigzag underlay spacing (mm)"></param>
|
||||
<param name="zigzag_underlay_inset_mm" type="string" _gui-text="Inset for zigzag underlay (mm)"></param>
|
||||
<param name="stroke_first" type="string" _gui-text="Stitch stroke before fill? (yes/no)"></param>
|
||||
<param name="fill_underlay" type="string" _gui-text="Underlay for fill stitch? (yes/no)"></param>
|
||||
<param name="fill_underlay_angle" type="string" _gui-text="Angle for lines in fill underlay (degrees)"></param>
|
||||
<param name="fill_underlay_row_spacing_mm" type="string" _gui-text="Row spacing for fill underlay (mm)"></param>
|
||||
<param name="fill_underlay_max_stitch_length_mm" type="string" _gui-text="Maximum stitch length for fill underlay (mm)"></param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
|
|
|
@ -1,59 +1,573 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Set embroidery parameter attributes on all selected objects. If an option
|
||||
# value is blank, the parameter is created as blank on all objects that don't
|
||||
# already have it. If an option value is given, any existing node parameter
|
||||
# values are overwritten on all selected objects.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sys
|
||||
sys.path.append("/usr/share/inkscape/extensions")
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from cStringIO import StringIO
|
||||
import wx
|
||||
from wx.lib.scrolledpanel import ScrolledPanel
|
||||
from collections import defaultdict
|
||||
import inkex
|
||||
from embroider import Param, EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn, descendants
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
|
||||
|
||||
def presets_path():
|
||||
try:
|
||||
import appdirs
|
||||
config_path = appdirs.user_config_dir('inkscape-embroidery')
|
||||
except ImportError:
|
||||
config_path = os.path.expanduser('~/.inkscape-embroidery')
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
os.makedirs(config_path)
|
||||
return os.path.join(config_path, 'presets.json')
|
||||
|
||||
|
||||
def load_presets():
|
||||
try:
|
||||
with open(presets_path(), 'r') as presets:
|
||||
presets = json.load(presets)
|
||||
return presets
|
||||
except:
|
||||
return {}
|
||||
|
||||
|
||||
def save_presets(presets):
|
||||
with open(presets_path(), 'w') as presets_file:
|
||||
json.dump(presets, presets_file)
|
||||
|
||||
|
||||
def load_preset(name):
|
||||
return load_presets().get(name)
|
||||
|
||||
|
||||
def save_preset(name, data):
|
||||
presets = load_presets()
|
||||
presets[name] = data
|
||||
save_presets(presets)
|
||||
|
||||
|
||||
def delete_preset(name):
|
||||
presets = load_presets()
|
||||
presets.pop(name, None)
|
||||
save_presets(presets)
|
||||
|
||||
|
||||
def confirm_dialog(parent, question, caption = 'inkscape-embroidery'):
|
||||
dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
|
||||
result = dlg.ShowModal() == wx.ID_YES
|
||||
dlg.Destroy()
|
||||
return result
|
||||
|
||||
|
||||
def info_dialog(parent, message, caption = 'inkscape-embroidery'):
|
||||
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
|
||||
dlg.ShowModal()
|
||||
dlg.Destroy()
|
||||
|
||||
|
||||
class ParamsTab(ScrolledPanel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.params = kwargs.pop('params', [])
|
||||
self.name = kwargs.pop('name', None)
|
||||
self.nodes = kwargs.pop('nodes')
|
||||
kwargs["style"] = wx.TAB_TRAVERSAL
|
||||
ScrolledPanel.__init__(self, *args, **kwargs)
|
||||
self.SetupScrolling()
|
||||
|
||||
self.changed_inputs = set()
|
||||
self.dependent_tabs = []
|
||||
self.parent_tab = None
|
||||
self.param_inputs = {}
|
||||
self.paired_tab = None
|
||||
self.disable_notify_pair = False
|
||||
|
||||
toggles = [param for param in self.params if param.type == 'toggle']
|
||||
|
||||
if toggles:
|
||||
self.toggle = toggles[0]
|
||||
self.params.remove(self.toggle)
|
||||
self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description)
|
||||
|
||||
value = any(self.toggle.values)
|
||||
if self.toggle.inverse:
|
||||
value = not value
|
||||
self.toggle_checkbox.SetValue(value)
|
||||
|
||||
self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state)
|
||||
self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed)
|
||||
|
||||
self.param_inputs[self.toggle.name] = self.toggle_checkbox
|
||||
else:
|
||||
self.toggle = None
|
||||
|
||||
self.settings_grid = wx.FlexGridSizer(rows=0, cols=3, hgap=10, vgap=10)
|
||||
self.settings_grid.AddGrowableCol(0, 1)
|
||||
self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL)
|
||||
|
||||
self.__set_properties()
|
||||
self.__do_layout()
|
||||
|
||||
if self.toggle:
|
||||
self.update_toggle_state()
|
||||
# end wxGlade
|
||||
|
||||
def pair(self, tab):
|
||||
# print self.name, "paired with", tab.name
|
||||
self.paired_tab = tab
|
||||
self.update_description()
|
||||
|
||||
def add_dependent_tab(self, tab):
|
||||
self.dependent_tabs.append(tab)
|
||||
self.update_description()
|
||||
|
||||
def set_parent_tab(self, tab):
|
||||
self.parent_tab = tab
|
||||
|
||||
def update_toggle_state(self, event=None, notify_pair=True):
|
||||
enable = self.toggle_checkbox.IsChecked()
|
||||
# print self.name, "update_toggle_state", enable
|
||||
for child in self.settings_grid.GetChildren():
|
||||
widget = child.GetWindow()
|
||||
if widget:
|
||||
child.GetWindow().Enable(enable)
|
||||
|
||||
if notify_pair and self.paired_tab:
|
||||
self.paired_tab.pair_changed(self.toggle_checkbox.IsChecked())
|
||||
|
||||
for tab in self.dependent_tabs:
|
||||
tab.dependent_enable(enable)
|
||||
|
||||
if event:
|
||||
event.Skip()
|
||||
|
||||
def pair_changed(self, value):
|
||||
# print self.name, "pair_changed", value
|
||||
new_value = not value
|
||||
|
||||
if self.toggle_checkbox.IsChecked() != new_value:
|
||||
self.set_toggle_state(not value)
|
||||
self.toggle_checkbox.changed = True
|
||||
self.update_toggle_state(notify_pair=False)
|
||||
|
||||
def dependent_enable(self, enable):
|
||||
if enable:
|
||||
self.toggle_checkbox.Enable()
|
||||
else:
|
||||
self.set_toggle_state(False)
|
||||
self.toggle_checkbox.Disable()
|
||||
self.toggle_checkbox.changed = True
|
||||
self.update_toggle_state()
|
||||
|
||||
def set_toggle_state(self, value):
|
||||
self.toggle_checkbox.SetValue(value)
|
||||
|
||||
def get_values(self):
|
||||
values = {}
|
||||
|
||||
if self.toggle:
|
||||
checked = self.toggle_checkbox.IsChecked()
|
||||
if self.toggle_checkbox in self.changed_inputs:
|
||||
values[self.toggle.name] = checked
|
||||
|
||||
if not checked:
|
||||
# Ignore params on this tab if the toggle is unchecked,
|
||||
# because they're grayed out anyway.
|
||||
return values
|
||||
|
||||
for name, input in self.param_inputs.iteritems():
|
||||
if input in self.changed_inputs:
|
||||
values[name] = input.GetValue()
|
||||
|
||||
return values
|
||||
|
||||
def apply(self):
|
||||
values = self.get_values()
|
||||
for node in self.nodes:
|
||||
print >> sys.stderr, node.id, values
|
||||
for name, value in values.iteritems():
|
||||
node.set_param(name, value)
|
||||
|
||||
def changed(self, event):
|
||||
self.changed_inputs.add(event.GetEventObject())
|
||||
event.Skip()
|
||||
|
||||
def load_preset(self, preset):
|
||||
preset_data = preset.get(self.name, {})
|
||||
|
||||
for name, value in preset_data.iteritems():
|
||||
if name in self.param_inputs:
|
||||
self.param_inputs[name].SetValue(value)
|
||||
self.changed_inputs.add(self.param_inputs[name])
|
||||
|
||||
self.update_toggle_state()
|
||||
|
||||
def save_preset(self, storage):
|
||||
preset = storage[self.name] = {}
|
||||
for name, input in self.param_inputs.iteritems():
|
||||
preset[name] = input.GetValue()
|
||||
|
||||
def update_description(self):
|
||||
description = "These settings will be applied to %d object%s." % \
|
||||
(len(self.nodes), "s" if len(self.nodes) != 1 else "")
|
||||
|
||||
if any(len(param.values) > 1 for param in self.params):
|
||||
description += "\n • Some settings had different values across objects. Select a value from the dropdown or enter a new one."
|
||||
|
||||
if self.dependent_tabs:
|
||||
description += "\n • Disabling this tab will disable the following %d tabs." % len(self.dependent_tabs)
|
||||
|
||||
if self.paired_tab:
|
||||
description += "\n • Enabling this tab will disable %s and vice-versa." % self.paired_tab.name
|
||||
|
||||
self.description_text = description
|
||||
|
||||
def resized(self, event):
|
||||
if not hasattr(self, 'rewrap_timer'):
|
||||
self.rewrap_timer = wx.Timer()
|
||||
self.rewrap_timer.Bind(wx.EVT_TIMER, self.rewrap)
|
||||
|
||||
# If we try to rewrap every time we get EVT_SIZE then a resize is
|
||||
# extremely slow.
|
||||
self.rewrap_timer.Start(50, oneShot=True)
|
||||
event.Skip()
|
||||
|
||||
def rewrap(self, event=None):
|
||||
self.description.SetLabel(self.description_text)
|
||||
self.description.Wrap(self.GetSize().x - 20)
|
||||
self.description_container.Layout()
|
||||
if event:
|
||||
event.Skip()
|
||||
|
||||
def __set_properties(self):
|
||||
# begin wxGlade: SatinPane.__set_properties
|
||||
# end wxGlade
|
||||
pass
|
||||
|
||||
def __do_layout(self):
|
||||
# just to add space around the settings
|
||||
box = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
summary_box = wx.StaticBox(self, wx.ID_ANY, label="Inkscape objects")
|
||||
sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL)
|
||||
# sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.description = wx.StaticText(self, style=wx.TE_WORDWRAP)
|
||||
self.update_description()
|
||||
self.description.SetLabel(self.description_text)
|
||||
self.description_container = box
|
||||
self.Bind(wx.EVT_SIZE, self.resized)
|
||||
sizer.Add(self.description, proportion=0, flag=wx.EXPAND|wx.ALL, border=5)
|
||||
box.Add(sizer, proportion=0, flag=wx.ALL, border=5)
|
||||
|
||||
if self.toggle:
|
||||
box.Add(self.toggle_checkbox, proportion=0, flag=wx.BOTTOM, border=10)
|
||||
|
||||
for param in self.params:
|
||||
self.settings_grid.Add(wx.StaticText(self, label=param.description), proportion=1, flag=wx.EXPAND|wx.RIGHT, border=40)
|
||||
|
||||
if param.type == 'boolean':
|
||||
|
||||
values = list(set(param.values))
|
||||
if len(values) > 1:
|
||||
input = wx.CheckBox(self, style=wx.CHK_3STATE)
|
||||
input.Set3StateValue(wx.CHK_UNDETERMINED)
|
||||
elif values:
|
||||
input = wx.CheckBox(self)
|
||||
input.SetValue(values[0])
|
||||
|
||||
input.Bind(wx.EVT_CHECKBOX, self.changed)
|
||||
elif len(param.values) > 1:
|
||||
input = wx.ComboBox(self, wx.ID_ANY, choices=param.values, style=wx.CB_DROPDOWN | wx.CB_SORT)
|
||||
input.Bind(wx.EVT_COMBOBOX, self.changed)
|
||||
input.Bind(wx.EVT_TEXT, self.changed)
|
||||
else:
|
||||
value = param.values[0] if param.values else ""
|
||||
input = wx.TextCtrl(self, wx.ID_ANY, value=value)
|
||||
input.Bind(wx.EVT_TEXT, self.changed)
|
||||
|
||||
self.param_inputs[param.name] = input
|
||||
|
||||
self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
|
||||
box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10)
|
||||
self.SetSizer(box)
|
||||
|
||||
self.Layout()
|
||||
|
||||
# end of class SatinPane
|
||||
|
||||
class SettingsFrame(wx.Frame):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# begin wxGlade: MyFrame.__init__
|
||||
self.tabs_factory = kwargs.pop('tabs_factory', [])
|
||||
wx.Frame.__init__(self, None, wx.ID_ANY,
|
||||
"Embroidery Params"
|
||||
)
|
||||
self.notebook = wx.Notebook(self, wx.ID_ANY)
|
||||
self.tabs = self.tabs_factory(self.notebook)
|
||||
|
||||
self.presets_box = wx.StaticBox(self, wx.ID_ANY, label="Presets")
|
||||
|
||||
self.preset_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_SORT)
|
||||
self.update_preset_list()
|
||||
|
||||
self.load_preset_button = wx.Button(self, wx.ID_ANY, "Load")
|
||||
self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset)
|
||||
|
||||
self.add_preset_button = wx.Button(self, wx.ID_ANY, "Add")
|
||||
self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset)
|
||||
|
||||
self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, "Overwrite")
|
||||
self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset)
|
||||
|
||||
self.delete_preset_button = wx.Button(self, wx.ID_ANY, "Delete")
|
||||
self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset)
|
||||
|
||||
self.cancel_button = wx.Button(self, wx.ID_ANY, "Cancel")
|
||||
self.cancel_button.Bind(wx.EVT_BUTTON, self.close)
|
||||
|
||||
self.apply_button = wx.Button(self, wx.ID_ANY, "Apply and Quit")
|
||||
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
|
||||
|
||||
self.__set_properties()
|
||||
self.__do_layout()
|
||||
# end wxGlade
|
||||
|
||||
def update_preset_list(self):
|
||||
self.preset_chooser.SetItems(load_presets().keys())
|
||||
|
||||
def get_preset_name(self):
|
||||
preset_name = self.preset_chooser.GetValue().strip()
|
||||
if preset_name:
|
||||
return preset_name
|
||||
else:
|
||||
info_dialog(self, "Please enter or select a preset name first.", caption='Preset')
|
||||
return
|
||||
|
||||
def check_and_load_preset(self, preset_name):
|
||||
preset = load_preset(preset_name)
|
||||
if not preset:
|
||||
info_dialog(self, 'Preset "%s" not found.' % preset_name, caption='Preset')
|
||||
|
||||
return preset
|
||||
|
||||
def get_preset_data(self):
|
||||
preset = {}
|
||||
|
||||
current_tab = self.tabs[self.notebook.GetSelection()]
|
||||
while current_tab.parent_tab:
|
||||
current_tab = current_tab.parent_tab
|
||||
|
||||
tabs = [current_tab]
|
||||
if current_tab.paired_tab:
|
||||
tabs.append(current_tab.paired_tab)
|
||||
tabs.extend(current_tab.dependent_tabs)
|
||||
|
||||
for tab in tabs:
|
||||
tab.save_preset(preset)
|
||||
|
||||
return preset
|
||||
|
||||
def add_preset(self, event, overwrite=False):
|
||||
preset_name = self.get_preset_name()
|
||||
if not preset_name:
|
||||
return
|
||||
|
||||
if not overwrite and load_preset(preset_name):
|
||||
info_dialog(self, 'Preset "%s" already exists. Please use another name or press "Overwrite"' % preset_name, caption='Preset')
|
||||
|
||||
save_preset(preset_name, self.get_preset_data())
|
||||
self.update_preset_list()
|
||||
|
||||
event.Skip()
|
||||
|
||||
def overwrite_preset(self, event):
|
||||
self.add_preset(event, overwrite=True)
|
||||
|
||||
def load_preset(self, event):
|
||||
preset_name = self.get_preset_name()
|
||||
if not preset_name:
|
||||
return
|
||||
|
||||
preset = self.check_and_load_preset(preset_name)
|
||||
if not preset:
|
||||
return
|
||||
|
||||
for tab in self.tabs:
|
||||
tab.load_preset(preset)
|
||||
|
||||
event.Skip()
|
||||
|
||||
def delete_preset(self, event):
|
||||
preset_name = self.get_preset_name()
|
||||
if not preset_name:
|
||||
return
|
||||
|
||||
preset = self.check_and_load_preset(preset_name)
|
||||
if not preset:
|
||||
return
|
||||
|
||||
delete_preset(preset_name)
|
||||
self.update_preset_list()
|
||||
self.preset_chooser.SetValue("")
|
||||
|
||||
event.Skip()
|
||||
|
||||
def apply(self, event):
|
||||
for tab in self.tabs:
|
||||
tab.apply()
|
||||
|
||||
self.Close()
|
||||
|
||||
def close(self, event):
|
||||
self.Close()
|
||||
|
||||
def __set_properties(self):
|
||||
# begin wxGlade: MyFrame.__set_properties
|
||||
self.SetTitle("frame_1")
|
||||
self.notebook.SetMinSize((800, 400))
|
||||
self.preset_chooser.SetSelection(-1)
|
||||
# end wxGlade
|
||||
|
||||
def __do_layout(self):
|
||||
# begin wxGlade: MyFrame.__do_layout
|
||||
sizer_1 = wx.BoxSizer(wx.VERTICAL)
|
||||
#self.sizer_3_staticbox.Lower()
|
||||
sizer_2 = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL)
|
||||
sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
|
||||
for tab in self.tabs:
|
||||
self.notebook.AddPage(tab, tab.name)
|
||||
sizer_1.Add(self.notebook, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, 10)
|
||||
sizer_2.Add(self.preset_chooser, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
sizer_2.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
sizer_2.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
sizer_2.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
sizer_2.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
sizer_1.Add(sizer_2, 0, flag=wx.EXPAND|wx.ALL, border=10)
|
||||
sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT|wx.RIGHT, 5)
|
||||
sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5)
|
||||
sizer_1.Add(sizer_3, 0, wx.ALIGN_RIGHT, 0)
|
||||
self.SetSizer(sizer_1)
|
||||
sizer_1.Fit(self)
|
||||
self.Layout()
|
||||
# end wxGlade
|
||||
|
||||
class EmbroiderParams(inkex.Effect):
|
||||
def get_nodes(self):
|
||||
if self.selected:
|
||||
nodes = []
|
||||
for node in self.document.getroot().iter():
|
||||
if node.get("id") in self.selected:
|
||||
nodes.extend(descendants(node))
|
||||
else:
|
||||
nodes = descendants(self.document.getroot())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
inkex.Effect.__init__(self)
|
||||
return nodes
|
||||
|
||||
self.params = ["zigzag_spacing_mm",
|
||||
"running_stitch_length_mm",
|
||||
"row_spacing_mm",
|
||||
"max_stitch_length_mm",
|
||||
"repeats",
|
||||
"angle",
|
||||
"flip",
|
||||
"satin_column",
|
||||
"stroke_first",
|
||||
"pull_compensation_mm",
|
||||
"contour_underlay",
|
||||
"contour_underlay_inset_mm",
|
||||
"contour_underlay_stitch_length_mm",
|
||||
"center_walk_underlay",
|
||||
"center_walk_underlay_stitch_length_mm",
|
||||
"zigzag_underlay",
|
||||
"zigzag_underlay_inset_mm",
|
||||
"fill_underlay",
|
||||
"fill_underlay_angle",
|
||||
"fill_underlay_row_spacing_mm",
|
||||
"fill_underlay_max_stitch_length_mm",
|
||||
]
|
||||
def embroidery_classes(self, node):
|
||||
element = EmbroideryElement(node)
|
||||
classes = []
|
||||
|
||||
if element.get_style("fill"):
|
||||
classes.append(AutoFill)
|
||||
classes.append(Fill)
|
||||
elif element.get_style("stroke"):
|
||||
classes.append(Stroke)
|
||||
|
||||
if element.get_style("stroke-dasharray") is None:
|
||||
classes.append(SatinColumn)
|
||||
|
||||
return classes
|
||||
|
||||
def get_nodes_by_class(self):
|
||||
nodes = self.get_nodes()
|
||||
nodes_by_class = defaultdict(list)
|
||||
|
||||
for node in self.get_nodes():
|
||||
for cls in self.embroidery_classes(node):
|
||||
nodes_by_class[cls].append(node)
|
||||
|
||||
return sorted(nodes_by_class.items(), key=lambda (cls, nodes): cls.__name__)
|
||||
|
||||
def get_values(self, param, nodes):
|
||||
getter = 'get_param'
|
||||
|
||||
if param.type in ('toggle', 'boolean'):
|
||||
getter = 'get_boolean_param'
|
||||
elif param.type:
|
||||
getter = 'get_%s_param' % param.type
|
||||
|
||||
values = filter(lambda item: item is not None,
|
||||
(getattr(node, getter)(param.name, param.default) for node in nodes))
|
||||
|
||||
if param.type in ('int', 'float'):
|
||||
values = [str(value) for value in values]
|
||||
|
||||
return values
|
||||
|
||||
def group_params(self, params):
|
||||
def by_group(param):
|
||||
return param.group
|
||||
|
||||
return groupby(sorted(params, key=by_group), by_group)
|
||||
|
||||
def create_tabs(self, parent):
|
||||
tabs = []
|
||||
for cls, nodes in self.get_nodes_by_class():
|
||||
nodes = [cls(node) for node in nodes]
|
||||
params = cls.get_params()
|
||||
|
||||
for param in params:
|
||||
param.values = self.get_values(param, nodes)
|
||||
|
||||
parent_tab = None
|
||||
new_tabs = []
|
||||
for group, params in self.group_params(params):
|
||||
tab = ParamsTab(parent, id=wx.ID_ANY, name=group or cls.__name__, params=list(params), nodes=nodes)
|
||||
new_tabs.append(tab)
|
||||
|
||||
if group is None:
|
||||
parent_tab = tab
|
||||
|
||||
for tab in new_tabs:
|
||||
if tab != parent_tab:
|
||||
parent_tab.add_dependent_tab(tab)
|
||||
tab.set_parent_tab(parent_tab)
|
||||
|
||||
tabs.extend(new_tabs)
|
||||
|
||||
for tab in tabs:
|
||||
if tab.toggle and tab.toggle.inverse:
|
||||
for other_tab in tabs:
|
||||
if other_tab != tab and other_tab.toggle.name == tab.toggle.name:
|
||||
tab.pair(other_tab)
|
||||
other_tab.pair(tab)
|
||||
|
||||
return tabs
|
||||
|
||||
for param in self.params:
|
||||
self.OptionParser.add_option("--%s" % param, default="")
|
||||
|
||||
def effect(self):
|
||||
for node in self.selected.itervalues():
|
||||
for param in self.params:
|
||||
value = getattr(self.options, param).strip()
|
||||
param = "embroider_" + param
|
||||
app = wx.App()
|
||||
frame = SettingsFrame(tabs_factory=self.create_tabs)
|
||||
frame.Show()
|
||||
app.MainLoop()
|
||||
|
||||
if node.get(param) is not None and not value:
|
||||
# only overwrite existing params if they gave a value
|
||||
continue
|
||||
else:
|
||||
node.set(param, value)
|
||||
# end of class MyFrame
|
||||
if __name__ == "__main__":
|
||||
# GTK likes to spam stderr, which inkscape will show in a dialog.
|
||||
null = open('/dev/null', 'w')
|
||||
stderr_dup = os.dup(sys.stderr.fileno())
|
||||
os.dup2(null.fileno(), 2)
|
||||
stderr_backup = sys.stderr
|
||||
sys.stderr = StringIO()
|
||||
|
||||
if __name__ == '__main__':
|
||||
e = EmbroiderParams()
|
||||
e.affect()
|
||||
|
||||
os.dup2(stderr_dup, 2)
|
||||
stderr_backup.write(sys.stderr.getvalue())
|
||||
sys.stderr = stderr_backup
|
||||
|
|
Ładowanie…
Reference in New Issue