├── LICENSE ├── README.md └── src ├── .gitignore ├── unicorn.inx ├── unicorn.py └── unicorn ├── __init__.py ├── context.py ├── entities.py └── svg_parser.py /LICENSE: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 MakerBot Industries 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MakerBot Unicorn G-Code Output for Inkscape 2 | =========================================== 3 | 4 | Notice 5 | ------ 6 | 7 | **This extension is no longer supported or maintained. The last tested version of Inkscape is 0.48.5.** 8 | 9 | **Please feel free to fork and improve this extension for your own needs!** 10 | 11 | This is an Inkscape extension that allows you to save your Inkscape drawings as 12 | G-Code files suitable for plotting with the [MakerBot Unicorn Pen Plotter](http://store.makerbot.com/makerbot-unicorn-pen-plotter-kit.html). 13 | 14 | **Users who use this extension to generate G-Code for a machine other than a MakerBot CupCake CNC with a Unicorn Pen Plotter attachment do so at their own risk.** 15 | 16 | Author: [Marty McGuire](http://github.com/martymcguire) 17 | 18 | Website: [http://github.com/martymcguire/inkscape-unicorn](http://github.com/martymcguire/inkscape-unicorn) 19 | 20 | Credits 21 | ======= 22 | 23 | * Marty McGuire pulled this all together into an Inkscape extension. 24 | * [Inkscape](http://www.inkscape.org/) is an awesome open source vector graphics app. 25 | * [Scribbles](https://github.com/makerbot/Makerbot/tree/master/Unicorn/Scribbles%20Scripts) is the original DXF-to-Unicorn Python script. 26 | * [The Egg-Bot Driver for Inkscape](http://code.google.com/p/eggbotcode/) provided inspiration and good examples for working with Inkscape's extensions API. 27 | 28 | Install 29 | ======= 30 | 31 | Copy the contents of `src/` to your Inkscape `extensions/` folder. 32 | 33 | Typical locations include: 34 | 35 | * OS X - `/Applications/Inkscape.app/Contents/Resources/extensions` 36 | * Linux - `/usr/share/inkscape/extensions` 37 | * Windows - `C:\Program Files\Inkscape\share\extensions` 38 | 39 | Usage 40 | ===== 41 | 42 | * Size and locate your image appropriately: 43 | * The CupCake CNC build platform size is 100mm x 100mm. 44 | * Setting units to **mm** in Inkscape makes it easy to size your drawing. 45 | * The extension will automatically attempt to center everything. 46 | * Convert all text to paths: 47 | * Select all text objects. 48 | * Choose **Path | Object to Path**. 49 | * Save as G-Code: 50 | * **File | Save a Copy**. 51 | * Select **MakerBot Unicorn G-Code (\*.gcode)**. 52 | * Save your file. 53 | * Preview 54 | * For OS X, [Pleasant3D](http://www.pleasantsoftware.com/developer/pleasant3d/index.shtml) is great for this. 55 | * For other operating systems... I don't know! 56 | * Print! 57 | * Open your `.gcode` file in [ReplicatorG](http://replicat.org/) 58 | * Set up your Unicorn and pen. 59 | * Center your build platform. 60 | * Click the **Build** button! 61 | 62 | TODOs 63 | ===== 64 | 65 | * Rename `*PolyLine` stuff to `*Path` to be less misleading. 66 | * Formalize "home" to be a reasonable place to change pages/pens. 67 | * Parameterize smoothness for curve approximation. 68 | * Use native curve G-Codes instead of converting to paths? 69 | * Include example templates? 70 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /src/unicorn.inx: -------------------------------------------------------------------------------- 1 | 2 | 3 | <_name>MakerBot Unicorn G-Code Output 4 | com.makerbot.unicorn.gcode 5 | org.inkscape.output.svg.inkscape 6 | unicorn.py 7 | inkex.py 8 | 9 | 10 | 50 11 | 30 12 | 150.0 13 | 150.0 14 | 3500.0 15 | 150.0 16 | 0.0 17 | 0.0 18 | 19 | 20 | <_param name="reg_help" type="description" xml:space="preserve">Pen Registration Check 21 | 22 | This feature adds a quick pen-down/pen-up at the beginning of a plot so you can check whether the pen hits the paper. 23 | 24 | When plotting, ReplicatorG will pause to ask if registration was successful. If you say "No", it will simply abort the plot so you can restart. 25 | true 26 | 27 | 28 | <_param name="homing_help" type="description" xml:space="preserve">Where do you like to set your platform when you start a plot? 29 | 30 | Some examples: 31 | - X = 0, Y = 0 if it starts centered under the pen. 32 | - X = 50, Y = 50 if it starts in the front-left corner of a Cupcake CNC. 33 | 34 | Note: Double-check the orientation of your axes when changing these values! 35 | 0.00 36 | 0.00 37 | 38 | 39 | <_param name="copies_help" type="description" xml:space="preserve">Add page-changing prompts so you can plot multiple copies! 40 | 1 41 | false 42 | 43 | 44 | false 45 | 46 | 47 | <_param name="ext_help" type="description" xml:space="preserve">MakerBot Unicorn G-Code Output. 48 | 49 | - All text must be converted to paths. 50 | - Curves are approximated with line segments. 51 | 52 | More Info: http://github.com/martymcguire/inkscape-unicorn/ 53 | 54 | 55 | 56 | 57 | .gcode 58 | application/x-gcode 59 | <_filetypename>MakerBot Unicorn G-Code (*.gcode) 60 | <_filetypetooltip>Toolpath for the MakerBot Unicorn Pen Plotter 61 | true 62 | 63 | 66 | 67 | -------------------------------------------------------------------------------- /src/unicorn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Copyright (c) 2010 MakerBot Industries 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | ''' 19 | import sys,os 20 | import inkex 21 | from math import * 22 | import getopt 23 | from unicorn.context import GCodeContext 24 | from unicorn.svg_parser import SvgParser 25 | 26 | class MyEffect(inkex.Effect): 27 | def __init__(self): 28 | inkex.Effect.__init__(self) 29 | self.OptionParser.add_option("--pen-up-angle", 30 | action="store", type="float", 31 | dest="pen_up_angle", default="50.0", 32 | help="Pen Up Angle") 33 | self.OptionParser.add_option("--pen-down-angle", 34 | action="store", type="float", 35 | dest="pen_down_angle", default="30.0", 36 | help="Pen Down Angle") 37 | self.OptionParser.add_option("--start-delay", 38 | action="store", type="float", 39 | dest="start_delay", default="150.0", 40 | help="Delay after pen down command before movement in milliseconds") 41 | self.OptionParser.add_option("--stop-delay", 42 | action="store", type="float", 43 | dest="stop_delay", default="150.0", 44 | help="Delay after pen up command before movement in milliseconds") 45 | self.OptionParser.add_option("--xy-feedrate", 46 | action="store", type="float", 47 | dest="xy_feedrate", default="3500.0", 48 | help="XY axes feedrate in mm/min") 49 | self.OptionParser.add_option("--z-feedrate", 50 | action="store", type="float", 51 | dest="z_feedrate", default="150.0", 52 | help="Z axis feedrate in mm/min") 53 | self.OptionParser.add_option("--z-height", 54 | action="store", type="float", 55 | dest="z_height", default="0.0", 56 | help="Z axis print height in mm") 57 | self.OptionParser.add_option("--finished-height", 58 | action="store", type="float", 59 | dest="finished_height", default="0.0", 60 | help="Z axis height after printing in mm") 61 | self.OptionParser.add_option("--register-pen", 62 | action="store", type="string", 63 | dest="register_pen", default="true", 64 | help="Add pen registration check(s)") 65 | self.OptionParser.add_option("--x-home", 66 | action="store", type="float", 67 | dest="x_home", default="0.0", 68 | help="Starting X position") 69 | self.OptionParser.add_option("--y-home", 70 | action="store", type="float", 71 | dest="y_home", default="0.0", 72 | help="Starting Y position") 73 | self.OptionParser.add_option("--num-copies", 74 | action="store", type="int", 75 | dest="num_copies", default="1") 76 | self.OptionParser.add_option("--continuous", 77 | action="store", type="string", 78 | dest="continuous", default="false", 79 | help="Plot continuously until stopped.") 80 | self.OptionParser.add_option("--pause-on-layer-change", 81 | action="store", type="string", 82 | dest="pause_on_layer_change", default="false", 83 | help="Pause on layer changes.") 84 | self.OptionParser.add_option("--tab", 85 | action="store", type="string", 86 | dest="tab") 87 | 88 | def output(self): 89 | self.context.generate() 90 | 91 | def effect(self): 92 | self.context = GCodeContext(self.options.xy_feedrate, self.options.z_feedrate, 93 | self.options.start_delay, self.options.stop_delay, 94 | self.options.pen_up_angle, self.options.pen_down_angle, 95 | self.options.z_height, self.options.finished_height, 96 | self.options.x_home, self.options.y_home, 97 | self.options.register_pen, 98 | self.options.num_copies, 99 | self.options.continuous, 100 | self.svg_file) 101 | parser = SvgParser(self.document.getroot(), self.options.pause_on_layer_change) 102 | parser.parse() 103 | for entity in parser.entities: 104 | entity.get_gcode(self.context) 105 | 106 | if __name__ == '__main__': #pragma: no cover 107 | e = MyEffect() 108 | e.affect() 109 | -------------------------------------------------------------------------------- /src/unicorn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martymcguire/inkscape-unicorn/5bf0f6691c08b8e3b07ecace2567413941cd8957/src/unicorn/__init__.py -------------------------------------------------------------------------------- /src/unicorn/context.py: -------------------------------------------------------------------------------- 1 | from math import * 2 | import sys 3 | 4 | class GCodeContext: 5 | def __init__(self, xy_feedrate, z_feedrate, start_delay, stop_delay, pen_up_angle, pen_down_angle, z_height, finished_height, x_home, y_home, register_pen, num_pages, continuous, file): 6 | self.xy_feedrate = xy_feedrate 7 | self.z_feedrate = z_feedrate 8 | self.start_delay = start_delay 9 | self.stop_delay = stop_delay 10 | self.pen_up_angle = pen_up_angle 11 | self.pen_down_angle = pen_down_angle 12 | self.z_height = z_height 13 | self.finished_height = finished_height 14 | self.x_home = x_home 15 | self.y_home = y_home 16 | self.register_pen = register_pen 17 | self.num_pages = num_pages 18 | self.continuous = continuous 19 | self.file = file 20 | 21 | self.drawing = False 22 | self.last = None 23 | 24 | self.preamble = [ 25 | "(Scribbled version of %s @ %.2f)" % (self.file, self.xy_feedrate), 26 | "( %s )" % " ".join(sys.argv), 27 | "G21 (metric ftw)", 28 | "G90 (absolute mode)", 29 | "G92 X%.2f Y%.2f Z%.2f (you are here)" % (self.x_home, self.y_home, self.z_height), 30 | "" 31 | ] 32 | 33 | self.postscript = [ 34 | "", 35 | "(end of print job)", 36 | "M300 S%0.2F (pen up)" % self.pen_up_angle, 37 | "G4 P%d (wait %dms)" % (self.stop_delay, self.stop_delay), 38 | "M300 S255 (turn off servo)", 39 | "G1 X0 Y0 F%0.2F" % self.xy_feedrate, 40 | "G1 Z%0.2F F%0.2F (go up to finished level)" % (self.finished_height, self.z_feedrate), 41 | "G1 X%0.2F Y%0.2F F%0.2F (go home)" % (self.x_home, self.y_home, self.xy_feedrate), 42 | "M18 (drives off)", 43 | ] 44 | 45 | self.registration = [ 46 | "M300 S%d (pen down)" % (self.pen_down_angle), 47 | "G4 P%d (wait %dms)" % (self.start_delay, self.start_delay), 48 | "M300 S%d (pen up)" % (self.pen_up_angle), 49 | "G4 P%d (wait %dms)" % (self.stop_delay, self.stop_delay), 50 | "M18 (disengage drives)", 51 | "M01 (Was registration test successful?)", 52 | "M17 (engage drives if YES, and continue)", 53 | "" 54 | ] 55 | 56 | self.sheet_header = [ 57 | "(start of sheet header)", 58 | "G92 X%.2f Y%.2f Z%.2f (you are here)" % (self.x_home, self.y_home, self.z_height), 59 | ] 60 | if self.register_pen == 'true': 61 | self.sheet_header.extend(self.registration) 62 | self.sheet_header.append("(end of sheet header)") 63 | 64 | self.sheet_footer = [ 65 | "(Start of sheet footer.)", 66 | "M300 S%d (pen up)" % (self.pen_up_angle), 67 | "G4 P%d (wait %dms)" % (self.stop_delay, self.stop_delay), 68 | "G91 (relative mode)", 69 | "G0 Z15 F%0.2f" % (self.z_feedrate), 70 | "G90 (absolute mode)", 71 | "G0 X%0.2f Y%0.2f F%0.2f" % (self.x_home, self.y_home, self.xy_feedrate), 72 | "M01 (Have you retrieved the print?)", 73 | "(machine halts until 'okay')", 74 | "G4 P%d (wait %dms)" % (self.start_delay, self.start_delay), 75 | "G91 (relative mode)", 76 | "G0 Z-15 F%0.2f (return to start position of current sheet)" % (self.z_feedrate), 77 | "G0 Z-0.01 F%0.2f (move down one sheet)" % (self.z_feedrate), 78 | "G90 (absolute mode)", 79 | "M18 (disengage drives)", 80 | "(End of sheet footer)", 81 | ] 82 | 83 | self.loop_forever = [ "M30 (Plot again?)" ] 84 | 85 | self.codes = [] 86 | 87 | def generate(self): 88 | if self.continuous == 'true': 89 | self.num_pages = 1 90 | 91 | codesets = [self.preamble] 92 | if (self.continuous == 'true' or self.num_pages > 1): 93 | codesets.append(self.sheet_header) 94 | elif self.register_pen == 'true': 95 | codesets.append(self.registration) 96 | codesets.append(self.codes) 97 | if (self.continuous == 'true' or self.num_pages > 1): 98 | codesets.append(self.sheet_footer) 99 | 100 | if self.continuous == 'true': 101 | codesets.append(self.loop_forever) 102 | for codeset in codesets: 103 | for line in codeset: 104 | print line 105 | else: 106 | for p in range(0,self.num_pages): 107 | for codeset in codesets: 108 | for line in codeset: 109 | print line 110 | for line in self.postscript: 111 | print line 112 | 113 | def start(self): 114 | self.codes.append("M300 S%0.2F (pen down)" % self.pen_down_angle) 115 | self.codes.append("G4 P%d (wait %dms)" % (self.start_delay, self.start_delay)) 116 | self.drawing = True 117 | 118 | def stop(self): 119 | self.codes.append("M300 S%0.2F (pen up)" % self.pen_up_angle) 120 | self.codes.append("G4 P%d (wait %dms)" % (self.stop_delay, self.stop_delay)) 121 | self.drawing = False 122 | 123 | def go_to_point(self, x, y, stop=False): 124 | if self.last == (x,y): 125 | return 126 | if stop: 127 | return 128 | else: 129 | if self.drawing: 130 | self.codes.append("M300 S%0.2F (pen up)" % self.pen_up_angle) 131 | self.codes.append("G4 P%d (wait %dms)" % (self.stop_delay, self.stop_delay)) 132 | self.drawing = False 133 | self.codes.append("G1 X%.2f Y%.2f F%.2f" % (x,y, self.xy_feedrate)) 134 | self.last = (x,y) 135 | 136 | def draw_to_point(self, x, y, stop=False): 137 | if self.last == (x,y): 138 | return 139 | if stop: 140 | return 141 | else: 142 | if self.drawing == False: 143 | self.codes.append("M300 S%0.2F (pen down)" % self.pen_up_angle) 144 | self.codes.append("G4 P%d (wait %dms)" % (self.start_delay, self.start_delay)) 145 | self.drawing = True 146 | self.codes.append("G1 X%0.2f Y%0.2f F%0.2f" % (x,y, self.xy_feedrate)) 147 | self.last = (x,y) 148 | -------------------------------------------------------------------------------- /src/unicorn/entities.py: -------------------------------------------------------------------------------- 1 | from math import cos, sin, radians 2 | import pprint 3 | 4 | class Entity: 5 | def get_gcode(self,context): 6 | #raise NotImplementedError() 7 | return "NIE" 8 | 9 | class Line(Entity): 10 | def __str__(self): 11 | return "Line from [%.2f, %.2f] to [%.2f, %.2f]" % (self.start[0], self.start[1], self.end[0], self.end[1]) 12 | def get_gcode(self,context): 13 | "Emit gcode for drawing line" 14 | context.codes.append("(" + str(self) + ")") 15 | context.go_to_point(self.start[0],self.start[1]) 16 | context.draw_to_point(self.end[0],self.end[1]) 17 | context.codes.append("") 18 | 19 | class Circle(Entity): 20 | def __str__(self): 21 | return "Circle at [%.2f,%.2f], radius %.2f" % (self.center[0], self.center[1], self.radius) 22 | def get_gcode(self,context): 23 | "Emit gcode for drawing arc" 24 | start = (self.center[0] - self.radius, self.center[1]) 25 | arc_code = "G3 I%.2f J0 F%.2f" % (self.radius, context.xy_feedrate) 26 | 27 | context.codes.append("(" + str(self) + ")") 28 | context.go_to_point(start[0],start[1]) 29 | context.start() 30 | context.codes.append(arc_code) 31 | context.stop() 32 | context.codes.append("") 33 | 34 | class Arc(Entity): 35 | def __str__(self): 36 | return "Arc at [%.2f, %.2f], radius %.2f, from %.2f to %.2f" % (self.center[0], self.center[1], self.radius, self.start_angle, self.end_angle) 37 | 38 | def find_point(self,proportion): 39 | "Find point at the given proportion along the arc." 40 | delta = self.end_angle - self.start_angle 41 | angle = self.start_angle + delta*proportion 42 | 43 | return (self.center[0] + self.radius*cos(angle), self.center[1] + self.radius*sin(angle)) 44 | 45 | def get_gcode(self,context): 46 | "Emit gcode for drawing arc" 47 | start = self.find_point(0) 48 | end = self.find_point(1) 49 | delta = self.end_angle - self.start_angle 50 | 51 | if (delta < 0): 52 | arc_code = "G3" 53 | else: 54 | arc_code = "G3" 55 | arc_code = arc_code + " X%.2f Y%.2f I%.2f J%.2f F%.2f" % (end[0], end[1], self.center[0] - start[0], self.center[1] - start[1], context.xy_feedrate) 56 | 57 | context.codes.append("(" + str(self) + ")") 58 | context.go_to_point(start[0],start[1]) 59 | context.last = end 60 | context.start() 61 | context.codes.append(arc_code) 62 | context.stop() 63 | context.codes.append("") 64 | 65 | class Ellipse(Entity): 66 | #NOT YET IMPLEMENTED 67 | def __str__(self): 68 | return "Ellipse at [%.2f, %.2f], major [%.2f, %.2f], minor/major %.2f" + " start %.2f end %.2f" % \ 69 | (self.center[0], self.center[1], self.major[0], self.major[1], self.minor_to_major, self.start_param, self.end_param) 70 | 71 | class PolyLine(Entity): 72 | def __str__(self): 73 | return "Polyline consisting of %d segments." % len(self.segments) 74 | 75 | def get_gcode(self,context): 76 | "Emit gcode for drawing polyline" 77 | if hasattr(self, 'segments'): 78 | for points in self.segments: 79 | start = points[0] 80 | 81 | context.codes.append("(" + str(self) + ")") 82 | context.go_to_point(start[0],start[1]) 83 | context.start() 84 | for point in points[1:]: 85 | context.draw_to_point(point[0],point[1]) 86 | context.last = point 87 | context.stop() 88 | context.codes.append("") 89 | 90 | -------------------------------------------------------------------------------- /src/unicorn/svg_parser.py: -------------------------------------------------------------------------------- 1 | import inkex, cubicsuperpath, simplepath, simplestyle, cspsubdiv 2 | from simpletransform import * 3 | from bezmisc import * 4 | import entities 5 | from math import radians 6 | import sys, pprint 7 | 8 | def parseLengthWithUnits( str ): 9 | ''' 10 | Parse an SVG value which may or may not have units attached 11 | This version is greatly simplified in that it only allows: no units, 12 | units of px, and units of %. Everything else, it returns None for. 13 | There is a more general routine to consider in scour.py if more 14 | generality is ever needed. 15 | ''' 16 | u = 'px' 17 | s = str.strip() 18 | if s[-2:] == 'px': 19 | s = s[:-2] 20 | elif s[-1:] == '%': 21 | u = '%' 22 | s = s[:-1] 23 | try: 24 | v = float( s ) 25 | except: 26 | return None, None 27 | return v, u 28 | 29 | def subdivideCubicPath( sp, flat, i=1 ): 30 | """ 31 | Break up a bezier curve into smaller curves, each of which 32 | is approximately a straight line within a given tolerance 33 | (the "smoothness" defined by [flat]). 34 | 35 | This is a modified version of cspsubdiv.cspsubdiv(). I rewrote the recursive 36 | call because it caused recursion-depth errors on complicated line segments. 37 | """ 38 | 39 | while True: 40 | while True: 41 | if i >= len( sp ): 42 | return 43 | 44 | p0 = sp[i - 1][1] 45 | p1 = sp[i - 1][2] 46 | p2 = sp[i][0] 47 | p3 = sp[i][1] 48 | 49 | b = ( p0, p1, p2, p3 ) 50 | 51 | if cspsubdiv.maxdist( b ) > flat: 52 | break 53 | 54 | i += 1 55 | 56 | one, two = beziersplitatt( b, 0.5 ) 57 | sp[i - 1][2] = one[1] 58 | sp[i][0] = two[2] 59 | p = [one[2], one[3], two[1]] 60 | sp[i:1] = [p] 61 | 62 | class SvgIgnoredEntity: 63 | def load(self,node,mat): 64 | self.tag = node.tag 65 | def __str__(self): 66 | return "Ignored '%s' tag" % self.tag 67 | def get_gcode(self,context): 68 | #context.codes.append("(" + str(self) + ")") 69 | #context.codes.append("") 70 | return 71 | 72 | class SvgPath(entities.PolyLine): 73 | def load(self, node, mat): 74 | d = node.get('d') 75 | if len(simplepath.parsePath(d)) == 0: 76 | return 77 | p = cubicsuperpath.parsePath(d) 78 | applyTransformToPath(mat, p) 79 | 80 | # p is now a list of lists of cubic beziers [ctrl p1, ctrl p2, endpoint] 81 | # where the start-point is the last point in the previous segment 82 | self.segments = [] 83 | for sp in p: 84 | points = [] 85 | subdivideCubicPath(sp,0.2) # TODO: smoothness preference 86 | for csp in sp: 87 | points.append((csp[1][0],csp[1][1])) 88 | self.segments.append(points) 89 | 90 | def new_path_from_node(self, node): 91 | newpath = inkex.etree.Element(inkex.addNS('path','svg')) 92 | s = node.get('style') 93 | if s: 94 | newpath.set('style',s) 95 | t = node.get('transform') 96 | if t: 97 | newpath.set('transform',t) 98 | return newpath 99 | 100 | class SvgRect(SvgPath): 101 | def load(self, node, mat): 102 | newpath = self.new_path_from_node(node) 103 | x = float(node.get('x')) 104 | y = float(node.get('y')) 105 | w = float(node.get('width')) 106 | h = float(node.get('height')) 107 | a = [] 108 | a.append(['M ', [x,y]]) 109 | a.append([' l ', [w,0]]) 110 | a.append([' l ', [0,h]]) 111 | a.append([' l ', [-w,0]]) 112 | a.append([' Z', []]) 113 | newpath.set('d', simplepath.formatPath(a)) 114 | SvgPath.load(self,newpath,mat) 115 | 116 | class SvgLine(SvgPath): 117 | def load(self, node, mat): 118 | newpath = self.new_path_from_node(node) 119 | x1 = float(node.get('x1')) 120 | y1 = float(node.get('y1')) 121 | x2 = float(node.get('x2')) 122 | y2 = float(node.get('y2')) 123 | a = [] 124 | a.append(['M ', [x1,y1]]) 125 | a.append([' L ', [x2,y2]]) 126 | newpath.set('d', simplepath.formatPath(a)) 127 | SvgPath.load(self,newpath,mat) 128 | 129 | class SvgPolyLine(SvgPath): 130 | def load(self, node, mat): 131 | newpath = self.new_path_from_node(node) 132 | pl = node.get('points','').strip() 133 | if pl == '': 134 | return 135 | pa = pl.split() 136 | if not len(pa): 137 | return 138 | 139 | d = "M " + pa[0] 140 | for i in range(1, len(pa)): 141 | d += " L " + pa[i] 142 | newpath.set('d',d) 143 | SvgPath.load(self,newpath,mat) 144 | 145 | class SvgEllipse(SvgPath): 146 | def load(self, node,mat): 147 | rx = float(node.get('rx','0')) 148 | ry = float(node.get('ry','0')) 149 | SvgPath.load(self,self.make_ellipse_path(rx,ry,node), mat) 150 | def make_ellipse_path(self, rx, ry, node): 151 | if rx == 0 or ry == 0: 152 | return None 153 | cx = float(node.get('cx','0')) 154 | cy = float(node.get('cy','0')) 155 | x1 = cx - rx 156 | x2 = cx + rx 157 | d = 'M %f,%f ' % (x1,cy) + \ 158 | 'A %f,%f ' % (rx,ry) + \ 159 | '0 1 0 %f, %f ' % (x2,cy) + \ 160 | 'A %f,%f ' % (rx,ry) + \ 161 | '0 1 0 %f,%f' % (x1,cy) 162 | newpath = self.new_path_from_node(node) 163 | newpath.set('d',d) 164 | return newpath 165 | 166 | class SvgCircle(SvgEllipse): 167 | def load(self, node,mat): 168 | rx = float(node.get('r','0')) 169 | SvgPath.load(self,self.make_ellipse_path(rx,rx,node), mat) 170 | 171 | class SvgText(SvgIgnoredEntity): 172 | def load(self,node,mat): 173 | inkex.errormsg('Warning: unable to draw text. please convert it to a path first.') 174 | SvgIgnoredEntity.load(self,node,mat) 175 | 176 | class SvgLayerChange(): 177 | def __init__(self,layer_name): 178 | self.layer_name = layer_name 179 | def get_gcode(self,context): 180 | context.codes.append("M01 (Plotting layer '%s')" % self.layer_name) 181 | 182 | class SvgParser: 183 | 184 | entity_map = { 185 | 'path': SvgPath, 186 | 'rect': SvgRect, 187 | 'line': SvgLine, 188 | 'polyline': SvgPolyLine, 189 | 'polygon': SvgPolyLine, 190 | 'circle': SvgCircle, 191 | 'ellipse': SvgEllipse, 192 | 'pattern': SvgIgnoredEntity, 193 | 'metadata': SvgIgnoredEntity, 194 | 'defs': SvgIgnoredEntity, 195 | 'eggbot': SvgIgnoredEntity, 196 | ('namedview','sodipodi'): SvgIgnoredEntity, 197 | 'text': SvgText 198 | } 199 | 200 | def __init__(self, svg, pause_on_layer_change='false'): 201 | self.svg = svg 202 | self.pause_on_layer_change = pause_on_layer_change 203 | self.entities = [] 204 | 205 | def getLength( self, name, default ): 206 | ''' 207 | Get the attribute with name "name" and default value "default" 208 | Parse the attribute into a value and associated units. Then, accept 209 | no units (''), units of pixels ('px'), and units of percentage ('%'). 210 | ''' 211 | str = self.svg.get( name ) 212 | if str: 213 | v, u = parseLengthWithUnits( str ) 214 | if not v: 215 | # Couldn't parse the value 216 | return None 217 | elif ( u == '' ) or ( u == 'px' ): 218 | return v 219 | elif u == '%': 220 | return float( default ) * v / 100.0 221 | else: 222 | # Unsupported units 223 | return None 224 | else: 225 | # No width specified; assume the default value 226 | return float( default ) 227 | 228 | def parse(self): 229 | # 0.28222 scale determined by comparing pixels-per-mm in a default Inkscape file. 230 | self.svgWidth = self.getLength('width', 354) * 0.28222 231 | self.svgHeight = self.getLength('height', 354) * 0.28222 232 | self.recursivelyTraverseSvg(self.svg, [[0.28222, 0.0, -(self.svgWidth/2.0)], [0.0, -0.28222, (self.svgHeight/2.0)]]) 233 | 234 | # TODO: center this thing 235 | def recursivelyTraverseSvg(self, nodeList, 236 | matCurrent = [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0]], 237 | parent_visibility = 'visible'): 238 | """ 239 | Recursively traverse the svg file to plot out all of the 240 | paths. The function keeps track of the composite transformation 241 | that should be applied to each path. 242 | 243 | This function handles path, group, line, rect, polyline, polygon, 244 | circle, ellipse and use (clone) elements. Notable elements not 245 | handled include text. Unhandled elements should be converted to 246 | paths in Inkscape. 247 | 248 | TODO: There's a lot of inlined code in the eggbot version of this 249 | that would benefit from the Entities method of dealing with things. 250 | """ 251 | for node in nodeList: 252 | # Ignore invisible nodes 253 | v = node.get('visibility', parent_visibility) 254 | if v == 'inherit': 255 | v = parent_visibility 256 | if v == 'hidden' or v == 'collapse': 257 | pass 258 | 259 | # first apply the current matrix transform to this node's transform 260 | matNew = composeTransform(matCurrent, parseTransform(node.get("transform"))) 261 | 262 | if node.tag == inkex.addNS('g','svg') or node.tag == 'g': 263 | if (node.get(inkex.addNS('groupmode','inkscape')) == 'layer'): 264 | layer_name = node.get(inkex.addNS('label','inkscape')) 265 | if(self.pause_on_layer_change == 'true'): 266 | self.entities.append(SvgLayerChange(layer_name)) 267 | self.recursivelyTraverseSvg(node, matNew, parent_visibility = v) 268 | elif node.tag == inkex.addNS('use','svg') or node.tag == 'use': 269 | refid = node.get(inkex.addNS('href','xlink')) 270 | if refid: 271 | # [1:] to ignore leading '#' in reference 272 | path = '//*[@id="%s"]' % refid[1:] 273 | refnode = node.xpath( path ) 274 | if refnode: 275 | x = float(node.get('x','0')) 276 | y = float(node.get('y','0')) 277 | # Note: the transform has already been applied 278 | if (x!=0) or (y!=0): 279 | matNew2 = composeTransform(matNew,parseTransform('translate(%f,%f)' % (x,y))) 280 | else: 281 | matNew2 = matNew 282 | v = node.get('visibility',v) 283 | self.recursivelyTraverseSvg(refnode,matNew2,parent_visibility=v) 284 | else: 285 | pass 286 | else: 287 | pass 288 | elif not isinstance(node.tag, basestring): 289 | pass 290 | else: 291 | entity = self.make_entity(node, matNew) 292 | if entity == None: 293 | inkex.errormsg('Warning: unable to draw object, please convert it to a path first.') 294 | 295 | def make_entity(self,node,mat): 296 | for nodetype in SvgParser.entity_map.keys(): 297 | tag = nodetype 298 | ns = 'svg' 299 | if(type(tag) is tuple): 300 | tag = nodetype[0] 301 | ns = nodetype[1] 302 | if node.tag == inkex.addNS(tag,ns) or node.tag == tag: 303 | constructor = SvgParser.entity_map[nodetype] 304 | entity = constructor() 305 | entity.load(node,mat) 306 | self.entities.append(entity) 307 | return entity 308 | return None 309 | --------------------------------------------------------------------------------