Source code for pygencad.autocad

"""Wrapper for AutoCAD command script generation."""
from contextlib import contextmanager
import commandfile

EXT = '.scr'

[docs]class Layer(commandfile.Layer): """AutoCAD layer wrapper. In addition to color, line-weight, and line-type you can also set plot-style, non-plot, and description. The co, lw, and lc values default to "bylevel" which means that the attribute is not output in the script. :param name: The name visible in Acad after the script is run. A tuple of layer properties, or a Layer object may be passed as the first argument (name) in which case the new layer is initialized with provided values. :type name: string OR Layer-like :param co: The color of the layer. :type co: int :param lw: The line-weight of the layer. :type lw: number :param lc: The line-class (type) of the layer. :type lc: string :param desc: The layer description. :type desc: string :param st: The layer style. :type st: string :param plt: Whether the layer is can be plotted. :type plt: boolean """ ctemplate = 'c {0.co}\n ' wtemplate = 'lw {0.lw}\n ' ltemplate = 'l {0.lc}\n ' ptemplate = 'p {0.plt}\n ' stemplate = 'ps {0.st}\n ' dtemplate = 'd {0.desc}\n{0.name}\n' def __init__(self, name, co=None, lw=None, lc=None, desc='', st='', plt=True): if not co: co = 'bylevel' if not lw: lw = 'bylevel' if not lc: lc = 'bylevel' # Using oldstyle super call commandfile.Layer.__init__(self, name, co, lw, lc) if hasattr(name, '__iter__'): # Is iterable self.desc, self.st, self.plt = name[1:4] elif type(name) not in (str, buffer, unicode): # Assume we received another layer object to copy self.desc = name.desc self.st = name.st self.plt = name.plt else: self.desc = desc self.st = st self.plt = 'plot' if plt else 'no' # Command string conversion template self.template = '-layer make {0.name}\n'
[docs] def __str__(self): """Render this layer as a string. The __str__ method is used to output the commands necessary to set the Layer objects properties active in AutoCAD. """ s = self.template if self.co != 'bylevel': s += self.ctemplate if self.lw != 'bylevel': s += self.wtemplate if self.lc != 'bylevel': s += self.ltemplate if self.desc: s += self.dtemplate if self.st: s += self.stemplate s += self.ptemplate return s.format(self)
[docs]class CommandFile(commandfile.CommandFile): """Wrapper for AutoCAD script generation. In addition to implementing the methods defined in the base class this class provides a UCS context manager, 3d polyline method, and block method. :param filelike: An object with a write method. :type filelike: filelike :param setup: Commands to include at the beginning of the script. :type setup: string OR callable :param teardown: Commands to include at the end of the script. :type teardown: string OR callable """ Layer = Layer #: Local binding to the Acad Layer class def __init__(self, filelike, setup=None, teardown=None): commandfile.CommandFile.__init__(self, filelike, setup, teardown)
[docs] def setup(self): """Write default configuration info and run user provided setup func. The default setup stores the active osnaps in a variable named "osmodeinit" and turns off osnaps (which normally interfere with scripts). The initial value of cmdecho is recorded as "cmdechoinit" and cmdecho is also disabled. """ # Write AutoCAD specific setup #TODO load commands directly from file? self.cmd('!(setq osmodeinit (getvar "OSMODE"))') self.cmd("osmode 0") self.cmd('!(setq cmdechoinit (getvar "CMDECHO"))') self.cmd('!(setvar "CMDECHO" 0)') # Add ssjoin lisp self.cmd('''!(defun ssjoin ( ss1 ss2 / count ) (setq count 0) (repeat (sslength ss2) (ssadd (ssname ss2 count) ss1) (setq count (1+ count)) ))''') # Run base class setup commandfile.CommandFile.setup(self)
[docs] def teardown(self): """Write cleanup commands and run user provided teardown func. The initial values recorded in the setup method are restored. """ # Write AutoCAD specific teardown #TODO load commands directly from file? self.cmd("osmode !osmodeinit") self.cmd('!(setvar "CMDECHO" cmdechoinit)') # Run base class cleanup commandfile.CommandFile.teardown(self)
@contextmanager
[docs] def ucs(self, command, points=None, count=1): """User Coordinate System context manager. Write a set of UCS commands and rollback changes when done. >>> from pygencad.autocad import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> script = CommandFile(f) >>> with script.ucs('ucs w\\nucs zaxis', ((0,0,0), (1,1,1)), 2): ... # Draw a circle at origin pointing to the positive quadrant ... script.circle((0,0,0), 5) ... 'l' >>> print f.getvalue() ucs w ucs zaxis 0,0,0 1,1,1 circle 0,0,0 5 ucs p ucs p <BLANKLINE> :param command: UCS commands to execute on entry. :type command: string :param points: A list of points required by the *last* UCS command. :type points: iterable :param count: How many UCS commands were issued. :type count: number """ self.set_ucs(command, points) yield self.pop_ucs(count)
[docs] def set_ucs(self, command, points=None): """Change the active UCS. :param command: UCS commands to execute on entry. :type command: string :param points: A list of points required by the *last* UCS command. :type points: iterable """ self.cmd(command) if points: self.points(points)
[docs] def pop_ucs(self, count=1): """Rollback the indicated number of UCS changes. :param count: How many UCS commands need to be rolled back. :type count: number """ self.file.write("ucs p\n" * count)
[docs] def point(self, p, reset=False, mode=None, write=True): """Convert an iterable into a point of the correct format. The mode argument is currently unsupported, but should support relative, polar, and relative polar coordinate input. :param p: The point to be converted. :type command: iterable :param reset: Optionally append the result of calling the reset method after writing the point. :type reset: boolean :param mode: Optional point output mode. :type mode: string :param write: Flag for writing output to script file. :type write: boolean :returns: The point converted to a string. """ coord = "{0}".format(",".join(str(i) for i in p)) if reset: coord += self.reset(write=False) if write: self.file.write(coord + '\n') return coord
[docs] def points(self, ps, reset=False, mode=None): """Calls CommandFile.point on each member of an iterable. The mode Argument is currently unsupported, but should support relative, polar, and relative polar coordinate input. :param ps: The points to be converted. :type command: iterable :param reset: Optionally append the result of calling the reset method after writing all points. :type reset: boolean :param mode: Optional point output mode. :type mode: string """ for p in ps: self.point(p, mode) if reset: self.reset()
[docs] def reset(self, write=True): """Write the sequence required to exit the current command. For Acad this method currently just outputs a newline. :param write: Flag for writing output to script file. :type write: boolean :returns: The command that output by reset as a string. """ cmd = "\n" if write: self.file.write(cmd) return cmd
[docs] def line(self, ps, reset=False, mode=None): """Write the Acad line command. The mode argument is currently unsupported. :param ps: The end points of the lines. :type command: iterable :param reset: Optionally append the result of calling the reset method after writing all points. :type reset: boolean :param mode: Optional point output mode. :type mode: string :returns: Reference to created object. """ self.cmd("line") self.points(ps, reset, mode) return 'l'
[docs] def polyline(self, ps, close=True, reset=False, mode=None): """Write the Acad pline command. Support could be added in the future for drawing arc segments or other special pline settings. The mode argument is currently unsupported. :param ps: The ordered points forming the line. :type command: iterable :param close: Flag indicating whether the line should end where it started. :type close: boolean :param reset: Optionally append the result of calling the reset method after writing all points. :type reset: boolean :param mode: Optional point output mode. :type mode: string :returns: Reference to created object. """ self.cmd("pline") self.points(ps, mode=mode) if close: self.cmd("c") if reset: self.reset() return 'l'
[docs] def poly3d(self, ps, close=True, reset=False, mode=None): """Write the Acad 3dpolyline command. In Acad polylines are strictly 2d in the xy plane of the active UCS. To draw non-planer polylines you must use the 3dpolyline. While 2d plines support all properties, arc segments, and even splines, 3dpolylines are always straight segments, and don't support line-types (as of Acad2012). The mode argument is currently unsupported. :param ps: The ordered points forming the line. :type command: iterable :param close: Flag indicating whether the line should end where it started. :type close: boolean :param reset: Optionally append the result of calling the reset method after writing all points. :type reset: boolean :param mode: Optional point output mode. :type mode: string :returns: Reference to created object. """ self.cmd("3dpoly") self.points(ps, mode=mode) if close: self.cmd("c") if reset: self.reset() return 'l'
[docs] def circle(self, center, radius): """Write the Acad circle command. :param center: Point in a format passable to CommandFile.point. :type center: iterable :param radius: Radius of the circle. :type radius: number :returns: Reference to created object. """ self.cmd("circle {} {}", self.point(center, write=False), radius) return 'l'
[docs] def text(self, text, p, height=1.0, ang=0, just='tl'): """Write a text placement command. Text is placed as mtext. The text object width is set to 0 (infinite) meaning the text does not automatically wrap. Support for setting width, and setting the active text style, could be added in the future. :param text: The text to place. :type text: string :param p: The insertion point in a format passable to CommandFile.point. :type p: iterable :param height: How tall the text lines are. :type height: number :param ang: The angle of the text in degrees. :type ang: number :param just: The justification of the text, a pair of [t, c, b][l, c, r]. :type just: string :returns: Reference to created object. """ self.file.write('-mtext\n') self.point(p) self.file.write('h {0}\n'.format(height)) self.file.write('j {0}\n'.format(just)) self.file.write('r {0:0.0{1}f}\n'.format(ang, 4)) self.file.write('w 0\n') # set mtext width to zero (infinite) self.file.write(text) self.file.write('\n\n') # Final returns to exit command return 'l'
[docs] def block(self, name, loc, ang=0, scale=1, attrs=None): """Write a block placement command. Currently only basic insertion is supported. Setting non-uniform scale factors, setting text values for attribute driven blocks, and setting dynamic block attributes could be added in the future. The attrs argument is currently unsupported. :param name: The name of the block in Acad that is to be inserted. :type name: string :param loc: The block insertion point. :type loc: iterable :param ang: The rotation angle of the inserted block in degrees. :type ang: number :param scale: The uniform scale factor of the inserted block. :type scale: number :param attrs: Attributes to be set for the block after insertion. :type attrs: mapping :returns: Reference to created object. """ # FIXME check commands # FIXME refactor "loc" argument to match other methods self.file.write('insert {0}\n'.format(name)) self.file.write('a {0:0.0{1}f}\n'.format(ang, 8)) self.file.write('s {0:0.0{1}f}\n'.format(scale, 8)) self.point(loc) return 'l'
[docs] def store(self, name, *els): r"""Store provided elements to be manipulated later. For AutoCAD named selection sets are created (AutoCAD has a hard limit of 128 named selection sets). The selection set name will have a '!' prepended, and can be passed to any command expecting a selection. If els contains 'l' than only one item (the last drawn) is added to the stored selection. Alternately, if els contains only selections set names, a new joined selection is created. :param name: The id used to store the selection. :type name: string :param \*els: The elements to store. :type \*els: 'l' for last, or selection set names. :raises: ValueError if no elements are provided. :returns: The name of the selection set. """ if '!' not in name: ssname = name name = '!' + name else: ssname = name.strip('!') # Handle empty els if not els: raise ValueError("No elements provided to store!") # Create the named selection set if it doesn't exist self.cmd("!(if (= {0} nil) (setq {0} (ssadd)))".format(ssname)) # There are two options for building sets if 'l' in els: # if 'l' (last) is in the els then only one element can be added self.cmd("!(ssadd (entlast) {})".format(ssname)) else: # Otherwise els should be a list of sset names for el in els: self.cmd('!(ssjoin {} {})'.format(ssname, el.strip('!'))) return name
[docs] def move(self, els, base=(0, 0), dest=(0, 0)): """Transform the provided elements from base to dest. :param els: The elements to move. :type els: A valid AutoCAD selection string. :param base: The base point to transform from. :type base: iterable :param dest: The dest point to transform to. :type dest: iterable :returns: Reference to modified objects. """ self.file.write('move {} \n'.format(els)) self.point(base) self.point(dest) return els
[docs] def copy(self, els, base=(0, 0), dest=(0, 0)): """Transform duplicates of the provided elements from base to dest. :param els: The elements to copy. :type els: A valid AutoCAD selection string. :param base: The base point to transform from. :type base: iterable :param dest: The dest point to transform to. :type dest: iterable :returns: Reference to modified objects. """ # TODO copy should return a selection set containing the *new* elements self.file.write('copy {} \n'.format(els)) self.point(base) self.point(dest) return els
[docs] def rotate(self, els, base=(0, 0), ang=0): """Rotate the provided elements from base by angle. :param els: The elements to rotate. :type els: A valid AutoCAD selection string. :param base: The base point to transform from. :type base: iterable :param ang: The angle to rotate in degrees (counter-clockwise). :type ang: number :returns: Reference to modified objects. """ self.file.write('rotate {} \n'.format(els)) self.point(base) self.file.write('{}\n'.format(ang)) return els
[docs] def scale(self, els, base=(0, 0), scale=1): """Scale the provided elements from base by scale. Remember that scaling an axis by -1 is equivalent to mirroring. :param els: The elements to scale. :type els: A valid AutoCAD selection string. :param base: The base point to transform from. :type base: iterable :param scale: The scale factor to apply. :type scale: number :returns: Reference to modified objects. """ self.file.write('scale {} \n'.format(els)) self.point(base) self.file.write('{}\n'.format(scale)) return els
[docs] def erase(self, els): """Remove the indicated elements. :param els: The elements to remove. :type els: A valid AutoCAD selection string. """ self.file.write('erase {} \n'.format(els))
if __name__ == "__main__": # pragma: no cover # When run as a script generate a square from cStringIO import StringIO from contextlib import closing with closing(StringIO()) as outfile: # Build custom layer mylayer = Layer('custom') # Create script file with CommandFile(outfile) as script: # Draw a shape with script.layer('first'): script.cmd('line') script.point((1, 2)) script.point((2, 2)) script.reset() with script.layer(mylayer): script.cmd('line') script.point((2, 2)) script.point((2, 1)) script.reset() with script.layer('third'): script.cmd('line') script.points(( (2, 1), (1, 1) )) script.reset() with script.layer('fourth'): script.cmd('line') script.point((1, 1)) script.point((1, 2)) script.reset() print outfile.getvalue()