diff --git a/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz b/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz new file mode 100644 index 00000000..b7844c51 Binary files /dev/null and b/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz differ diff --git a/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz b/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz new file mode 100644 index 00000000..d84345fc Binary files /dev/null and b/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz differ diff --git a/logs/silhouette_cameo3_default_settings.pcapng.gz b/logs/silhouette_cameo3_default_settings.pcapng.gz new file mode 100644 index 00000000..ea35f088 Binary files /dev/null and b/logs/silhouette_cameo3_default_settings.pcapng.gz differ diff --git a/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz b/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz new file mode 100644 index 00000000..72d7a69a Binary files /dev/null and b/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz differ diff --git a/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz b/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz new file mode 100644 index 00000000..8a82b970 Binary files /dev/null and b/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz differ diff --git a/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz b/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz new file mode 100644 index 00000000..d6c359f0 Binary files /dev/null and b/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz differ diff --git a/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz b/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz new file mode 100644 index 00000000..52fff7c7 Binary files /dev/null and b/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz differ diff --git a/logs/silhouette_cameo3_pen.pcapng.gz b/logs/silhouette_cameo3_pen.pcapng.gz new file mode 100644 index 00000000..8513ce93 Binary files /dev/null and b/logs/silhouette_cameo3_pen.pcapng.gz differ diff --git a/logs/silhouette_cameo3_ratchet_blade.pcapng.gz b/logs/silhouette_cameo3_ratchet_blade.pcapng.gz new file mode 100644 index 00000000..309a0614 Binary files /dev/null and b/logs/silhouette_cameo3_ratchet_blade.pcapng.gz differ diff --git a/logs/silhouette_cameo3_tool_2.pcapng.gz b/logs/silhouette_cameo3_tool_2.pcapng.gz new file mode 100644 index 00000000..8862bd63 Binary files /dev/null and b/logs/silhouette_cameo3_tool_2.pcapng.gz differ diff --git a/logs/silhouette_cameo3_track_enhancing.pcapng.gz b/logs/silhouette_cameo3_track_enhancing.pcapng.gz new file mode 100644 index 00000000..fa8dce08 Binary files /dev/null and b/logs/silhouette_cameo3_track_enhancing.pcapng.gz differ diff --git a/sendto_silhouette.inx b/sendto_silhouette.inx index d41063d5..2ecbcc6c 100644 --- a/sendto_silhouette.inx +++ b/sendto_silhouette.inx @@ -7,12 +7,22 @@ sendto_silhouette.py - 0.0 - 0.0 + 0.0 + 0.0 + + Cameo 12x12 + Cameo 12x24 + None + + + Red (left tool) + Blue (right tool) + Media default Pen Cut + AutoBlade 'pen' executes the strokes exactly as sent, 'cut' adds small serifs to help the knive find its orientation at corners. @@ -20,11 +30,11 @@ [P>0 S>0] Custom: use non-zero Pressure and Speed below [P=27,S=10] SCard without Craft Paper Backing [P=27,S=10] Card with Craft Paper Backing - [P=10,S=10] Vinyl Sticker + [P=10,S= 5,D=1] Vinyl Sticker [P=14,S=10] Film Labels [P=27,S=10] Thick Media [P= 2,S=10] Thin Media - [P=10,S=10] Pen + [P=18,S=10] Pen [P=30,S=10] Bond Paper 13-28 lbs (105g) [P=30,S=10] Bristol Paper 57-67 lbs (145g) [P=30,S=10] Cardstock 40-60 lbs (90g) @@ -47,14 +57,14 @@ 0 0 + -1 + Use speed=0, pressure=0, depth=-1 to take the media defaults. Pressure values of 19 or more could trigger the trackenhancing feature, which means a movement along the full media height before start. Beware. - Use speed=0, pressure=0 to take the media defaults. Pressure values of 19 or more could trigger the trackenhancing feature, which means a movement along the full media height before start. Beware. false Convert paths with dashed strokes to separate subpaths for perforated cuts. false Shift to the top lefthand corner, then do offsets. false To see the used area, tick the checkmark above and use pressure=1 (or better remove tool) - 0.5 1 false @@ -79,6 +89,10 @@ false Keep dialog open until device becomes idle again. + false Lift head at sharp corners + 0.1 + 0.1 + 0.5 Z-Order Without mat @@ -89,6 +103,7 @@ Without mat: Subdivide, sort, and choose cut directions, so that a cutting mat is not needed in most cases. Minimal Traveling: Find the nearest startpoint to minimize travel movements Minimal Traveling (fully optimized): Additionally search startpoints in closed paths + true false Used for debugging and developent only! @@ -115,7 +130,7 @@ Always use the least amount of blade possible. inkscape-silhouette extension from https://github.com/jnweiger/inkscape-silhouette by Jürgen Weigert [juergen@fabmail.org] and contributors - Version 1.20 + Version 1.21 diff --git a/sendto_silhouette.py b/sendto_silhouette.py index 4d339e25..86b29bed 100644 --- a/sendto_silhouette.py +++ b/sendto_silhouette.py @@ -72,19 +72,21 @@ # Added misc/silhouette_move.py misc/silhouette_cut.py, misc/endless_clock.py # 2016-01-15 jw, v1.15 -- ubuntu loads the wrong usb library. # 2016-05-15 jw, v1.16 -- merged regmarks code from https://github.com/fablabnbg/inkscape-silhouette/pull/23 -# 2016-05-17 jw, v1.17 -- fix avoid dev.reset in Graphtec.py, fix helps with +# 2016-05-17 jw, v1.17 -- fix avoid dev.reset in Graphtec.py, fix helps with # https://github.com/fablabnbg/inkscape-silhouette/issues/10 # 2016-05-21 jw, v1.18 -- warn about python-usb < 1.0 and give instructions. -# Limit pressure to 18. 19 or 20 make the machine +# Limit pressure to 18. 19 or 20 make the machine # scroll forward backward for several minutes. # Support document unit inches. https://github.com/fablabnbg/inkscape-silhouette/issues/19 # 2016-12-18, jw, v1.19 -- support for dashed lines added. Thanks to mehtank # https://github.com/fablabnbg/inkscape-silhouette/pull/33 # Added new cutting strategy "Minimized Traveling" # Added parameter for blade diameter -# 2018-06-01, jw, v1.20-- Make it compile again. Hmm. +# 2018-06-01, jw, v1.20 -- Make it compile again. Hmm. +# 2019-07-25, jw, v1.21 -- merge from github.com/olegdeezus/inkscape-silhouette +# merge from fablabnbg -__version__ = '1.20' # Keep in sync with sendto_silhouette.inx ca line 79 +__version__ = '1.21' # Keep in sync with sendto_silhouette.inx ca line 79 __author__ = 'Juergen Weigert and contributors' import sys, os, shutil, time, logging, tempfile, math, re @@ -276,6 +278,12 @@ def __init__(self): self.OptionParser.add_option('-c', '--bladediameter', action = 'store', dest = 'bladediameter', type = 'float', default = 0.9, help="[0..2.3] diameter of the used blade [mm], default = 0.9") + self.OptionParser.add_option('-C', '--cuttingmat', action = 'store', + choices=('cameo_12x12', 'cameo_12x24', 'no_mat'), dest = 'cuttingmat', default = 'cameo_12x12', + help='Use cutting mat') + self.OptionParser.add_option('-D', '--depth', + action = 'store', dest = 'depth', type = 'int', default = -1, + help="[0..10], or -1 for media default") self.OptionParser.add_option('--dummy', action = 'store', dest = 'dummy', type = 'inkbool', default = False, help="Dump raw data to "+self.dumpname+" instead of cutting.") @@ -283,6 +291,9 @@ def __init__(self): action = 'store', dest = 'strategy', default = 'mintravel', choices=('mintravel','mintravelfull','matfree','zorder' ), help="Cutting Strategy: mintravel, mintravelfull, matfree or zorder") + self.OptionParser.add_option('-l', '--sw_clipping', + action = 'store', dest = 'sw_clipping', type = 'inkbool', default = True, + help='Enable software clipping') self.OptionParser.add_option('-m', '--media', '--media-id', '--media_id', action = 'store', dest = 'media', default = '132', choices=('100','101','102','106','111','112','113', @@ -298,6 +309,15 @@ def __init__(self): self.OptionParser.add_option('-p', '--pressure', action = 'store', dest = 'pressure', type = 'int', default = 10, help="[1..18], or 0 for media default") + self.OptionParser.add_option('-P', '--sharpencorners', + action = 'store', dest = 'sharpencorners', type = 'inkbool', default = False, + help='Lift head at sharp corners') + self.OptionParser.add_option('--sharpencorners_start', + action = 'store', dest = 'sharpencorners_start', type = 'float', default = 0.1, + help="Sharpen Corners - Start Ext. [mm]") + self.OptionParser.add_option('--sharpencorners_end', + action = 'store', dest = 'sharpencorners_end', type = 'float', default = 0.1, + help="Sharpen Corners - End Ext. [mm]") self.OptionParser.add_option('-r', '--reversetoggle', action = 'store', dest = 'reversetoggle', type = 'inkbool', default = False, help="Cut each path the other direction. Affects every second pass when multipass.") @@ -307,7 +327,9 @@ def __init__(self): self.OptionParser.add_option( "-S", "--smoothness", action="store", type="float", dest="smoothness", default=.2, help="Smoothness of curves" ) self.OptionParser.add_option('-t', '--tool', action = 'store', - choices=('cut', 'pen','default'), dest = 'tool', default = None, help="Optimize for pen or knive") + choices=('autoblade', 'cut', 'pen','default'), dest = 'tool', default = None, help="Optimize for pen or knive") + self.OptionParser.add_option('-T', '--toolholder', action = 'store', + choices=('1', '2'), dest = 'toolholder', default = None, help="[1..2]") self.OptionParser.add_option('-V', '--version', action = 'store_const', const=True, dest = 'version', default = False, help='Just print version number ("'+__version__+'") and exit.') @@ -475,11 +497,18 @@ def recursivelyTraverseSvg( self, aNodeList, for node in aNodeList: # Ignore invisible nodes - v = node.get( 'visibility', parent_visibility ) + v = None + style = node.get('style') + if style is not None: + kvs = {k.strip():v.strip() for k,v in [x.split(':', 1) for x in style.split(';')]} + if 'display' in kvs and kvs['display'] == 'none': + v = 'hidden' + if v is None: + v = node.get( 'visibility', parent_visibility ) if v == 'inherit': v = parent_visibility if v == 'hidden' or v == 'collapse': - pass + continue # calculate this object's transform transform = composeParents(node, IDENTITY_TRANSFORM) @@ -1001,9 +1030,15 @@ def write_progress(done, total, msg): # Traverse the entire document self.recursivelyTraverseSvg( self.document.getroot() ) + if self.options.toolholder is not None: + self.options.toolholder = int(self.options.toolholder) self.pen=None + self.autoblade=False if self.options.tool == 'pen': self.pen=True if self.options.tool == 'cut': self.pen=False + if self.options.tool == 'autoblade': + self.pen=False + self.autoblade=True # scale all points to unit mm for path in self.paths: @@ -1044,6 +1079,20 @@ def write_progress(done, total, msg): # on a closed path some overlapping doesn't harm, limited to a maximum of one additional round overcut = self.options.overcut if (overcut > 0) and self.is_closed_path(mm_path): + precut = overcut + pfrom = mm_path[-1] + for pprev in reversed(mm_path[:-1]): + dx = pprev[0] - pfrom[0] + dy = pprev[1] - pfrom[1] + dist = math.sqrt(dx*dx + dy*dy) + if (precut > dist): # Full segment needed + precut -= dist + multipath.insert(0, pprev) + pfrom = pprev + else: # only partial segement needed, create new endpoint + pprev = (pfrom[0]+dx*(precut/dist), pfrom[1]+dy*(precut/dist)) + multipath.insert(0, pprev) + break pfrom = mm_path[0] for pnext in mm_path[1:]: dx = pnext[0] - pfrom[0] @@ -1082,7 +1131,16 @@ def write_progress(done, total, msg): if self.options.pressure == 0: self.options.pressure = None if self.options.speed == 0: self.options.speed = None + if self.options.depth == -1: self.options.depth = None dev.setup(media=int(self.options.media,10), pen=self.pen, + toolholder=self.options.toolholder, + cuttingmat=self.options.cuttingmat, + sharpencorners=self.options.sharpencorners, + sharpencorners_start=self.options.sharpencorners_start, + sharpencorners_end=self.options.sharpencorners_end, + autoblade=self.autoblade, + depth=self.options.depth, + sw_clipping=self.options.sw_clipping, bladediameter=self.options.bladediameter, pressure=self.options.pressure, speed=self.options.speed) @@ -1163,15 +1221,15 @@ def write_progress(done, total, msg): e = SendtoSilhouette() if len(sys.argv) < 2: - # write a tempfile that is autoremoved on exit - tmpfile=tempfile.NamedTemporaryFile(suffix='.svg', prefix='inkscape-silhouette') - sys.argv.append(tmpfile.name) - print sys.argv - print >>tmpfile, '' - tmpfile.flush() + # write a tempfile that is autoremoved on exit + tmpfile=tempfile.NamedTemporaryFile(suffix='.svg', prefix='inkscape-silhouette') + sys.argv.append(tmpfile.name) + print sys.argv + print >>tmpfile, '' + tmpfile.flush() e.affect(sys.argv[1:]) - # os.remove(tmpfile.name) - sys.exit(0) + # os.remove(tmpfile.name) + sys.exit(0) start = time.time() e.affect() diff --git a/silhouette/Graphtec.py b/silhouette/Graphtec.py index e21bee16..57a6a5ab 100644 --- a/silhouette/Graphtec.py +++ b/silhouette/Graphtec.py @@ -83,34 +83,34 @@ MEDIA = [ # CAUTION: keep in sync with sendto_silhouette.inx -# media, pressure, speed, cap-color, name - ( 100, 27, 10, "yellow", "Card without Craft Paper Backing"), - ( 101, 27, 10, "yellow", "Card with Craft Paper Backing"), - ( 102, 10, 10, "blue", "Vinyl Sticker"), - ( 106, 14, 10, "blue", "Film Labels"), - ( 111, 27, 10, "yellow", "Thick Media"), - ( 112, 2, 10, "blue", "Thin Media"), - ( 113, 10, 10, "pen", "Pen"), - ( 120, 30, 10, "blue", "Bond Paper 13-28 lbs (105g)"), - ( 121, 30, 10, "yellow", "Bristol Paper 57-67 lbs (145g)"), - ( 122, 30, 10, "yellow", "Cardstock 40-60 lbs (90g)"), - ( 123, 30, 10, "yellow", "Cover 40-60 lbs (170g)"), - ( 124, 1, 10, "blue", "Film, Double Matte Translucent"), - ( 125, 1, 10, "blue", "Film, Vinyl With Adhesive Back"), - ( 126, 1, 10, "blue", "Film, Window With Kling Adhesive"), - ( 127, 30, 10, "red", "Index 90 lbs (165g)"), - ( 128, 20, 10, "yellow", "Inkjet Photo Paper 28-44 lbs (70g)"), - ( 129, 27, 10, "red", "Inkjet Photo Paper 45-75 lbs (110g)"), - ( 130, 30, 3, "red", "Magnetic Sheet"), - ( 131, 30, 10, "blue", "Offset 24-60 lbs (90g)"), - ( 132, 5, 10, "blue", "Print Paper Light Weight"), - ( 133, 25, 10, "yellow", "Print Paper Medium Weight"), - ( 134, 20, 10, "blue", "Sticker Sheet"), - ( 135, 20, 10, "red", "Tag 100 lbs (275g)"), - ( 136, 30, 10, "blue", "Text Paper 24-70 lbs (105g)"), - ( 137, 30, 10, "yellow", "Vellum Bristol 57-67 lbs (145g)"), - ( 138, 30, 10, "blue", "Writing Paper 24-70 lbs (105g)"), - ( 300, None, None, "custom", "Custom"), +# media, pressure, speed, depth, cap-color, name + ( 100, 27, 10, 1, "yellow", "Card without Craft Paper Backing"), + ( 101, 27, 10, 1, "yellow", "Card with Craft Paper Backing"), + ( 102, 10, 5, 1, "blue", "Vinyl Sticker"), + ( 106, 14, 10, 1, "blue", "Film Labels"), + ( 111, 27, 10, 1, "yellow", "Thick Media"), + ( 112, 2, 10, 1, "blue", "Thin Media"), + ( 113, 18, 10,None, "pen", "Pen"), + ( 120, 30, 10, 1, "blue", "Bond Paper 13-28 lbs (105g)"), + ( 121, 30, 10, 1, "yellow", "Bristol Paper 57-67 lbs (145g)"), + ( 122, 30, 10, 1, "yellow", "Cardstock 40-60 lbs (90g)"), + ( 123, 30, 10, 1, "yellow", "Cover 40-60 lbs (170g)"), + ( 124, 1, 10, 1, "blue", "Film, Double Matte Translucent"), + ( 125, 1, 10, 1, "blue", "Film, Vinyl With Adhesive Back"), + ( 126, 1, 10, 1, "blue", "Film, Window With Kling Adhesive"), + ( 127, 30, 10, 1, "red", "Index 90 lbs (165g)"), + ( 128, 20, 10, 1, "yellow", "Inkjet Photo Paper 28-44 lbs (70g)"), + ( 129, 27, 10, 1, "red", "Inkjet Photo Paper 45-75 lbs (110g)"), + ( 130, 30, 3, 1, "red", "Magnetic Sheet"), + ( 131, 30, 10, 1, "blue", "Offset 24-60 lbs (90g)"), + ( 132, 5, 10, 1, "blue", "Print Paper Light Weight"), + ( 133, 25, 10, 1, "yellow", "Print Paper Medium Weight"), + ( 134, 20, 10, 1, "blue", "Sticker Sheet"), + ( 135, 20, 10, 1, "red", "Tag 100 lbs (275g)"), + ( 136, 30, 10, 1, "blue", "Text Paper 24-70 lbs (105g)"), + ( 137, 30, 10, 1, "yellow", "Vellum Bristol 57-67 lbs (145g)"), + ( 138, 30, 10, 1, "blue", "Writing Paper 24-70 lbs (105g)"), + ( 300, None, None,None, "custom", "Custom"), ] # robocut/Plotter.h:53 ff @@ -120,26 +120,33 @@ PRODUCT_ID_SILHOUETTE_SD_1 = 0x111c PRODUCT_ID_SILHOUETTE_SD_2 = 0x111d PRODUCT_ID_SILHOUETTE_CAMEO = 0x1121 +PRODUCT_ID_SILHOUETTE_CAMEO2 = 0x112b +PRODUCT_ID_SILHOUETTE_CAMEO3 = 0x112f PRODUCT_ID_SILHOUETTE_PORTRAIT = 0x1123 +PRODUCT_ID_SILHOUETTE_PORTRAIT2 = 0x1132 DEVICE = [ - { 'vendor_id': 0x0b4d, 'product_id': 0x1123, 'name': 'Silhouette Portrait', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT, 'name': 'Silhouette Portrait', 'width_mm': 206, 'length_mm': 3000, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x1132, 'name': 'Silhouette Portrait2', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT2, 'name': 'Silhouette Portrait2', 'width_mm': 203, 'length_mm': 3000, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x1121, 'name': 'Silhouette Cameo', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO, 'name': 'Silhouette Cameo', # margin_top_mm is just for safety when moving backwards with thin media # margin_left_mm is a physical limit, but is relative to width_mm! 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x112b, 'name': 'Silhouette Cameo2', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO2, 'name': 'Silhouette Cameo2', + # margin_top_mm is just for safety when moving backwards with thin media + # margin_left_mm is a physical limit, but is relative to width_mm! 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x112f, 'name': 'Silhouette Cameo3', - 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':5, 'margin_top_mm':15.5, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x110a, 'name': 'Craft Robo CC200-20', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO3, 'name': 'Silhouette Cameo3', + # margin_top_mm is just for safety when moving backwards with thin media + # margin_left_mm is a physical limit, but is relative to width_mm! + 'width_mm': 304.8, 'length_mm': 3000, 'margin_left_mm':0.0, 'margin_top_mm':0.0, 'regmark': True }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC200_20, 'name': 'Craft Robo CC200-20', 'width_mm': 200, 'length_mm': 1000, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111a, 'name': 'Craft Robo CC300-20' }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111c, 'name': 'Silhouette SD 1' }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111d, 'name': 'Silhouette SD 2' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC300_20, 'name': 'Craft Robo CC300-20' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_1, 'name': 'Silhouette SD 1' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_2, 'name': 'Silhouette SD 2' }, ] def _bbox_extend(bb, x, y): @@ -283,6 +290,10 @@ def __init__(self, log=sys.stderr, no_device=False, progress_cb=None): self.regmark = False # not yet implemented. See robocut/Plotter.cpp:446 if self.dev is None or 'width_mm' in self.hardware: self.leftaligned = True + self.enable_sw_clipping = True + + def product_id(s): + return s.hardware['product_id'] if 'product_id' in s.hardware else None def write(s, string, timeout=10000): """Send a command to the device. Long commands are sent in chunks of 4096 bytes. @@ -499,21 +510,31 @@ def initialize(s): #except: # pass - #s.write("TB71\x03") # Get machine calibration of regmark sensor (y, x) in machine units - #try: - # resp = s.read(timeout=1000) - # if len(resp) > 1: - # print("TB71: '%s'" % (resp[:-1]), file=s.log) - #except: - # pass + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: - #s.write("FA\x03") # Get machine calibration (x, y) (carriage, roller) unit is 0.01% i.e. 0.0001 - #try: - # resp = s.read(timeout=1000) - # if len(resp) > 1: - # print("FA: '%s'" % (resp[:-1]), file=s.log) # response '0,0' - #except: - # pass + s.write("TB71\x03") # Unknown: 2 five digit numbers + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("TB71: '%s'" % (resp[:-1]), file=s.log) + except: + pass + + s.write("FA\x03") # Unknown: 2 five digit numbers + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("FA: '%s'" % (resp[:-1]), file=s.log) # response '0,0' + except: + pass + + s.write("TC\x03") + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("TC: '%s'" % (resp[:-1]), file=s.log) # response '0,0' + except: + pass def get_version(s): """Retrieve the firmware version string from the device.""" @@ -529,7 +550,7 @@ def get_version(s): return resp[0:-2] # chop of 0x03 - def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=False, bladediameter=0.9, landscape=False, leftaligned=None): + def setup(s, media=132, speed=None, pressure=None, toolholder=None, pen=None, cuttingmat=None, sharpencorners=False, sharpencorners_start=0.1, sharpencorners_end=0.1, autoblade=False, depth=None, sw_clipping=True, trackenhancing=False, bladediameter=0.9, landscape=False, leftaligned=None, mediawidth=210.0, mediaheight=297.0): """media range is [100..300], default 132, "Print Paper Light Weight" speed range is [1..10], default None, from paper (132 -> 10) pressure range is [1..33], default None, from paper (132 -> 5) @@ -548,9 +569,35 @@ def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=Fals s.initialize() + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if cuttingmat == 'cameo_12x12': + s.write("TG1\x03") + elif cuttingmat == 'cameo_12x24': + s.write("TG2\x03") + else: + s.write("TG0\x03") + + #FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other + # side of media (boundary check?), but next cut run will stall + #TB50,x: x = 1 landscape mode, x = 0 portrait mode + s.write("FN0\x03TB50,0\x03") + + if cuttingmat == 'cameo_12x12': + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, 6096, 6096)) + elif cuttingmat == 'cameo_12x24': + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, 12192, 6096)) + else: + width = s.hardware['width_mm'] if 'width_mm' in s.hardware else mediawidth + height = s.hardware['length_mm'] if 'length_mm' in s.hardware else mediaheight + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, height * 20.0, width * 20.0)) + if media is not None: if media < 100 or media > 300: media = 300 - s.write("FW%d\x03" % media); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + # Silhouette Studio does not appear to issue this command + pass + else: + s.write("FW%d\x03" % media); if pen is None: if media == 113: pen = True @@ -558,39 +605,86 @@ def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=Fals pen = False for i in MEDIA: if i[0] == media: - print("Media=%d, cap='%s', name='%s'" % (media, i[3], i[4]), file=s.log) + print("Media=%d, cap='%s', name='%s'" % (media, i[4], i[5]), file=s.log) if pressure is None: pressure = i[1] if speed is None: speed = i[2] + if depth is None: depth = i[3] + break + + if toolholder is None: + toolholder = 1 + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("J%d\x03" % toolholder) + print("toolholder: %d" % toolholder, file=s.log) if speed is not None: if speed < 1: speed = 1 if speed > 10: speed = 10 - s.write("!%d\x03" % speed); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("!%d,%d\x03" % (speed, toolholder)); + else: + s.write("!%d\x03" % speed); print("speed: %d" % speed, file=s.log) if pressure is not None: if pressure < 1: pressure = 1 if pressure > 33: pressure = 33 - s.write("FX%d\x03" % pressure); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("FX%d,%d\x03" % (pressure, toolholder)); + else: + s.write("FX%d\x03" % pressure); # s.write("FX%d,0\x03" % pressure); # oops, graphtecprint does it like this print("pressure: %d" % pressure, file=s.log) + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if pen: + s.write("FC0,1,%d\x03" % (toolholder)) + if s.leftaligned: print("Loaded media is expected left-aligned.", file=s.log) else: print("Loaded media is expected right-aligned.", file=s.log) + # Lift plotter head at sharp corners + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if sharpencorners: + s.write("FE1,%d\x03" % toolholder) + else: + s.write("FE0,%d\x03" % toolholder) + + if pen: + s.write("FF0,0,%d\x03" % (toolholder)) + else: + sharpencorners_start = int((sharpencorners_start + 0.05) * 10.0) + sharpencorners_end = int((sharpencorners_end + 0.05) * 10.0) + s.write("FF%d,0,%d\x03FF%d,%d,%d\x03" % (sharpencorners_start, toolholder, sharpencorners_start, sharpencorners_end, toolholder)) + # robocut/Plotter.cpp:393 says: # It is 0 for the pen, 18 for cutting. Default diameter of a blade is 0.9mm # C possible stands for curvature. Not that any of the other letters make sense... # C possible stands for circle. # This value is the circle diameter which is exectuted on direction changes on corners to adjust the blade. # Seems to be limited to 46 or 47. Values above does keep the last setting on the device. - if pen: - circle = 0 + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if not pen: + circle = 0.5 + bladediameter * 20 + s.write("FC0,1,%d\x03FC%d,1,%d\x03" % (toolholder, circle, toolholder)) + + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if autoblade and depth is not None: + if toolholder == 1: + if depth < 0: depth = 0 + if depth > 10: depth = 10 + s.write("TF%d,%d\x03" % (depth, toolholder)); + print("depth: %d" % depth, file=s.log) else: - circle = bladediameter * 20 - s.write("FC%d\x03" % circle) + if pen: + circle = 0 + else: + circle = bladediameter * 20 + s.write("FC%d\x03" % circle) + + s.enable_sw_clipping = sw_clipping # if enabled, rollers three times forward and back. # needs a pressure of 19 or more, else nothing will happen @@ -598,19 +692,25 @@ def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=Fals if trackenhancing: s.write("FY0\x03") else: - s.write("FY1\x03") + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + s.write("FY1\x03") #FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other # side of media (boundary check?), but next cut run will stall #TB50,x: x = 1 landscape mode, x = 0 portrait mode - if landscape is not None: - if landscape: - s.write("FN0\x03TB50,1\x03") - else: - s.write("FN0\x03TB50,0\x03") + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + if landscape is not None: + if landscape: + s.write("FN0\x03TB50,1\x03") + else: + s.write("FN0\x03TB50,0\x03") - # Don't lift plotter head between paths - s.write("FE0,0\x03") + # Don't lift plotter head between paths + s.write("FE0,0\x03") def find_bbox(s, cut): """Find the bounding box of the cut, returns (xmin,ymin,xmax,ymax)""" @@ -743,7 +843,7 @@ def plot_cmds(s, plist, bbox, x_off_mm, y_off_mm): bbox['clip']['count'] = 1 if bbox['only'] is False: - if inside and last_inside: + if not s.enable_sw_clipping or (inside and last_inside): plotcmds.append("D%d,%d" % (int(0.5+y), int(0.5+x))) else: # // if outside the range just move @@ -877,8 +977,11 @@ def plot(s, mediawidth=210.0, mediaheight=297.0, margintop=None, #p = "FU%d,%d\x03" % (height,width) # optional #s.write(p) - p = "\\0,0\x03Z%d,%d\x03L0\x03FE0,0\x03FF0,0,0\x03" % (height, width) - s.write(p) + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + p = "\\0,0\x03Z%d,%d\x03L0\x03FE0,0\x03FF0,0,0\x03" % (height, width) + s.write(p) bbox['clip'] = {'urx':width, 'ury':top, 'llx':left, 'lly':height} bbox['only'] = bboxonly @@ -903,7 +1006,10 @@ def plot(s, mediawidth=210.0, mediaheight=297.0, margintop=None, if not 'urx' in bbox: bbox['urx'] = 0 if not 'ury' in bbox: bbox['ury'] = 0 if endposition == 'start': - new_home = "H\x03" + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + new_home = "L0\x03\\0,0\x03M0,0\x03J0\x03FN0\x03TB50,0\x03" + else: + new_home = "H\x03" else: #includes 'below' new_home = "M%d,%d\x03SO0\x03" % (int(0.5+bbox['lly']+end_paper_offset*20.), 0) #! axis swapped when using Cameo-system #new_home += "FN0\x03TB50,0\x03" diff --git a/silhouette/beutil.py b/silhouette/beutil.py new file mode 100644 index 00000000..8e58a3ca --- /dev/null +++ b/silhouette/beutil.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# Conversion utilities for printer's binary encoding commands +# (c) 2018 D. Bajar + +from __future__ import print_function +import sys + +def to_BE(x, y): + if abs(x) < 112 and abs(y) < 112: + index = 224 * (x + 112) + (y + 112) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be1 = "%02X%02X" % (d1 + 0x20, d2 + 0x20) + # print("BE1:", be1) + return ("BE1", be1) + elif abs(x) < 1676 and abs(y) < 1676: + index = 3352 * (x + 1676) + (y + 1676) + d3 = index // (224 * 224) + index -= d3 * (224 * 224) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be2 = "%02X%02X%02X" % (d1 + 0x20, d2 + 0x20, d3 + 0x20) + # print("BE2:", be2) + return ("BE2", be2) + elif abs(x) < 375482 and abs(y) < 375482: + index = 750964 * (x + 375482) + (y + 375482) + d5 = index // (224 * 224 * 224 * 224) + index -= d5 * (224 * 224 * 224 * 224) + d4 = index // (224 * 224 * 224) + index -= d4 * (224 * 224 * 224) + d3 = index // (224 * 224) + index -= d3 * (224 * 224) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be3 = "%02X%02X%02X%02X%02X" % (d1 + 0x20, d2 + 0x20, d3 + 0x20, d4 + 0x20, d5 + 0x20) + # print("BE3:", be3) + return ("BE3", be3) + else: + raise ValueError("Invalid coordinate") + # end if +# end def to_BE + +def from_BE(be_stream): + + if len(be_stream) == 4: + d1 = int(be_stream[:2], 16) + d2 = int(be_stream[2:], 16) + if d1 < 0x20 or d2 < 0x20: + raise ValueError("Invalid BE1 stream digit") + # end if + index = (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 224 - 112 + y = index % 224 - 112 + # print("BE1: %d,%d" % (x, y)) + return ("BE1", (x, y)) + elif len(be_stream) == 6: + d1 = int(be_stream[0:2], 16) + d2 = int(be_stream[2:4], 16) + d3 = int(be_stream[4:6], 16) + if d1 < 0x20 or d2 < 0x20 or d3 < 0x20: + raise ValueError("Invalid BE2 stream digit") + # end if + index = (d3 - 0x20) * (224 * 224) + (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 3352 - 1676 + y = index % 3352 - 1676 + # print("BE2: %d,%d" % (x, y)) + return ("BE2", (x, y)) + elif len(be_stream) == 10: + d1 = int(be_stream[0:2], 16) + d2 = int(be_stream[2:4], 16) + d3 = int(be_stream[4:6], 16) + d4 = int(be_stream[6:8], 16) + d5 = int(be_stream[8:10], 16) + if d1 < 0x20 or d2 < 0x20 or d3 < 0x20 or d4 < 0x20 or d5 < 0x20: + raise ValueError("Invalid BE3 stream digit") + # end if + index = (d5 - 0x20) * (224 * 224 * 224 * 224) + (d4 - 0x20) * (224 * 224 * 224) + (d3 - 0x20) * (224 * 224) + (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 750964 - 375482 + y = index % 750964 - 375482 + # print("BE3: %d,%d" % (x, y)) + return ("BE3", (x, y)) + else: + raise ValueError("Invalid length hex stream") + # end if + +# end def from_BE + +def test_BE(x, y, be_stream, be_enc): + + enc, stream = to_BE(x, y) + passed = True if enc == be_enc and stream == be_stream else False + print("to_BE: (%d, %d) -> '%s' %s= '%s' : %s" % (x, y, stream, '=' if passed else '!', be_stream, "PASSED" if passed else "FAILED")) + if not passed: + sys.exit(-1) + # end if + + enc, xy = from_BE(be_stream) + passed = True if enc == be_enc and xy == (x, y) else False + print("from_BE: '%s -> (%d, %d) %s= (%d, %d) : %s" % (be_stream, xy[0], xy[1], '=' if passed else '!', x, y, "PASSED" if passed else "FAILED")) + if not passed: + sys.exit(-1) + # end if + +# end def test_BE + +def test(): + + print("Running tests ...") + + test_BE( 0, 0, "9090", "BE1") + test_BE( 0, 1, "9190", "BE1") + test_BE( 1, 0, "9091", "BE1") + test_BE( 0, -1, "8F90", "BE1") + test_BE( -1, 0, "908F", "BE1") + test_BE( 1, 1, "9191", "BE1") + test_BE( 1, -1, "8F91", "BE1") + test_BE( -1, -1, "8F8F", "BE1") + test_BE( -1, 1, "918F", "BE1") + test_BE( 0, 111, "FF90", "BE1") + test_BE( 111, 0, "90FF", "BE1") + test_BE( 0, -111, "2190", "BE1") + test_BE(-111, 0, "9021", "BE1") + test_BE( 111, 111, "FFFF", "BE1") + test_BE( 111, -111, "21FF", "BE1") + test_BE(-111, -111, "2121", "BE1") + test_BE(-111, 111, "FF21", "BE1") + test_BE( 56, -27, "75C8", "BE1") + test_BE( -77, 44, "BC43", "BE1") + test_BE( -39, 106, "FA69", "BE1") + test_BE( 72, -25, "77D8", "BE1") + + test_BE( 0, 112, "3C2090", "BE2") + test_BE( 112, 0, "AC8B97", "BE2") + test_BE( 0, -112, "3CFF8F", "BE2") + test_BE( -112, 0, "AC9388", "BE2") + test_BE( 112, 112, "3C8C97", "BE2") + test_BE( 112, -112, "3C8B97", "BE2") + test_BE( -112, -112, "3C9388", "BE2") + test_BE( -112, 112, "3C9488", "BE2") + test_BE( 0, 1675, "372790", "BE2") + test_BE( 1675, 0, "D4E8FF", "BE2") + test_BE( 0, -1675, "41F88F", "BE2") + test_BE(-1675, 0, "843620", "BE2") + test_BE( 1675, 1675, "5FF0FF", "BE2") + test_BE( 1675, -1675, "69E1FF", "BE2") + test_BE(-1675, -1675, "F92E20", "BE2") + test_BE(-1675, 1675, "EF3D20", "BE2") + test_BE( 1091, 674, "B6E8D8", "BE2") + test_BE( 116, 1421, "D9CD97", "BE2") + test_BE( -702, 485, "E13861", "BE2") + test_BE(-1463, -1153, "C3552E", "BE2") + + print("All test PASSED !!!") + sys.exit(0) + +# end def test + +def main(argv): + + # test() + + if len(argv) <= 1: + print("Usage: %s | " % (argv[0])) + return 1 + # end if + + if len(argv) <= 2: + be_stream = argv[1] + res = from_BE(be_stream) + print("%s %s -> %d,%d" % (res[0], be_stream, res[1][0], res[1][1])) + else: + x = int(argv[1], 0) + y = int(argv[2], 0) + res = to_BE(x, y) + print("%d,%d -> %s %s" % (x, y, res[0], res[1])) + # end if + + return 0 + +# end def main + +if __name__ == '__main__': + sys.exit(main(sys.argv)) +# end if diff --git a/silhouette_multi.inx b/silhouette_multi.inx new file mode 100644 index 00000000..bd780fa1 --- /dev/null +++ b/silhouette_multi.inx @@ -0,0 +1,21 @@ + + + <_name>Silhouette Multiple Actions + com.github.jnweiger.inskscape-silhouette-multi + org.inkscape.output.svg.inkscape + inkex.py + silhouette_multi.py + + false + + + all + + + + + + + diff --git a/silhouette_multi.py b/silhouette_multi.py new file mode 100644 index 00000000..ab47131d --- /dev/null +++ b/silhouette_multi.py @@ -0,0 +1,802 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +import sys +import time +import cPickle +import subprocess +from threading import Thread +from tempfile import NamedTemporaryFile +from collections import defaultdict, OrderedDict +import xmltodict +import traceback +from cStringIO import StringIO +import wx +from wx.lib.scrolledpanel import ScrolledPanel +from wx.lib.agw import ultimatelistctrl as ulc +from wx.lib.embeddedimage import PyEmbeddedImage +from collections import defaultdict +import inkex +import simplestyle +from functools import partial +from itertools import groupby + + +small_up_arrow = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADxJ" + "REFUOI1jZGRiZqAEMFGke2gY8P/f3/9kGwDTjM8QnAaga8JlCG3CAJdt2MQxDCAUaOjyjKMp" + "cRAYAABS2CPsss3BWQAAAABJRU5ErkJggg==") + +small_down_arrow = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAEhJ" + "REFUOI1jZGRiZqAEMFGke9QABgYGBgYWdIH///7+J6SJkYmZEacLkCUJacZqAD5DsInTLhDR" + "bcPlKrwugGnCFy6Mo3mBAQChDgRlP4RC7wAAAABJRU5ErkJggg==") + + + +def presets_path(): + try: + import appdirs + config_path = appdirs.user_config_dir('inkscape-silhouette') + except ImportError: + config_path = os.path.expanduser('~/.inkscape-silhouette') + + if not os.path.exists(config_path): + os.makedirs(config_path) + return os.path.join(config_path, 'presets.cPickle') + +def load_presets(): + try: + with open(presets_path(), 'r') as presets: + presets = cPickle.load(presets) + return presets + except: + return {} + +def save_presets(presets): + #print "saving presets", presets + with open(presets_path(), 'w') as presets_file: + cPickle.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 = 'Silhouette Multiple Actions'): + 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 = 'Silhouette Multiple Actions'): + dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + +class ParamsNotebook(wx.Notebook): + """Handle a notebook of tabs that contain params. + + Each param has a name, and all names are globally unique across all tabs. + """ + + def __init__(self, *args, **kwargs): + wx.Notebook.__init__(self, *args, **kwargs) + + self.load_inx() + self.create_tabs() + self.add_tabs() + + def load_inx(self): + with open('sendto_silhouette.inx') as inx_file: + self.inx = xmltodict.parse(inx_file, force_list=('param',)) + + def create_tabs(self): + self.notebook = self.inx['inkscape-extension']['param'][0] + + if self.notebook['@type'] != 'notebook': + print >> sys.stderr, "unexpected INX format" + return + + self.tabs = [] + for page in self.notebook['page']: + self.tabs.append(ParamsTab(self, wx.ID_ANY, name=page['@name'], title=page['@_gui-text'], params=page['param'])) + + def add_tabs(self): + for tab in self.tabs: + self.AddPage(tab, tab.title) + + def get_values(self): + values = {} + + for tab in self.tabs: + values.update(tab.get_values()) + + return values + + def get_defaults(self): + values = {} + + for tab in self.tabs: + values.update(tab.get_defaults()) + + return values + + + def set_values(self, values): + for tab in self.tabs: + tab.set_values(values) + + + def set_defaults(self): + for tab in self.tabs: + tab.set_defaults() + +class ParamsTab(ScrolledPanel): + def __init__(self, *args, **kwargs): + self.params = kwargs.pop('params', []) + self.name = kwargs.pop('name', None) + self.title = kwargs.pop('title', None) + kwargs["style"] = wx.TAB_TRAVERSAL + ScrolledPanel.__init__(self, *args, **kwargs) + self.SetupScrolling() + + self.param_inputs = {} + self.choices_by_label = {} + self.choices_by_value = {} + self.defaults = {} + + self.settings_grid = wx.GridBagSizer(hgap=0, vgap=0) + self.settings_grid.AddGrowableCol(0, 1) + self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) + + self.__set_properties() + self.__do_layout() + + def get_values(self): + values = {} + + for name, input in self.param_inputs.iteritems(): + if isinstance(input, wx.Choice): + choice = input.GetSelection() + + if choice == wx.NOT_FOUND: + values[name] = None + else: + values[name] = self.choices_by_label[name][input.GetString(choice)] + else: + values[name] = input.GetValue() + return values + + def get_defaults(self): + return self.defaults + + def set_values(self, values): + for name, value in values.iteritems(): + if name not in self.param_inputs: + # ignore params not contained in this tab + continue + + input = self.param_inputs[name] + + if isinstance(input, wx.Choice): + if value is None: + input.SetSelection(wx.NOT_FOUND) + else: + label = self.choices_by_value[name][value] + input.SetStringSelection(label) + else: + input.SetValue(value) + + def set_defaults(self): + self.set_values(self.defaults) + + 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) + + for row, param in enumerate(self.params): + param_type = param['@type'] + param_name = param['@name'] + if param_type == 'description': + self.settings_grid.Add(wx.StaticText(self, label=param.get('#text', '')), + pos=(row, 0), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.ALIGN_TOP, border=10) + else: + self.settings_grid.Add(wx.StaticText(self, label=param.get('@_gui-text', '')), + pos=(row, 0), flag=wx.EXPAND|wx.TOP|wx.ALIGN_TOP, border=5) + + if param_type == 'boolean': + input = wx.CheckBox(self) + elif param_type == 'float': + input = wx.SpinCtrlDouble(self, wx.ID_ANY, min=float(param.get('@min', 0.0)), max=float(param.get('@max', 2.0**32)), inc=0.1, value=param.get('#text', '')) + elif param_type == 'int': + input = wx.SpinCtrl(self, wx.ID_ANY, min=int(param.get('@min', 0)), max=int(param.get('@max', 2**32)), value=param.get('#text', '')) + elif param_type == 'enum': + choices = OrderedDict((item['#text'], item['@value']) for item in param['item']) + self.choices_by_label[param_name] = choices + self.choices_by_value[param_name] = { v: k for k, v in choices.iteritems() } + input = wx.Choice(self, wx.ID_ANY, choices=choices.keys(), style=wx.LB_SINGLE) + input.SetStringSelection(choices.keys()[0]) + else: + # not sure what else to do here... + continue + + self.param_inputs[param_name] = input + + self.settings_grid.Add(input, pos=(row, 1), flag=wx.ALIGN_BOTTOM|wx.TOP, border=5) + + self.defaults = self.get_values() + + box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) + self.SetSizer(box) + + self.Layout() + + +class SilhouetteMultiFrame(wx.Frame): + def __init__(self, *args, **kwargs): + # begin wxGlade: MyFrame.__init__ + self.colors = kwargs.pop('colors', []) + self.options = kwargs.pop('options') + self.run_callback = kwargs.pop('run_callback') + wx.Frame.__init__(self, None, wx.ID_ANY, + "Silhouette Multi-Action" + ) + + self.selected = None + self.color_settings = {} + self.color_enabled = {} + self.notebook = ParamsNotebook(self, wx.ID_ANY) + self.up_button = wx.Button(self, wx.ID_UP) + self.down_button = wx.Button(self, wx.ID_DOWN) + self.run_button = wx.Button(self, wx.ID_EXECUTE) + self.cancel_button = wx.Button(self, wx.ID_CANCEL, "Cancel") + + 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.load_preset_button = wx.Button(self, wx.ID_REVERT_TO_SAVED, "Load") + self.add_preset_button = wx.Button(self, wx.ID_SAVE, "Add") + self.overwrite_preset_button = wx.Button(self, wx.ID_SAVEAS, "Overwrite") + self.delete_preset_button = wx.Button(self, wx.ID_DELETE, "Delete") + + self.update_preset_list() + self.init_actions() + + self.Bind(wx.EVT_BUTTON, self.move_up, self.up_button) + self.Bind(wx.EVT_BUTTON, self.move_down, self.down_button) + self.Bind(wx.EVT_BUTTON, self.run, self.run_button) + self.Bind(wx.EVT_BUTTON, self.load_preset, self.load_preset_button) + self.Bind(wx.EVT_BUTTON, self.add_preset, self.add_preset_button) + self.Bind(wx.EVT_BUTTON, self.overwrite_preset, self.overwrite_preset_button) + self.Bind(wx.EVT_BUTTON, self.delete_preset, self.delete_preset_button) + self.Bind(wx.EVT_BUTTON, self.close, self.cancel_button) + + self._load_preset('__LAST__', silent=True) + + self.__set_properties() + self.__do_layout() + # end wxGlade + + def close(self, event): + self.Close() + + def load_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + self._load_preset(preset_name) + + 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') + + self.save_color_settings() + 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 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 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_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 update_preset_list(self): + preset_names = load_presets().keys() + preset_names = [preset for preset in preset_names if not preset.startswith("__")] + self.preset_chooser.SetItems(preset_names) + + def _load_preset(self, preset_name, silent=False): + preset = load_preset(preset_name) + + #print >> sys.stderr, preset + + if not preset: + return + + if self.selected: + self.actions.Select(self.selected, False) + + old_colors = self.colors + self.colors = [] + extra_colors = [] + + for color in preset['colors']: + if color in old_colors: + old_colors.remove(color) + self.colors.append(color) + self.color_enabled[color] = preset['color_enabled'].get(color, True) + self.color_settings[color] = preset['color_settings'].get(color, {}) + else: + extra_colors.append(color) + + reassigned = 0 + # If there are any leftover colors in this SVG that weren't in the + # preset, we have to add them back into the list. Let's try to + # use the settings from one of the "unclaimed" colors in the preset. + + for color in old_colors: + self.colors.append(color) + + if extra_colors: + reassigned += 1 + assigned_color = extra_colors.pop(0) + self.color_enabled[color] = preset['color_enabled'].get(assigned_color, True) + self.color_settings[color] = preset['color_settings'].get(assigned_color, {}) + else: + self.color_enabled[color] = False + + message = [] + + #print >> sys.stderr, reassigned, extra_colors + #print >> sys.stderr, self.colors + #print >> sys.stderr, self.color_settings + + if reassigned: + message.append("%d colors were reassigned." % reassigned) + + if extra_colors: + message.append("%d colors from the preset were not used." % len(extra_colors)) + + if message and not silent: + info_dialog(self, "Colors in the preset and this SVG did not match fully. " + " ".join(message)) + + self.refresh_actions() + + def _save_preset(self, preset_name): + self.save_color_settings() + + preset = self.get_preset_data() + save_preset(preset_name, preset) + + def get_preset_data(self): + return { 'colors': self.colors, + 'color_enabled': self.color_enabled, + 'color_settings': self.color_settings } + + def run(self, event): + self.save_color_settings() + + actions = [] + + for color in self.colors: + if self.color_enabled.get(color, True): + actions.append((color, self.color_settings.get(color) or self.notebook.get_defaults())) + + if actions: + if not self.options.dry_run: + if not confirm_dialog(self, "About to perform %d actions, continue?" % len(actions)): + return + else: + info_dialog(self, "No colors were enabled, so no actions can be performed.") + return + + self._save_preset('__LAST__') + self.run_callback(actions) + + def move_up(self, event): + if self.selected is None or self.selected == 0: + return + + this = self.selected + prev = this - 1 + + self.colors[this], self.colors[prev] = self.colors[prev], self.colors[this] + self.actions.Select(this, False) + self.actions.Select(prev) + + self.refresh_actions() + + def move_down(self, event): + if self.selected is None or self.selected == len(self.colors) - 1: + return + + this = self.selected + next = this + 1 + + self.colors[this], self.colors[next] = self.colors[next], self.colors[this] + self.actions.Select(this, False) + self.actions.Select(next) + + self.refresh_actions() + + def action_selected(self, event=None): + # first, save the settings for the color they were previously working on + self.save_color_settings() + + # then load the settings for the newly-selected color + self.selected = event.m_itemIndex + self.load_color_settings() + + self.up_button.Enable() + self.down_button.Enable() + self.notebook.Enable() + + def action_deselected(self, event=None): + self.save_color_settings() + + self.selected = None + + self.up_button.Disable() + self.down_button.Disable() + self.notebook.Disable() + + def load_color_settings(self): + color = self.colors[self.selected] + settings = self.color_settings.get(color) + + if settings: + self.notebook.set_values(settings) + else: + self.notebook.set_defaults() + + def save_color_settings(self): + #print "save:", self.selected + + if self.selected is None: + return + + color = self.colors[self.selected] + settings = self.notebook.get_values() + self.color_settings[color] = settings + + #print "settings:", settings + + def item_checked(self, event): + item = event.m_itemIndex + checked = self.actions.IsItemChecked(item, 2) + self.color_enabled[self.colors[item]] = checked + + def init_actions(self): + self.actions = ulc.UltimateListCtrl(self, size=(300, 150), agwStyle=wx.LC_REPORT|ulc.ULC_HRULES|ulc.ULC_SINGLE_SEL) + + self.Bind(ulc.EVT_LIST_ITEM_SELECTED, self.action_selected, self.actions) + self.Bind(ulc.EVT_LIST_ITEM_DESELECTED, self.action_deselected, self.actions) + self.Bind(ulc.EVT_LIST_ITEM_CHECKED, self.item_checked, self.actions) + self.action_deselected() + + self.actions.InsertColumn(0, "Step") + self.actions.InsertColumn(1, "Color") + self.actions.InsertColumn(2, "Perform Action?") + self.actions.SetColumnWidth(2, ulc.ULC_AUTOSIZE_FILL) + + self.action_checkboxes = [] + + for i, color in enumerate(self.colors): + self.actions.InsertStringItem(i, "%d." % (i + 1)) + + item = self.actions.GetItem(i, 2) + item.SetKind(1) # "a checkbox-like item" + item.SetMask(ulc.ULC_MASK_KIND) + self.actions.SetItem(item) + + self.refresh_actions() + + def refresh_actions(self): + for i, color in enumerate(self.colors): + item = self.actions.GetItem(i, 1) + item.SetMask(ulc.ULC_MASK_BACKCOLOUR) + item.SetBackgroundColour(wx.Colour(*color)) + self.actions.SetItem(item) + + item = self.actions.GetItem(i, 2) + item.Check(self.color_enabled.get(color, True)) + item.SetMask(ulc.ULC_MASK_CHECK) + self.actions.SetItem(item) + + + def __set_properties(self): + # begin wxGlade: MyFrame.__set_properties + self.SetTitle("Silhouette Multi-Action") + self.notebook.SetMinSize((800, 800)) + # end wxGlade + + def __do_layout(self): + # begin wxGlade: MyFrame.__do_layout + sizer_1 = wx.BoxSizer(wx.VERTICAL) + + sizer_2 = wx.BoxSizer(wx.HORIZONTAL) + sizer_2.Add(self.actions, 0, flag=wx.ALL|wx.EXPAND, border=10) + + sizer_3 = wx.BoxSizer(wx.VERTICAL) + sizer_3.Add(self.up_button, 0, border=10) + sizer_3.Add(self.down_button, 0, border=10) + + sizer_2.Add(sizer_3, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=10) + + sizer_4 = wx.StaticBoxSizer(self.presets_box, wx.VERTICAL) + sizer_4.Add(self.preset_chooser, 0, flag=wx.BOTTOM|wx.LEFT|wx.RIGHT|wx.TOP|wx.EXPAND, border=10) + + sizer_5 = wx.BoxSizer(wx.HORIZONTAL) + sizer_5.Add(self.load_preset_button, 0, flag=wx.RIGHT|wx.LEFT|wx.BOTTOM, border=10) + sizer_5.Add(self.add_preset_button, 0, flag=wx.RIGHT, border=10) + sizer_5.Add(self.overwrite_preset_button, 0, flag=wx.RIGHT, border=10) + sizer_5.Add(self.delete_preset_button, 0, flag=wx.RIGHT, border=10) + + sizer_4.Add(sizer_5, 0) + sizer_2.Add(sizer_4, 0, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=30) + + sizer_6 = wx.BoxSizer(wx.VERTICAL) + sizer_6.Add(self.run_button, 0, flag=wx.ALIGN_RIGHT|wx.BOTTOM, border=10) + sizer_6.Add(self.cancel_button, 0, flag=wx.ALIGN_RIGHT|wx.EXPAND) + sizer_2.Add(sizer_6, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=30) + + sizer_1.Add(sizer_2, 0, flag=wx.EXPAND|wx.ALL, border=10) + + sizer_1.Add(self.notebook, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, 10) + self.SetSizer(sizer_1) + sizer_1.Fit(self) + self.Layout() + # end wxGlade + +class SilhouetteMulti(inkex.Effect): + def __init__(self, *args, **kwargs): + inkex.Effect.__init__(self, *args, **kwargs) + + self.OptionParser.add_option("-d", "--dry_run", type='inkbool', default=False) + + def get_style(self, element): + if element.get('style') is not None: + return simplestyle.parseStyle(element.get('style')) + else: + return {} + + return style + + def get_color(self, element): + if element.tag == inkex.addNS( 'g', 'svg'): + # Sometimes Inkscape sets a stroke style on a group, which seems to + # have no visual effect. If we didn't ignore those, we'd cut those + # objects twice. + return None + + color = self.get_style(element).get('stroke', 'none') + + if color == 'none': + color = self.get_style(element).get('fill', 'none') + + if color != 'none': + return simplestyle.parseColor(color) + else: + return None + + def load_selected_objects(self): + self.selected_objects = [] + + def traverse_element(element, selected=False, parent_visibility="visible"): + if self.get_style(element).get('display') == 'none': + return + + visibility = element.get('visibility', parent_visibility) + + if visibility == 'inherit': + visibility = parent_visibility + + if element.get('id') in self.selected: + selected = True + + if selected and visibility not in ('hidden', 'collapse'): + self.selected_objects.append(element) + + for child in element: + traverse_element(child, selected, visibility) + + # if they haven't selected specific objects, then process all objects + if self.selected: + select_all = False + else: + select_all = True + + traverse_element(self.document.getroot(), selected=select_all) + + def split_objects_by_color(self): + self.objects_by_color = defaultdict(list) + self.load_selected_objects() + + for obj in self.selected_objects: + color = self.get_color(obj) + if color: + self.objects_by_color[color].append(obj) + + def effect(self): + app = wx.App() + self.split_objects_by_color() + self.frame = SilhouetteMultiFrame(colors=self.objects_by_color.keys(), run_callback=self.run, options=self.options) + self.frame.Show() + app.MainLoop() + + def save_copy(self): + self.svg_file = NamedTemporaryFile(suffix='.svg', prefix='silhouette-multiple-actions') + self.document.write(self.svg_file) + self.svg_file.flush() + + def format_args(self, args): + if isinstance(args, dict): + args = args.iteritems() + + return " ".join(("--%s=%s" % (k, v) for k, v in args)) + + def id_args(self, nodes): + return self.format_args(("id", node.get("id")) for node in nodes) + + def format_commands(self, actions): + commands = [] + + for color, settings in actions: + command = "python sendto_silhouette.py" + command += " " + self.format_args(settings) + command += " " + self.id_args(self.objects_by_color[color]) + command += " " + self.svg_file.name + + commands.append(command) + + return commands + + def run(self, actions): + self.frame.Close() + restore_stderr() + + self.save_copy() + + commands = self.format_commands(actions) + + if self.options.dry_run: + print >> sys.stderr, "\n\n".join(commands) + else: + self.run_commands_with_dialog(commands) + + def run_commands_with_dialog(self, commands): + for i, command in enumerate(commands): + if not self.run_command_with_dialog(command, step=i + 1, total=len(commands)): + info_dialog(None, "Action failed.") + print >> sys.stderr, "The command that failed:" + print >> sys.stderr, command + + sys.exit(1) + + def run_command_with_dialog(self, command, step, total): + # exec ensures that the shell gets replaced so that we can terminate the + # actual python script if the user cancels + process = subprocess.Popen("exec " + command, shell=True) + + dialog = wx.ProgressDialog(style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_ELAPSED_TIME, + message="Performing action %d of %d..." % (step, total), + title="Silhouette Multiple Actions") + + last_tick = time.time() + + while process.returncode is None: + if time.time() - last_tick > 0.5: + dialog.Pulse() + last_tick = time.time() + + process.poll() + wx.Yield() + time.sleep(0.1) + + if dialog.WasCancelled(): + def cancel(): + process.terminate() + process.wait() + + Thread(target=cancel).start() + + dialog.Destroy() + wx.Yield() + info_dialog(None, "Action aborted. It may take awhile for the machine to cancel its operation.") + sys.exit(1) + + dialog.Destroy() + wx.Yield() + return process.returncode == 0 + +def save_stderr(): + # GTK likes to spam stderr, which inkscape will show in a dialog. + null = open('/dev/null', 'w') + sys.stderr_dup = os.dup(sys.stderr.fileno()) + os.dup2(null.fileno(), 2) + sys.stderr_backup = sys.stderr + sys.stderr = StringIO() + + +def restore_stderr(): + if hasattr(sys, "stderr_backup"): + os.dup2(sys.stderr_dup, 2) + sys.stderr_backup.write(sys.stderr.getvalue()) + sys.stderr = sys.stderr_backup + + del sys.stderr_backup + + +# end of class MyFrame +if __name__ == "__main__": + + pid = os.fork() + if pid == 0: + # Forking and closing stdout and stderr allows inkscape to continue on + # while the silhouette machine is cutting. This is useful if you're + # cutting something really big and want to work on another document. + os.close(1) + os.close(2) + + try: + e = SilhouetteMulti() + e.affect() + except: + traceback.print_exc() + + + sys.exit(0)