Source code for pygencad.commandfile

"""Base classes for implementing drawing backend wrappers.

The base CommandFile class methods implement AutoCAD like outputs, and some are
used un-modified in the autocad.CommandFile implementation.
"""
from contextlib import contextmanager

[docs]class Layer(object): """Object representing a grouping and styling context. Layer like objects are expected to support at least naming, color, line weight, and line type. All properties except name are expected to be optional. >>> from pygencad.commandfile import Layer >>> l = Layer("test") >>> l <pygencad.commandfile.Layer object at 0x...> >>> print l # doctest: +NORMALIZE_WHITESPACE Layer: lv=test co=None lw=None lc=None :param name: The name visible in the backend 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. :param lw: The line-weight of the layer. :param lc: The line-class (type) of the layer. """ def __init__(self, name, co=None, lw=None, lc=None): """Store properties and setup template.""" # When passed a layer-like object return a new copy #if type(name) in (list, tuple): # Is iterable, but not string if hasattr(name, '__iter__'): # Is iterable self.name, self.co, self.lw, self.lc = name[:4] elif isinstance(name, Layer): # Assume we received another level object to copy self.name = name.name self.co = name.co self.lw = name.lw self.lc = name.lc else: self.name = name self.co = co self.lw = lw self.lc = lc # Command string conversion template self.template = 'Layer:\n\tlv={0.name}\n\tco={0.co}' self.template += '\n\tlw={0.lw}\n\tlc={0.lc}'
[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 the script context. """ return self.template.format(self)
[docs]class CommandFile(object): """Script generation interface. Provides convenience methods for issuing common commands, managing script context, and for issuing arbitrary commands. >>> from pygencad.commandfile import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> with CommandFile(f) as script: ... script.cmd("test") ... 'l' >>> print f.getvalue() test <BLANKLINE> :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 #: A local binding to the backend specific Layer class def __init__(self, filelike, setup=None, teardown=None): self.file = filelike self._layer = None self.layer_stack = list() self.anno_layer = 'annotation' self.user_teardown = teardown self.user_setup = setup
[docs] def setup(self): """Write default configuration info and run user provided setup func. Called automatically by CommandFile context manager. Writes default setup code, then adds any user provided setup. If the user setup is callable the return value is written to the script, otherwise the user setup is written as a string. """ if self.user_setup: try: # Assume user_setup is callable self.user_setup(self) except TypeError: # Fall back to printing it as a string self.file.write(self.user_setup)
[docs] def teardown(self): """Write cleanup commands and run user provided teardown func. Called automatically by CommandFile context manager. Writesany user provided teardown, then writes default teardown code. If the user teardown is callable the return value is written to the script, otherwise the user teardown is written as a string. """ if self.user_teardown: try: # Assume user_teardown is callable self.user_teardown(self) except TypeError: # Fall back to printing it as a string self.file.write(self.user_teardown)
def __enter__(self): """Setup script context.""" self.setup() return self def __exit__(self, kind, value, traceback): """Cleanup script context.""" self.teardown() @contextmanager
[docs] def layer(self, layer, *args, **kwargs): r"""Script layer context manager. Activates the provided layer settings, and resets layer state on exit. >>> from pygencad.commandfile import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> with CommandFile(f) as script: ... with script.layer('layer'): ... script.cmd("test") ... 'l' >>> print f.getvalue() # doctest: +NORMALIZE_WHITESPACE Layer: lv=layer co=None lw=None lc=None test <BLANKLINE> :param \*args: Valid Layer arguments. :param \*\*kwargs: Valid Layer arguments. """ self.set_layer(layer, *args, **kwargs) yield self.pop_layer()
[docs] def set_layer(self, layer, *args, **kwargs): r"""Activates the provided layer object or description. :param layer: A layer object, or a valid Layer name argument. :param \*args: Valid Layer arguments. :param \*\*kwargs: Valid Layer arguments. """ if self._layer: self.layer_stack.append(self._layer) # Force creation of new layer self._layer = self.Layer(layer, *args, **kwargs) self.file.write('{0}\n'.format(self._layer))
[docs] def pop_layer(self): """Restores active layer prior to last set_layer call.""" if self.layer_stack: self._layer = self.layer_stack.pop() self.file.write('{0}\n'.format(self._layer))
[docs] def cmd(self, command, *args): r"""Write an arbitrary script command. If the command string contains format expressions, and additional arguments were passed, the command string will be formated using the additional arguments. >>> from pygencad.commandfile import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> with CommandFile(f) as script: ... script.cmd("command with data: {:0.5f}", 355/113.0) ... 'l' >>> print f.getvalue() command with data: 3.14159 <BLANKLINE> :param command: The command to be written to the script. :type command: string :param \*args: Values to be formated by the command string. :returns: Some reference to any object created. """ try: self.file.write(command.format(*args) + '\n') except (IndexError, KeyError): # String format failed with too few args, user used literal {}? self.file.write(command + '\n') return 'l'
[docs] def point(self, p, reset=False, mode=None, write=True): """Convert an iterable into a point of the correct format. This method should be overridden in sub-classes. >>> from pygencad.commandfile import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> point = (1, 2, 3) >>> with CommandFile(f) as script: ... script.point(point) ... script.point(point, reset=True) ... script.point(point, mode='special') ... p = script.point(point, write=False) ... script.cmd("Here is the point: {}", p) ... '1,2,3' '1,2,3\\n' '1,2,3' 'l' >>> print f.getvalue() 1,2,3 1,2,3 <BLANKLINE> 1,2,3 Here is the point: 1,2,3 <BLANKLINE> :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 (unimplemented in base class). :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. This method may need to be overridden in sub-classes. >>> from pygencad.commandfile import CommandFile >>> from StringIO import StringIO >>> f = StringIO() >>> with CommandFile(f) as script: ... script.points(( ... (1, 2), ... (3, 4), ... (5, 6), ... )) ... >>> print f.getvalue() 1,2 3,4 5,6 <BLANKLINE> :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 (unimplemented in base class). :type mode: string """ for p in ps: self.point(p, mode=mode) if reset: self.reset()
[docs] def reset(self, write=True): """Write the sequence required to exit the current command. This method may need to be overridden in sub-classes. :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 a line segment command for each pair of points provided This method should be overridden in sub-classes. The output command for the line method is expected to produce a set of disconnected line segments. See the polyline command for outputting connected "line strings". :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 unimplemented in base class. :type mode: string :returns: Some reference to any object created. """ self.cmd("line") self.points(ps, reset, mode) return 'l'
[docs] def polyline(self, ps, close=True, reset=False, mode=None): """Write a string of lines connected at each of the provided points. This method should be overridden in sub-classes. The output command for the polyline method is expected to produce a single object connecting all the provided points in order. The object produced is expected to be "closable". Otherwise the method should ensure that the last point is equal to the first to close the polyline. :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 unimplemented in base class. :type mode: string :returns: Some reference to any object created. """ # This method is a placeholder and should be overridden in subclasses # Polylines are connected line strings, optionally closed if close: ps = list(ps) ps.append(ps[0]) self.cmd("polyline") self.points(ps, reset, mode) return 'l'
[docs] def circle(self, center, radius): """Write a circle command. This method should be overridden in sub-classes. :param center: Point in a format passable to CommandFile.point. :type center: iterable :param radius: Radius of the circle. :type radius: number :returns: Some reference to any object created. """ self.cmd("circle {} {}", self.point(center, write=False), radius) return 'l'
[docs] def text(self, text, p, height=1.0): """Write a text placement command. This method should be overridden in sub-classes. :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 :returns: Some reference to any object created. """ self.cmd("text {} {} h={}", self.point(p, write=False), text, height) return 'l'
[docs] def store(self, name, *els): r"""Store provided elements to be manipulated later. This method should be overridden in sub-classes. This method provides a uniform interface for grouping objects regardless of backend. This is required for the move/copy/rotate/scale methods to be of any practical use. Each backend's store method should transparently handle combining elements and groups. :param name: The id used to store the selection. :type name: backend specific :param \*els: The elements to store. :type \*els: backend specific :raises: ValueError if no elements are provided. :returns: An element group passable to move/copy/rotate/scale methods. """ if not els: raise ValueError('No elements provided to store!') self.file.write('{}({})\n'.format(name, ','.join(els))) 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: backend specific :param base: The base point to transform from. :type base: iterable :param dest: The dest point to transform to. :type dest: iterable :returns: Some reference to modified objects. """ self.file.write('move {} {} {}\n'.format(str(els), base, 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: backend specific :param base: The base point to transform from. :type base: iterable :param dest: The dest point to transform to. :type dest: iterable :returns: Some reference to duplicated objects. """ self.file.write('copy {} {} {}\n'.format(str(els), base, 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: backend specific :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: Some reference to modified objects. """ self.file.write('rotate {} {} {}\n'.format(els, base, 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: backend specific :param base: The base point to transform from. :type base: iterable :param scale: The scale factor to apply. :type scale: number :returns: Some reference to modified objects. """ self.file.write('scale {} {} {}\n'.format(els, base, scale)) return els
[docs] def erase(self, els): """Remove the indicated elements. :param els: The elements to remove. :type els: backend specific """ 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, "Initialize\n", "Finalize\n") 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()