diff --git a/mesh_inset/__init__.py b/mesh_inset/__init__.py
index 108ff3657ba071ddbc0a1583f0f072350128a19f..0a441d1b7e7665746389c9c5abb4ef638352b77b 100644
--- a/mesh_inset/__init__.py
+++ b/mesh_inset/__init__.py
@@ -16,26 +16,30 @@
 #
 # ##### END GPL LICENSE BLOCK #####
 
+# <pep8 compliant>
+
 bl_info = {
-  "name": "Inset Polygon",
-  "author": "Howard Trickey",
-  "version": (0, 2),
-  "blender": (2, 5, 7),
-  "api": 36147,
-  "location": "View3D > Tools",
-  "description": "Make an inset polygon inside selection.",
-  "warning": "",
-  "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/Scripts/Modeling/Inset-Polygon",
-  "tracker_url": "http://projects.blender.org/tracker/index.php?func=detail&aid=27290&group_id=153&atid=468",
-  "category": "Mesh"}
+    "name": "Inset Polygon",
+    "author": "Howard Trickey",
+    "version": (0, 2),
+    "blender": (2, 5, 7),
+    "api": 36147,
+    "location": "View3D > Tools",
+    "description": "Make an inset polygon inside selection.",
+    "warning": "",
+    "wiki_url": \
+      "http://wiki.blender.org/index.php/Extensions:2.5/Py/Scripts/Modeling/Inset-Polygon",
+    "tracker_url": \
+      "http://projects.blender.org/tracker/index.php?func=detail&aid=27290&group_id=153&atid=468",
+    "category": "Mesh"}
 
 if "bpy" in locals():
-  import imp
+    import imp
 else:
-  from . import geom
-  from . import model
-  from . import offset
-  from . import triquad
+    from . import geom
+    from . import model
+    from . import offset
+    from . import triquad
 
 import math
 import bpy
@@ -44,134 +48,156 @@ from bpy.props import *
 
 
 class Inset(bpy.types.Operator):
-  bl_idname = "mesh.inset"
-  bl_label = "Inset"
-  bl_description = "Make an inset polygon inside selection"
-  bl_options = {'REGISTER', 'UNDO'}
-
-  inset_amount = FloatProperty(name="Amount",
-    description="Distance of inset edges from outer ones",
-    default = 0.05,
-    min = 0.0,
-    max = 1000.0,
-    soft_min = 0.0,
-    soft_max = 1.0,
-    unit = 'LENGTH')
-  inset_height = FloatProperty(name="Height",
-    description="Distance to raise inset faces",
-    default = 0.0,
-    min = -1000.0,
-    max = 1000.0,
-    soft_min = -1.0,
-    soft_max = 1.0,
-    unit = 'LENGTH')
-  region = BoolProperty(name="Region",
-    description="Inset selection as one region?",
-    default = True)
-
-  @classmethod
-  def poll(cls, context):
-    obj = context.active_object
-    return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
-
-  def draw(self, context):
-    layout= self.layout
-    box = layout.box()
-    box.label("Inset Options")
-    box.prop(self, "inset_amount")
-    box.prop(self, "inset_height")
-    box.prop(self, "region")
-
-  def invoke(self, context, event):
-    self.action(context)
-    return {'FINISHED'}
-
-  def execute(self, context):
-    self.action(context)
-    return {'FINISHED'}
-
-  def action(self, context):
-    save_global_undo = bpy.context.user_preferences.edit.use_global_undo
-    bpy.context.user_preferences.edit.use_global_undo = False
+    bl_idname = "mesh.inset"
+    bl_label = "Inset"
+    bl_description = "Make an inset polygon inside selection"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    inset_amount = FloatProperty(name="Amount",
+        description="Amount to move inset edges",
+        default=5.0,
+        min=0.0,
+        max=1000.0,
+        soft_min=0.0,
+        soft_max=100.0,
+        unit='LENGTH')
+    inset_height = FloatProperty(name="Height",
+        description="Amount to raise inset faces",
+        default=0.0,
+        min=-10000.0,
+        max=10000.0,
+        soft_min=-500.0,
+        soft_max=500.0,
+        unit='LENGTH')
+    region = BoolProperty(name="Region",
+        description="Inset selection as one region?",
+        default=True)
+    scale = EnumProperty(name="Scale",
+        description="Scale for amount",
+        items=[
+            ('PERCENT', "Percent",
+                "Percentage of maximum inset amount"),
+            ('ABSOLUTE', "Absolute",
+                "Length in blender units")
+            ],
+        default='PERCENT')
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+    def draw(self, context):
+        layout = self.layout
+        box = layout.box()
+        box.label("Inset Options")
+        box.prop(self, "scale")
+        box.prop(self, "inset_amount")
+        box.prop(self, "inset_height")
+        box.prop(self, "region")
+
+    def invoke(self, context, event):
+        self.action(context)
+        return {'FINISHED'}
+
+    def execute(self, context):
+        self.action(context)
+        return {'FINISHED'}
+
+    def action(self, context):
+        save_global_undo = bpy.context.user_preferences.edit.use_global_undo
+        bpy.context.user_preferences.edit.use_global_undo = False
+        bpy.ops.object.mode_set(mode='OBJECT')
+        obj = bpy.context.active_object
+        mesh = obj.data
+        do_inset(mesh, self.inset_amount, self.inset_height, self.region,
+            self.scale == 'PERCENT')
+        bpy.ops.object.mode_set(mode='EDIT')
+        bpy.context.user_preferences.edit.use_global_undo = save_global_undo
+
+
+def do_inset(mesh, amount, height, region, as_percent):
+    if amount <= 0.0:
+        return
+    pitch = math.atan(height / amount)
+    selfaces = []
+    selface_indices = []
+    for face in mesh.faces:
+        if face.select and not face.hide:
+            selfaces.append(face)
+            selface_indices.append(face.index)
+    m = geom.Model()
+    # if add all mesh.vertices, coord indices will line up
+    # Note: not using Points.AddPoint which does dup elim
+    # because then would have to map vertices in and out
+    m.points.pos = [v.co.to_tuple() for v in mesh.vertices]
+    for f in selfaces:
+        m.faces.append(list(f.vertices))
+        m.face_data.append(f.index)
+    orig_numv = len(m.points.pos)
+    orig_numf = len(m.faces)
+    model.BevelSelectionInModel(m, amount, pitch, True, region, as_percent)
+    if len(m.faces) == orig_numf:
+        # something went wrong with Bevel - just treat as no-op
+        return
+    # blender_faces: newfaces but all 4-tuples and no 0
+    # in 4th position if a 4-sided poly
+    blender_faces = []
+    blender_old_face_index = []
+    for i in range(orig_numf, len(m.faces)):
+        f = m.faces[i]
+        if len(f) == 3:
+            blender_faces.append(list(f) + [0])
+            blender_old_face_index.append(m.face_data[i])
+        elif len(f) == 4:
+            if f[3] == 0:
+                blender_faces.append([f[3], f[0], f[1], f[2]])
+            else:
+                blender_faces.append(f)
+            blender_old_face_index.append(m.face_data[i])
+    num_new_vertices = len(m.points.pos) - orig_numv
+    mesh.vertices.add(num_new_vertices)
+    for i in range(orig_numv, len(m.points.pos)):
+        mesh.vertices[i].co = mathutils.Vector(m.points.pos[i])
+    start_faces = len(mesh.faces)
+    mesh.faces.add(len(blender_faces))
+    for i, newf in enumerate(blender_faces):
+        mesh.faces[start_faces + i].vertices_raw = newf
+        # copy face attributes from old face that it was derived from
+        bfi = blender_old_face_index[i]
+        if bfi and 0 <= bfi < start_faces:
+            bfacenew = mesh.faces[start_faces + i]
+            bface = mesh.faces[bfi]
+            bfacenew.material_index = bface.material_index
+            bfacenew.use_smooth = bface.use_smooth
+    mesh.update(calc_edges=True)
+    # remove original faces
+    bpy.ops.object.mode_set(mode='EDIT')
+    save_select_mode = bpy.context.tool_settings.mesh_select_mode
+    bpy.context.tool_settings.mesh_select_mode = [False, False, True]
+    bpy.ops.mesh.select_all(action='DESELECT')
     bpy.ops.object.mode_set(mode='OBJECT')
-    obj = bpy.context.active_object
-    mesh = obj.data
-    do_inset(mesh, self.inset_amount, self.inset_height, self.region)
+    for fi in selface_indices:
+        mesh.faces[fi].select = True
     bpy.ops.object.mode_set(mode='EDIT')
-    bpy.context.user_preferences.edit.use_global_undo = save_global_undo
-
-
-def do_inset(mesh, amount, height, region):
-  if amount <= 0.0:
-    return
-  pitch = math.atan(height / amount)
-  selfaces = []
-  selface_indices = []
-  for face in mesh.faces:
-    if face.select and not face.hide:
-      selfaces.append(face)
-      selface_indices.append(face.index)
-  m = geom.Model()
-  # if add all mesh.vertices, coord indices will line up
-  # Note: not using Points.AddPoint which does dup elim
-  # because then would have to map vertices in and out
-  m.points.pos = [ v.co.to_tuple() for v in mesh.vertices ]
-  for f in selfaces:
-    m.faces.append(list(f.vertices))
-  orig_numv = len(m.points.pos)
-  orig_numf = len(m.faces)
-  model.BevelSelectionInModel(m, m.faces, amount, pitch, True, region)
-  if len(m.faces) == orig_numf:
-    # something went wrong with Bevel - just treat as no-op
-    return
-  # blender_faces: newfaces but all 4-tuples and no 0
-  # in 4th position if a 4-sided poly
-  blender_faces = []
-  for i in range(orig_numf, len(m.faces)):
-    f = m.faces[i]
-    if len(f) == 3:
-      blender_faces.append(list(f) + [0])
-    elif len(f) == 4:
-      if f[3] == 0:
-        blender_faces.append([f[3], f[0], f[1], f[2]])
-      else:
-        blender_faces.append(f)
-  num_new_vertices = len(m.points.pos) - orig_numv
-  mesh.vertices.add(num_new_vertices)
-  for i in range(orig_numv, len(m.points.pos)):
-    mesh.vertices[i].co = mathutils.Vector(m.points.pos[i])
-  start_faces = len(mesh.faces)
-  mesh.faces.add(len(blender_faces))
-  for i, newf in enumerate(blender_faces):
-    mesh.faces[start_faces + i].vertices_raw = newf
-  mesh.update(calc_edges = True)
-  # remove original faces
-  bpy.ops.object.mode_set(mode='EDIT')
-  save_select_mode = bpy.context.tool_settings.mesh_select_mode
-  bpy.context.tool_settings.mesh_select_mode = [False, False, True]
-  bpy.ops.mesh.select_all(action = 'DESELECT')
-  bpy.ops.object.mode_set(mode = 'OBJECT')
-  for fi in selface_indices:
-    mesh.faces[fi].select = True
-  bpy.ops.object.mode_set(mode = 'EDIT')
-  bpy.ops.mesh.delete(type = 'FACE')
-  bpy.context.tool_settings.mesh_select_mode = save_select_mode
+    bpy.ops.mesh.delete(type='FACE')
+    bpy.context.tool_settings.mesh_select_mode = save_select_mode
 
 
 def panel_func(self, context):
-  self.layout.label(text="Inset:")
-  self.layout.operator("mesh.inset", text="Inset")
+    self.layout.label(text="Inset:")
+    self.layout.operator("mesh.inset", text="Inset")
 
 
 def register():
-  bpy.utils.register_class(Inset)
-  bpy.types.VIEW3D_PT_tools_meshedit.append(panel_func)
+    bpy.utils.register_class(Inset)
+    bpy.types.VIEW3D_PT_tools_meshedit.append(panel_func)
 
 
 def unregister():
-  bpy.utils.unregister_class(Inset)
-  bpy.types.VIEW3D_PT_tools_meshedit.remove(panel_func)
+    bpy.utils.unregister_class(Inset)
+    bpy.types.VIEW3D_PT_tools_meshedit.remove(panel_func)
 
 
 if __name__ == "__main__":
-  register()
+    register()
diff --git a/mesh_inset/geom.py b/mesh_inset/geom.py
index 7c21015efe80ff88a73f284617c520ae7eef1427..98059dcf29d40ef11d77b24157106d61aaf0db43 100644
--- a/mesh_inset/geom.py
+++ b/mesh_inset/geom.py
@@ -16,6 +16,8 @@
 #
 # ##### END GPL LICENSE BLOCK #####
 
+# <pep8 compliant>
+
 """Geometry classes and operations.
 Also, vector file representation (Art).
 """
@@ -31,666 +33,687 @@ INVDISTTOL = 1e3
 
 
 class Points(object):
-  """Container of points without duplication, each mapped to an int.
+    """Container of points without duplication, each mapped to an int.
 
-  Points are either have dimension at least 2, maybe more.
+    Points are either have dimension at least 2, maybe more.
 
-  Implementation:
-  In order to efficiently find duplicates, we quantize the points
-  to triples of ints and map from quantized triples to vertex
-  index.
+    Implementation:
+    In order to efficiently find duplicates, we quantize the points
+    to triples of ints and map from quantized triples to vertex
+    index.
 
-  Attributes:
-    pos: list of tuple of float - coordinates indexed by
-        vertex number
-    invmap: dict of (int, int, int) to int - quantized coordinates
-        to vertex number map
-  """
+    Attributes:
+      pos: list of tuple of float - coordinates indexed by
+          vertex number
+      invmap: dict of (int, int, int) to int - quantized coordinates
+          to vertex number map
+    """
 
-  def __init__(self, initlist = []):
-    self.pos = []
-    self.invmap = dict()
-    for p in initlist:
-      self.AddPoint(p)
+    def __init__(self, initlist=[]):
+        self.pos = []
+        self.invmap = dict()
+        for p in initlist:
+            self.AddPoint(p)
 
-  @staticmethod
-  def Quantize(p):
-    """Quantize the float tuple into an int tuple.
+    @staticmethod
+    def Quantize(p):
+        """Quantize the float tuple into an int tuple.
 
-    Args:
-      p: tuple of float
-    Returns:
-      tuple of int - scaled by INVDISTTOL and rounded p
-    """
+        Args:
+          p: tuple of float
+        Returns:
+          tuple of int - scaled by INVDISTTOL and rounded p
+        """
 
-    return tuple([int(round(v*INVDISTTOL)) for v in p])
+        return tuple([int(round(v * INVDISTTOL)) for v in p])
 
-  def AddPoint(self, p):
-    """Add point p to the Points set and return vertex number.
+    def AddPoint(self, p):
+        """Add point p to the Points set and return vertex number.
 
-    If there is an existing point which quantizes the same,,
-    don't add a new one but instead return existing index.
+        If there is an existing point which quantizes the same,,
+        don't add a new one but instead return existing index.
 
-    Args:
-      p: tuple of float - coordinates (2-tuple or 3-tuple)
-    Returns:
-      int - the vertex number of added (or existing) point
-    """
+        Args:
+          p: tuple of float - coordinates (2-tuple or 3-tuple)
+        Returns:
+          int - the vertex number of added (or existing) point
+        """
 
-    qp = Points.Quantize(p)
-    if qp in self.invmap:
-      return self.invmap[qp]
-    else:
-      self.invmap[qp] = len(self.pos)
-      self.pos.append(p)
-      return len(self.pos)-1
+        qp = Points.Quantize(p)
+        if qp in self.invmap:
+            return self.invmap[qp]
+        else:
+            self.invmap[qp] = len(self.pos)
+            self.pos.append(p)
+            return len(self.pos) - 1
 
-  def AddPoints(self, points):
-    """Add another set of points to this set.
+    def AddPoints(self, points):
+        """Add another set of points to this set.
 
-    We need to return a mapping from indices
-    in the argument points space into indices
-    in this point space.
+        We need to return a mapping from indices
+        in the argument points space into indices
+        in this point space.
 
-    Args:
-      points: Points - to union into this set
-    Returns:
-      list of int: maps added indices to new ones
-    """
+        Args:
+          points: Points - to union into this set
+        Returns:
+          list of int: maps added indices to new ones
+        """
 
-    vmap = [ 0 ] * len(points.pos)
-    for i in range(len(points.pos)):
-      vmap[i] = self.AddPoint(points.pos[i])
-    return vmap
+        vmap = [0] * len(points.pos)
+        for i in range(len(points.pos)):
+            vmap[i] = self.AddPoint(points.pos[i])
+        return vmap
 
-  def AddZCoord(self, z):
-    """Change this in place to have a z coordinate, with value z.
+    def AddZCoord(self, z):
+        """Change this in place to have a z coordinate, with value z.
 
-    Assumes the coordinates are currently 2d.
+        Assumes the coordinates are currently 2d.
 
-    Args:
-      z: the value of the z coordinate to add
-    Side Effect:
-      self now has a z-coordinate added
-    """
+        Args:
+          z: the value of the z coordinate to add
+        Side Effect:
+          self now has a z-coordinate added
+        """
 
-    assert(len(self.pos) == 0 or len(self.pos[0]) == 2)
-    newinvmap = dict()
-    for i, (x,y) in enumerate(self.pos):
-      newp = (x, y, z)
-      self.pos[i] = newp
-      newinvmap[self.Quantize(newp)] = i
-    self.invmap = newinvmap
+        assert(len(self.pos) == 0 or len(self.pos[0]) == 2)
+        newinvmap = dict()
+        for i, (x, y) in enumerate(self.pos):
+            newp = (x, y, z)
+            self.pos[i] = newp
+            newinvmap[self.Quantize(newp)] = i
+        self.invmap = newinvmap
 
-  def AddToZCoord(self, i, delta):
-    """Change the z-coordinate of point with index i to add delta.
+    def AddToZCoord(self, i, delta):
+        """Change the z-coordinate of point with index i to add delta.
 
-    Assumes the coordinates are currently 3d.
+        Assumes the coordinates are currently 3d.
 
-    Args:
-      i: int - index of a point
-      delta: float - value to add to z-coord
-    """
+        Args:
+          i: int - index of a point
+          delta: float - value to add to z-coord
+        """
 
-    (x, y, z) = self.pos[i]
-    self.pos[i] = (x, y, z + delta)
+        (x, y, z) = self.pos[i]
+        self.pos[i] = (x, y, z + delta)
 
 
 class PolyArea(object):
-  """Contains a Polygonal Area (polygon with possible holes).
-
-  A polygon is a list of vertex ids, each an index given by
-  a Points object. The list represents a CCW-oriented
-  outer boundary (implicitly closed).
-  If there are holes, they are lists of CW-oriented vertices
-  that should be contained in the outer boundary.
-  (So the left face of both the poly and the holes is
-  the filled part.)
-
-  Attributes:
-    points: Points
-    poly: list of vertex ids
-    holes: list of lists of vertex ids (each a hole in poly)
-    color: (float, float, float)- rgb color used to fill
-  """
-
-  def __init__(self, points = None, poly = None, holes = None):
-    self.points = points if points else Points()
-    self.poly = poly if poly else []
-    self.holes = holes if holes else []
-    self.color = (0.0, 0.0, 0.0)
-
-  def AddHole(self, holepa):
-    """Add a PolyArea's poly as a hole of self.
-
-    Need to reverse the contour and
-    adjust the the point indexes and self.points.
-
-    Args:
-      holepa: PolyArea
+    """Contains a Polygonal Area (polygon with possible holes).
+
+    A polygon is a list of vertex ids, each an index given by
+    a Points object. The list represents a CCW-oriented
+    outer boundary (implicitly closed).
+    If there are holes, they are lists of CW-oriented vertices
+    that should be contained in the outer boundary.
+    (So the left face of both the poly and the holes is
+    the filled part.)
+
+    Attributes:
+      points: Points
+      poly: list of vertex ids
+      holes: list of lists of vertex ids (each a hole in poly)
+      data: any - application data (can hold color, e.g.)
     """
 
-    vmap = self.points.AddPoints(holepa.points)
-    holepoly = [ vmap[i] for i in holepa.poly ]
-    holepoly.reverse()
-    self.holes.append(holepoly)
+    def __init__(self, points=None, poly=None, holes=None, data=None):
+        self.points = points if points else Points()
+        self.poly = poly if poly else []
+        self.holes = holes if holes else []
+        self.data = data
 
-  def ContainsPoly(self, poly, points):
-    """Tests if poly is contained within self.poly.
+    def AddHole(self, holepa):
+        """Add a PolyArea's poly as a hole of self.
 
-    Args:
-      poly: list of int - indices into points
-      points: Points - maps to coords
-    Returns:
-      bool - True if poly is fully contained within self.poly
-    """
+        Need to reverse the contour and
+        adjust the the point indexes and self.points.
 
-    for v in poly:
-      if PointInside(points.pos[v], self.poly, self.points) == -1:
-        return False
-    return True
+        Args:
+          holepa: PolyArea
+        """
 
-  def Normal(self):
-    """Returns the normal of the polyarea's main poly."""
+        vmap = self.points.AddPoints(holepa.points)
+        holepoly = [vmap[i] for i in holepa.poly]
+        holepoly.reverse()
+        self.holes.append(holepoly)
 
-    pos = self.points.pos
-    poly = self.poly
-    if len(pos) == 0 or len(pos[0]) == 2 or len(poly) == 0:
-      print("whoops, not enough info to calculate normal")
-      return (0.0, 0.0, 1.0)
-    return Newell(poly, self.points)
+    def ContainsPoly(self, poly, points):
+        """Tests if poly is contained within self.poly.
 
+        Args:
+          poly: list of int - indices into points
+          points: Points - maps to coords
+        Returns:
+          bool - True if poly is fully contained within self.poly
+        """
 
-class PolyAreas(object):
-  """Contains a list of PolyAreas and a shared Points.
-
-  Attributes:
-    polyareas: list of PolyArea
-    points: Points
-  """
-
-  def __init__(self):
-    self.polyareas = []
-    self.points = Points()
-
-  def scale_and_center(self, scaled_side_target):
-    """Adjust the coordinates of the polyareas so that
-    it is centered at the origin and has its longest
-    dimension scaled to be scaled_side_target."""
-
-    if len(self.points.pos) == 0:
-      return
-    (minv, maxv) = self.bounds()
-    maxside = max([ maxv[i]-minv[i] for i in range(2) ])
-    if maxside > 0.0:
-      scale = scaled_side_target / maxside
-    else:
-      scale = 1.0
-    translate = [ -0.5*(maxv[i]+minv[i]) for i in range(2) ]
-    dim = len(self.points.pos[0])
-    if dim == 3:
-      translate.append([0.0])
-    for v in range(len(self.points.pos)):
-      self.points.pos[v] = tuple([ scale*(self.points.pos[v][i] + translate[i]) for i in range(dim) ])
+        for v in poly:
+            if PointInside(points.pos[v], self.poly, self.points) == -1:
+                return False
+        return True
 
-  def bounds(self):
-    """Find bounding box of polyareas in xy. 
+    def Normal(self):
+        """Returns the normal of the polyarea's main poly."""
 
-    Returns:
-      ([minx,miny],[maxx,maxy]) - all floats
-    """
+        pos = self.points.pos
+        poly = self.poly
+        if len(pos) == 0 or len(pos[0]) == 2 or len(poly) == 0:
+            print("whoops, not enough info to calculate normal")
+            return (0.0, 0.0, 1.0)
+        return Newell(poly, self.points)
 
-    huge = 1e100
-    minv = [huge, huge]
-    maxv = [-huge, -huge]
-    for pa in self.polyareas:
-      for face in [ pa.poly ] + pa.holes:
-        for v in face:
-          vcoords = self.points.pos[v]
-          for i in range(2):
-            if vcoords[i] < minv[i]:
-              minv[i] = vcoords[i]
-            if vcoords[i] > maxv[i]:
-              maxv[i] = vcoords[i]
-    if minv[0] == huge:
-      minv = [0.0, 0.0]
-    if maxv[0] == huge:
-      maxv = [0.0, 0.0]
-    return (minv, maxv)
 
+class PolyAreas(object):
+    """Contains a list of PolyAreas and a shared Points.
 
-class Model(object):
-  """Contains a generic 3d model.
+    Attributes:
+      polyareas: list of PolyArea
+      points: Points
+    """
 
-  A generic 3d model has vertices with 3d coordinates.
-  Each vertex gets a 'vertex id', which is an index that
-  can be used to refer to the vertex and can be used
-  to retrieve the 3d coordinates of the point.
+    def __init__(self):
+        self.polyareas = []
+        self.points = Points()
+
+    def scale_and_center(self, scaled_side_target):
+        """Adjust the coordinates of the polyareas so that
+        it is centered at the origin and has its longest
+        dimension scaled to be scaled_side_target."""
+
+        if len(self.points.pos) == 0:
+            return
+        (minv, maxv) = self.bounds()
+        maxside = max([maxv[i] - minv[i] for i in range(2)])
+        if maxside > 0.0:
+            scale = scaled_side_target / maxside
+        else:
+            scale = 1.0
+        translate = [-0.5 * (maxv[i] + minv[i]) for i in range(2)]
+        dim = len(self.points.pos[0])
+        if dim == 3:
+            translate.append([0.0])
+        for v in range(len(self.points.pos)):
+            self.points.pos[v] = tuple([scale * (self.points.pos[v][i] + \
+                translate[i]) for i in range(dim)])
+
+    def bounds(self):
+        """Find bounding box of polyareas in xy.
+
+        Returns:
+          ([minx,miny],[maxx,maxy]) - all floats
+        """
+
+        huge = 1e100
+        minv = [huge, huge]
+        maxv = [-huge, -huge]
+        for pa in self.polyareas:
+            for face in [pa.poly] + pa.holes:
+                for v in face:
+                    vcoords = self.points.pos[v]
+                    for i in range(2):
+                        if vcoords[i] < minv[i]:
+                            minv[i] = vcoords[i]
+                        if vcoords[i] > maxv[i]:
+                            maxv[i] = vcoords[i]
+        if minv[0] == huge:
+            minv = [0.0, 0.0]
+        if maxv[0] == huge:
+            maxv = [0.0, 0.0]
+        return (minv, maxv)
 
-  The actual visible part of the geometry are the faces,
-  which are n-gons (n>2), specified by a vector of the
-  n corner vertices.
-  Faces may also have colors.
 
-  Attributes:
-    points: geom.Points - the 3d vertices
-    faces: list of list of indices (each a CCW traversal of a face)
-    colors: list of (float, float, float) - if present, is parallel to 
-        faces list and gives rgb colors of faces
-  """
+class Model(object):
+    """Contains a generic 3d model.
+
+    A generic 3d model has vertices with 3d coordinates.
+    Each vertex gets a 'vertex id', which is an index that
+    can be used to refer to the vertex and can be used
+    to retrieve the 3d coordinates of the point.
+
+    The actual visible part of the geometry are the faces,
+    which are n-gons (n>2), specified by a vector of the
+    n corner vertices.
+    Faces may also have data associated with them,
+    and the data will be copied into newly created faces
+    from the most likely neighbor faces..
+
+    Attributes:
+      points: geom.Points - the 3d vertices
+      faces: list of list of indices (each a CCW traversal of a face)
+      face_data: list of any - if present, is parallel to
+          faces list and holds arbitrary data
+    """
 
-  def __init__(self):
-    self.points = Points()
-    self.faces = []
-    self.colors = []
+    def __init__(self):
+        self.points = Points()
+        self.faces = []
+        self.face_data = []
 
 
 class Art(object):
-  """Contains a vector art diagram.
+    """Contains a vector art diagram.
 
-  Attributes:
-    paths: list of Path objects
-  """
+    Attributes:
+      paths: list of Path objects
+    """
 
-  def __init__(self):
-    self.paths = []
+    def __init__(self):
+        self.paths = []
 
 
 class Paint(object):
-  """A color or pattern to fill or stroke with.
+    """A color or pattern to fill or stroke with.
 
-  For now, just do colors, but could later do
-  patterns or images too.
+    For now, just do colors, but could later do
+    patterns or images too.
 
-  Attributes:
-    color: (r,g,b) triple of floats, 0.0=no color, 1.0=max color
-  """
+    Attributes:
+      color: (r,g,b) triple of floats, 0.0=no color, 1.0=max color
+    """
 
-  def __init__(self, r=0.0, g=0.0, b=0.0):
-    self.color = (r, g, b)
+    def __init__(self, r=0.0, g=0.0, b=0.0):
+        self.color = (r, g, b)
 
-  @staticmethod
-  def CMYK(c, m, y, k):
-    """Return Paint specified in CMYK model.
+    @staticmethod
+    def CMYK(c, m, y, k):
+        """Return Paint specified in CMYK model.
 
-    Uses formula from 6.2.4 of PDF Reference.
+        Uses formula from 6.2.4 of PDF Reference.
 
-    Args:
-      c, m, y, k: float - in range [0, 1]
-    Returns:
-      Paint - with components in rgb form now
-    """
+        Args:
+          c, m, y, k: float - in range [0, 1]
+        Returns:
+          Paint - with components in rgb form now
+        """
 
-    return Paint(1.0 - min(1.0, c+k),
-        1.0 - min(1.0, m+k), 1.0 - min(1.0, y+k))
+        return Paint(1.0 - min(1.0, c + k),
+            1.0 - min(1.0, m + k), 1.0 - min(1.0, y + k))
 
 black_paint = Paint()
 white_paint = Paint(1.0, 1.0, 1.0)
 
 ColorDict = {
-  'aqua' : Paint(0.0, 1.0, 1.0),
-  'black' : Paint(0.0, 0.0, 0.0),
-  'blue' : Paint(0.0, 0.0, 1.0),
-  'fuchsia' : Paint(1.0, 0.0, 1.0),
-  'gray' : Paint(0.5, 0.5, 0.5),
-  'green' : Paint(0.0, 0.5, 0.0),
-  'lime' : Paint(0.0, 1.0, 0.0),
-  'maroon' : Paint(0.5, 0.0, 0.0),
-  'navy' : Paint(0.0, 0.0, 0.5),
-  'olive' : Paint(0.5, 0.5, 0.0),
-  'purple' : Paint(0.5, 0.0, 0.5),
-  'red' : Paint(1.0, 0.0, 0.0),
-  'silver' : Paint(0.75, 0.75, 0.75),
-  'teal' : Paint(0.0, 0.5, 0.5),
-  'white' : Paint(1.0, 1.0,1.0),
-  'yellow' : Paint(1.0, 1.0, 0.0)
+    'aqua': Paint(0.0, 1.0, 1.0),
+    'black': Paint(0.0, 0.0, 0.0),
+    'blue': Paint(0.0, 0.0, 1.0),
+    'fuchsia': Paint(1.0, 0.0, 1.0),
+    'gray': Paint(0.5, 0.5, 0.5),
+    'green': Paint(0.0, 0.5, 0.0),
+    'lime': Paint(0.0, 1.0, 0.0),
+    'maroon': Paint(0.5, 0.0, 0.0),
+    'navy': Paint(0.0, 0.0, 0.5),
+    'olive': Paint(0.5, 0.5, 0.0),
+    'purple': Paint(0.5, 0.0, 0.5),
+    'red': Paint(1.0, 0.0, 0.0),
+    'silver': Paint(0.75, 0.75, 0.75),
+    'teal': Paint(0.0, 0.5, 0.5),
+    'white': Paint(1.0, 1.0, 1.0),
+    'yellow': Paint(1.0, 1.0, 0.0)
 }
 
 
 class Path(object):
-  """Represents a path in the PDF sense, with painting instructions.
-
-  Attributes:
-    subpaths: list of Subpath objects
-    filled: True if path is to be filled
-    fillevenodd: True if use even-odd rule to fill (else non-zero winding)
-    stroked: True if path is to be stroked
-    fillpaint: Paint to fill with
-    strokepaint: Paint to stroke with
-  """
+    """Represents a path in the PDF sense, with painting instructions.
+
+    Attributes:
+      subpaths: list of Subpath objects
+      filled: True if path is to be filled
+      fillevenodd: True if use even-odd rule to fill (else non-zero winding)
+      stroked: True if path is to be stroked
+      fillpaint: Paint to fill with
+      strokepaint: Paint to stroke with
+    """
 
-  def __init__(self):
-     self.subpaths = []
-     self.filled = False
-     self.fillevenodd = False
-     self.stroked = False
-     self.fillpaint = black_paint
-     self.strokepaint = black_paint
+    def __init__(self):
+        self.subpaths = []
+        self.filled = False
+        self.fillevenodd = False
+        self.stroked = False
+        self.fillpaint = black_paint
+        self.strokepaint = black_paint
 
-  def AddSubpath(self, subpath):
-    """"Add a subpath."""
+    def AddSubpath(self, subpath):
+        """"Add a subpath."""
 
-    self.subpaths.append(subpath)
+        self.subpaths.append(subpath)
 
-  def Empty(self):
-    """Returns True if this Path as no subpaths."""
+    def Empty(self):
+        """Returns True if this Path as no subpaths."""
 
-    return not self.subpaths
+        return not self.subpaths
 
 
 class Subpath(object):
-  """Represents a subpath in PDF sense, either open or closed.
-
-  We'll represent lines, bezier pieces, circular arc pieces
-  as tuples with letters giving segment type in first position
-  and coordinates (2-tuples of floats) in the other positions.
-
-  Segment types:
-   ('L', a, b)       - line from a to b
-   ('B', a, b, c, d) - cubic bezier from a to b, with control points c,d
-   ('Q', a, b, c)    - quadratic bezier from a to b, with 1 control point c
-   ('A', a, b, rad, xrot, large-arc, ccw) - elliptical arc from a to b,
-     with rad=(rx, ry) as radii, xrot is x-axis rotation in degrees,
-     large-arc is True if arc should be >= 180 degrees,
-     ccw is True if start->end follows counter-clockwise direction
-     (see SVG spec); note that after rad,
-     the rest are floats or bools, not coordinate pairs
-  Note that s[1] and s[2] are the start and end points for any segment s.
-
-  Attributes:
-    segments: list of segment tuples (see above)
-    closed: True if closed
-  """
-
-  def __init__(self):
-    self.segments = []
-    self.closed = False
-
-  def Empty(self):
-    """Returns True if this subpath as no segments."""
-
-    return not self.segments
- 
-  def AddSegment(self, seg):
-    """Add a segment."""
-
-    self.segments.append(seg)
-
-  @staticmethod
-  def SegStart(s):
-    """Return start point for segment.
-
-    Args:
-      s: a segment tuple
-    Returns:
-      (float, float): the coordinates of the segment's start point
+    """Represents a subpath in PDF sense, either open or closed.
+
+    We'll represent lines, bezier pieces, circular arc pieces
+    as tuples with letters giving segment type in first position
+    and coordinates (2-tuples of floats) in the other positions.
+
+    Segment types:
+     ('L', a, b)       - line from a to b
+     ('B', a, b, c, d) - cubic bezier from a to b, with control points c,d
+     ('Q', a, b, c)    - quadratic bezier from a to b, with 1 control point c
+     ('A', a, b, rad, xrot, large-arc, ccw) - elliptical arc from a to b,
+       with rad=(rx, ry) as radii, xrot is x-axis rotation in degrees,
+       large-arc is True if arc should be >= 180 degrees,
+       ccw is True if start->end follows counter-clockwise direction
+       (see SVG spec); note that after rad,
+       the rest are floats or bools, not coordinate pairs
+    Note that s[1] and s[2] are the start and end points for any segment s.
+
+    Attributes:
+      segments: list of segment tuples (see above)
+      closed: True if closed
     """
 
-    return s[1]
+    def __init__(self):
+        self.segments = []
+        self.closed = False
 
-  @staticmethod
-  def SegEnd(s):
-    """Return end point for segment.
+    def Empty(self):
+        """Returns True if this subpath as no segments."""
 
-    Args:
-      s: a segment tuple
-    Returns:
-      (float, float): the coordinates of the segment's end point
-    """
+        return not self.segments
 
-    return s[2]
+    def AddSegment(self, seg):
+        """Add a segment."""
 
+        self.segments.append(seg)
 
-class TransformMatrix(object):
-  """Transformation matrix for 2d coordinates.
+    @staticmethod
+    def SegStart(s):
+        """Return start point for segment.
 
-  The transform matrix is:
-    [ a b 0 ]
-    [ c d 0 ]
-    [ e f 1 ]
-  and coordinate tranformation is defined by:
-    [x' y' 1] = [x y 1] x TransformMatrix
+        Args:
+          s: a segment tuple
+        Returns:
+          (float, float): the coordinates of the segment's start point
+        """
 
-  Attributes:
-    a, b, c, d, e, f: floats
-  """
+        return s[1]
 
-  def __init__(self, a=1.0, b=0.0, c=0.0, d=1.0, e=0.0, f=0.0):
-    self.a = a
-    self.b = b
-    self.c = c
-    self.d =d
-    self.e = e
-    self.f = f
+    @staticmethod
+    def SegEnd(s):
+        """Return end point for segment.
 
-  def __str__(self):
-    return str([self.a, self.b, self.c, self.d, self.e, self.f])
+        Args:
+          s: a segment tuple
+        Returns:
+          (float, float): the coordinates of the segment's end point
+        """
 
-  def Copy(self):
-    """Return a copy of this matrix."""
+        return s[2]
 
-    return TransformMatrix(self.a, self.b, self.c, self.d, self.e, self.f)
 
-  def ComposeTransform(self, a, b, c, d, e, f):
-    """Apply the transform given the the arguments on top of this one.
+class TransformMatrix(object):
+    """Transformation matrix for 2d coordinates.
 
-    This is accomplished by returning t x sel
-    where t is the transform matrix that would be formed from the args.
+    The transform matrix is:
+      [ a b 0 ]
+      [ c d 0 ]
+      [ e f 1 ]
+    and coordinate tranformation is defined by:
+      [x' y' 1] = [x y 1] x TransformMatrix
 
-    Arguments:
-      a, b, c, d, e, f: float - defines a composing TransformMatrix
+    Attributes:
+      a, b, c, d, e, f: floats
     """
 
-    newa = a * self.a + b*self.c
-    newb = a*self.b + b*self.d
-    newc = c*self.a + d*self.c
-    newd = c*self.b + d*self.d
-    newe = e*self.a + f*self.c + self.e
-    newf = e*self.b + f*self.d + self.f
-    self.a = newa
-    self.b = newb
-    self.c = newc
-    self.d = newd
-    self.e = newe
-    self.f = newf
-
-  def Apply(self, pt):
-    """Return the result of applying this tranform to pt = (x,y).
-
-    Arguments:
-      (x, y) : (float, float)
-    Returns:
-      (x', y'): 2-tuple of floats, the result of [x y 1] x self
-    """
+    def __init__(self, a=1.0, b=0.0, c=0.0, d=1.0, e=0.0, f=0.0):
+        self.a = a
+        self.b = b
+        self.c = c
+        self.d = d
+        self.e = e
+        self.f = f
 
-    (x, y) = pt
-    return (self.a*x + self.c*y + self.e, self.b*x + self.d*y + self.f)
+    def __str__(self):
+        return str([self.a, self.b, self.c, self.d, self.e, self.f])
+
+    def Copy(self):
+        """Return a copy of this matrix."""
+
+        return TransformMatrix(self.a, self.b, self.c, self.d, self.e, self.f)
+
+    def ComposeTransform(self, a, b, c, d, e, f):
+        """Apply the transform given the the arguments on top of this one.
+
+        This is accomplished by returning t x sel
+        where t is the transform matrix that would be formed from the args.
+
+        Arguments:
+          a, b, c, d, e, f: float - defines a composing TransformMatrix
+        """
+
+        newa = a * self.a + b * self.c
+        newb = a * self.b + b * self.d
+        newc = c * self.a + d * self.c
+        newd = c * self.b + d * self.d
+        newe = e * self.a + f * self.c + self.e
+        newf = e * self.b + f * self.d + self.f
+        self.a = newa
+        self.b = newb
+        self.c = newc
+        self.d = newd
+        self.e = newe
+        self.f = newf
+
+    def Apply(self, pt):
+        """Return the result of applying this tranform to pt = (x,y).
+
+        Arguments:
+          (x, y) : (float, float)
+        Returns:
+          (x', y'): 2-tuple of floats, the result of [x y 1] x self
+        """
+
+        (x, y) = pt
+        return (self.a * x + self.c * y + self.e, \
+            self.b * x + self.d * y + self.f)
 
 
 def ApproxEqualPoints(p, q):
-  """Return True if p and q are approximately the same points.
+    """Return True if p and q are approximately the same points.
 
-  Args:
-    p: n-tuple of float
-    q: n-tuple of float
-  Returns:
-    bool - True if the 1-norm <= DISTTOL
-  """
+    Args:
+      p: n-tuple of float
+      q: n-tuple of float
+    Returns:
+      bool - True if the 1-norm <= DISTTOL
+    """
 
-  for i in range(len(p)):
-    if abs(p[i] - q[i]) > DISTTOL:
-      return False
-    return True
+    for i in range(len(p)):
+        if abs(p[i] - q[i]) > DISTTOL:
+            return False
+        return True
 
 
 def PointInside(v, a, points):
-  """Return 1, 0, or -1 as v is inside, on, or outside polygon.
-
-  Cf. Eric Haines ptinpoly in Graphics Gems IV.
-
-  Args:
-    v : (float, float) or (float, float, float) - coordinates of a point
-    a : list of vertex indices defining polygon (assumed CCW)
-    points: Points - to get coordinates for polygon
-  Returns:
-    1, 0, -1: as v is inside, on, or outside polygon a
-  """
-
-  (xv, yv) = (v[0], v[1])
-  vlast = points.pos[a[-1]]
-  (x0, y0) = (vlast[0], vlast[1])
-  if x0 == xv and y0 == yv:
-    return 0
-  yflag0 = y0 > yv
-  inside = False
-  n = len(a)
-  for i in range(0, n):
-    vi = points.pos[a[i]]
-    (x1, y1) = (vi[0], vi[1])
-    if x1 == xv and y1 == yv:
-      return 0
-    yflag1 = y1 > yv
-    if yflag0 != yflag1:
-      xflag0 = x0 > xv
-      xflag1 = x1 > xv
-      if xflag0 == xflag1:
-        if xflag0:
-          inside = not inside
-      else:
-        z = x1 - (y1-yv)*(x0-x1)/(y0-y1)
-        if z >= xv:
-          inside = not inside
-    x0 = x1
-    y0 = y1
-    yflag0 = yflag1
-  if inside:
-    return 1
-  else:
-    return -1
+    """Return 1, 0, or -1 as v is inside, on, or outside polygon.
+
+    Cf. Eric Haines ptinpoly in Graphics Gems IV.
+
+    Args:
+      v : (float, float) or (float, float, float) - coordinates of a point
+      a : list of vertex indices defining polygon (assumed CCW)
+      points: Points - to get coordinates for polygon
+    Returns:
+      1, 0, -1: as v is inside, on, or outside polygon a
+    """
+
+    (xv, yv) = (v[0], v[1])
+    vlast = points.pos[a[-1]]
+    (x0, y0) = (vlast[0], vlast[1])
+    if x0 == xv and y0 == yv:
+        return 0
+    yflag0 = y0 > yv
+    inside = False
+    n = len(a)
+    for i in range(0, n):
+        vi = points.pos[a[i]]
+        (x1, y1) = (vi[0], vi[1])
+        if x1 == xv and y1 == yv:
+            return 0
+        yflag1 = y1 > yv
+        if yflag0 != yflag1:
+            xflag0 = x0 > xv
+            xflag1 = x1 > xv
+            if xflag0 == xflag1:
+                if xflag0:
+                    inside = not inside
+            else:
+                z = x1 - (y1 - yv) * (x0 - x1) / (y0 - y1)
+                if z >= xv:
+                    inside = not inside
+        x0 = x1
+        y0 = y1
+        yflag0 = yflag1
+    if inside:
+        return 1
+    else:
+        return -1
 
 
 def SignedArea(polygon, points):
-  """Return the area of the polgon, positive if CCW, negative if CW.
+    """Return the area of the polgon, positive if CCW, negative if CW.
 
-  Args:
-    polygon: list of vertex indices
-    points: Points
-  Returns:
-    float - area of polygon, positive if it was CCW, else negative
-  """
+    Args:
+      polygon: list of vertex indices
+      points: Points
+    Returns:
+      float - area of polygon, positive if it was CCW, else negative
+    """
 
-  a = 0.0
-  n = len(polygon)
-  for i in range(0, n):
-    u = points.pos[polygon[i]]
-    v = points.pos[polygon[(i+1) % n]]
-    a += u[0]*v[1] - u[1]*v[0]
-  return 0.5*a
+    a = 0.0
+    n = len(polygon)
+    for i in range(0, n):
+        u = points.pos[polygon[i]]
+        v = points.pos[polygon[(i + 1) % n]]
+        a += u[0] * v[1] - u[1] * v[0]
+    return 0.5 * a
 
 
 def VecAdd(a, b):
-  """Return vector a-b.
+    """Return vector a-b.
 
-  Args:
-    a: n-tuple of floats
-    b: n-tuple of floats
-  Returns:
-    n-tuple of floats - pairwise addition a+b
-  """
+    Args:
+      a: n-tuple of floats
+      b: n-tuple of floats
+    Returns:
+      n-tuple of floats - pairwise addition a+b
+    """
 
-  n = len(a)
-  assert(n == len(b))
-  return tuple([ a[i]+b[i] for i in range(n)])
+    n = len(a)
+    assert(n == len(b))
+    return tuple([a[i] + b[i] for i in range(n)])
 
 
 def VecSub(a, b):
-  """Return vector a-b.
+    """Return vector a-b.
+
+    Args:
+      a: n-tuple of floats
+      b: n-tuple of floats
+    Returns:
+      n-tuple of floats - pairwise subtraction a-b
+    """
 
-  Args:
-    a: n-tuple of floats
-    b: n-tuple of floats
-  Returns:
-    n-tuple of floats - pairwise subtraction a-b
-  """
+    n = len(a)
+    assert(n == len(b))
+    return tuple([a[i] - b[i] for i in range(n)])
+
+
+def VecDot(a, b):
+    """Return the dot product of two vectors.
+
+    Args:
+      a: n-tuple of floats
+      b: n-tuple of floats
+    Returns:
+      n-tuple of floats - dot product of a and b
+    """
 
-  n = len(a)
-  assert(n == len(b))
-  return tuple([ a[i]-b[i] for i in range(n)])
+    n = len(a)
+    assert(n == len(b))
+    sum = 0.0
+    for i in range(n):
+        sum += a[i] * b[i]
+    return sum
 
 
 def VecLen(a):
-  """Return the Euclidean lenght of the argument vector.
+    """Return the Euclidean lenght of the argument vector.
 
-  Args:
-    a: n-tuple of floats
-  Returns:
-    float: the 2-norm of a
-  """
+    Args:
+      a: n-tuple of floats
+    Returns:
+      float: the 2-norm of a
+    """
 
-  s = 0.0
-  for v in a:
-    s += v*v
-  return math.sqrt(s)
+    s = 0.0
+    for v in a:
+        s += v * v
+    return math.sqrt(s)
 
 
 def Newell(poly, points):
-  """Use Newell method to find polygon normal.
-
-  Assume poly has length at least 3 and points are 3d.
-
-  Args:
-    poly: list of int - indices into points.pos
-    points: Points - assumed 3d
-  Returns:
-    (float, float, float) - the average normal
-  """
-
-  sumx = 0.0
-  sumy = 0.0
-  sumz = 0.0
-  n = len(poly)
-  pos = points.pos
-  for i, ai in enumerate(poly):
-    bi = poly[(i+1) % n]
-    a = pos[ai]
-    b = pos[bi]
-    sumx += (a[1]-b[1]) * (a[2]+b[2])
-    sumy += (a[2]-b[2]) * (a[0]+b[0])
-    sumz += (a[0]-b[0]) * (a[1]+b[1])
-  return Norm3(sumx, sumy, sumz)
+    """Use Newell method to find polygon normal.
+
+    Assume poly has length at least 3 and points are 3d.
+
+    Args:
+      poly: list of int - indices into points.pos
+      points: Points - assumed 3d
+    Returns:
+      (float, float, float) - the average normal
+    """
+
+    sumx = 0.0
+    sumy = 0.0
+    sumz = 0.0
+    n = len(poly)
+    pos = points.pos
+    for i, ai in enumerate(poly):
+        bi = poly[(i + 1) % n]
+        a = pos[ai]
+        b = pos[bi]
+        sumx += (a[1] - b[1]) * (a[2] + b[2])
+        sumy += (a[2] - b[2]) * (a[0] + b[0])
+        sumz += (a[0] - b[0]) * (a[1] + b[1])
+    return Norm3(sumx, sumy, sumz)
 
 
 def Norm3(x, y, z):
-  """Return vector (x,y,z) normalized by dividing by squared length.
-  Return (0.0, 0.0, 1.0) if the result is undefined."""
-  sqrlen = x * x + y * y + z * z
-  if sqrlen < 1e-100:
-    return (0.0, 0.0, 1.0)
-  else:
-    try:
-      d = math.sqrt(sqrlen)
-      return (x / d, y / d, z / d)
-    except:
-      return (0.0, 0.0, 1.0)
+    """Return vector (x,y,z) normalized by dividing by squared length.
+    Return (0.0, 0.0, 1.0) if the result is undefined."""
+    sqrlen = x * x + y * y + z * z
+    if sqrlen < 1e-100:
+        return (0.0, 0.0, 1.0)
+    else:
+        try:
+            d = math.sqrt(sqrlen)
+            return (x / d, y / d, z / d)
+        except:
+            return (0.0, 0.0, 1.0)
 
 
 # We're using right-hand coord system, where
 # forefinger=x, middle=y, thumb=z on right hand.
 # Then, e.g., (1,0,0) x (0,1,0) = (0,0,1)
 def Cross3(a, b):
-  """Return the cross product of two vectors, a x b."""
+    """Return the cross product of two vectors, a x b."""
 
-  (ax, ay, az) = a
-  (bx, by, bz) = b
-  return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
+    (ax, ay, az) = a
+    (bx, by, bz) = b
+    return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
 
 
 def MulPoint3(p, m):
-  """Return matrix multiplication of p times m
-  where m is a 4x3 matrix and p is a 3d point, extended with 1."""
-
-  (x, y, z) = p
-  return (x * m[0] + y * m[3] + z * m[6] + m[9],
-      x * m[1] + y * m[4] + z * m[7] + m[10],
-      x * m[2] + y * m[5] + z * m[8] + m[11])
+    """Return matrix multiplication of p times m
+    where m is a 4x3 matrix and p is a 3d point, extended with 1."""
 
+    (x, y, z) = p
+    return (x * m[0] + y * m[3] + z * m[6] + m[9],
+        x * m[1] + y * m[4] + z * m[7] + m[10],
+        x * m[2] + y * m[5] + z * m[8] + m[11])
diff --git a/mesh_inset/model.py b/mesh_inset/model.py
index 4e3b0f664211205db385fb238bb936e9eaa5097a..a3eb2aacc5d68141459a608a145e44612f4c6a2c 100644
--- a/mesh_inset/model.py
+++ b/mesh_inset/model.py
@@ -16,6 +16,8 @@
 #
 # ##### END GPL LICENSE BLOCK #####
 
+# <pep8 compliant>
+
 """Manipulations of Models.
 """
 
@@ -28,480 +30,546 @@ import math
 
 
 def PolyAreasToModel(polyareas, bevel_amount, bevel_pitch, quadrangulate):
-  """Convert a PolyAreas into a Model object.
+    """Convert a PolyAreas into a Model object.
 
-  Assumes polyareas are in xy plane.
+    Assumes polyareas are in xy plane.
 
-  Args:
-    polyareas: geom.PolyAreas
-    bevel_amount: float - if > 0, amount of bevel
-    bevel_pitch: float - if > 0, angle in radians of bevel
-    quadrangulate: bool - should n-gons be quadrangulated?
-  Returns:
-    geom.Model
-  """
+    Args:
+      polyareas: geom.PolyAreas
+      bevel_amount: float - if > 0, amount of bevel
+      bevel_pitch: float - if > 0, angle in radians of bevel
+      quadrangulate: bool - should n-gons be quadrangulated?
+    Returns:
+      geom.Model
+    """
 
-  m = geom.Model()
-  if not polyareas:
+    m = geom.Model()
+    if not polyareas:
+        return m
+    polyareas.points.AddZCoord(0.0)
+    m.points = polyareas.points
+    for pa in polyareas.polyareas:
+        PolyAreaToModel(m, pa, bevel_amount, bevel_pitch, quadrangulate)
     return m
-  polyareas.points.AddZCoord(0.0)
-  m.points = polyareas.points
-  for pa in polyareas.polyareas:
-    PolyAreaToModel(m, pa, bevel_amount, bevel_pitch, quadrangulate)
-  return m
 
 
 def PolyAreaToModel(m, pa, bevel_amount, bevel_pitch, quadrangulate):
-  if bevel_amount > 0.0:
-    BevelPolyAreaInModel(m, pa, bevel_amount, bevel_pitch, quadrangulate)
-  elif quadrangulate:
-    if len(pa.poly) == 0:
-      return
-    qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes, pa.points)
-    m.faces.extend(qpa)
-    m.colors.extend([ pa.color ] * len(qpa))
-  else:
-    m.faces.append(pa.poly)
-    # TODO: just the first part of QuadrangulateFaceWithHoles, to join
-    # holes to outer poly
-    m.colors.append(pa.color)
+    if bevel_amount > 0.0:
+        BevelPolyAreaInModel(m, pa, bevel_amount, bevel_pitch, quadrangulate,
+            False)
+    elif quadrangulate:
+        if len(pa.poly) == 0:
+            return
+        qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes, pa.points)
+        m.faces.extend(qpa)
+        m.face_data.extend([pa.data] * len(qpa))
+    else:
+        m.faces.append(pa.poly)
+        # TODO: just the first part of QuadrangulateFaceWithHoles, to join
+        # holes to outer poly
+        m.face_data.append(pa.data)
 
 
 def ExtrudePolyAreasInModel(mdl, polyareas, depth, cap_back):
-  """Extrude the boundaries given by polyareas by -depth in z.
-
-  Assumes polyareas are in xy plane.
-
-  Arguments:
-    mdl: geom.Model - where to do extrusion
-    polyareas: geom.Polyareas
-    depth: float
-    cap_back: bool - if True, cap off the back
-  Side Effects:
-    For all edges in polys in polyareas, make quads in Model
-    extending those edges by depth in the negative z direction.
-    The color will be the color of the face that the edge is part of.
-  """
-
-  for pa in polyareas.polyareas:
-    back_poly = _ExtrudePoly(mdl, pa.poly, depth, pa.color, True)
-    back_holes = []
-    for p in pa.holes:
-      back_holes.append(_ExtrudePoly(mdl, p, depth, pa.color, False))
-    if cap_back:
-      qpa = triquad.QuadrangulateFaceWithHoles(back_poly, back_holes,
-        polyareas.points)
-      # need to reverse each poly to get normals pointing down
-      for i, p in enumerate(qpa):
-        t = list(p)
-        t.reverse()
-        qpa[i] = tuple(t)
-      model.faces.extend(qpa)
-      model.colors.extend([pa.color] * len(qpa))
-
-
-def _ExtrudePoly(mdl, poly, depth, color, isccw):
-  """Extrude the poly by -depth in z
-
-  Arguments:
-    mdl: geom.Model - where to do extrusion
-    poly: list of vertex indices
-    depth: float
-    color: tuple of three floats
-    isccw: True if counter-clockwise
-  Side Effects
-    For all edges in poly, make quads in Model
-    extending those edges by depth in the negative z direction.
-    The color will be the color of the face that the edge is part of.
-  Returns:
-    list of int - vertices for extruded poly
-  """
-
-  if len(poly) < 2:
-    return
-  extruded_poly = []
-  points = mdl.points
-  if isccw:
-    incr = 1
-  else:
-    incr = -1
-  for i, v in enumerate(poly):
-    vnext = poly[(i+incr) % len(poly)]
-    (x0,y0,z0) = points.pos[v]
-    (x1,y1,z1) = points.pos[vnext]
-    vextrude = points.AddPoint((x0,y0,z0-depth))
-    vnextextrude = points.AddPoint((x1,y1,z1-depth))
+    """Extrude the boundaries given by polyareas by -depth in z.
+
+    Assumes polyareas are in xy plane.
+
+    Arguments:
+      mdl: geom.Model - where to do extrusion
+      polyareas: geom.Polyareas
+      depth: float
+      cap_back: bool - if True, cap off the back
+    Side Effects:
+      For all edges in polys in polyareas, make quads in Model
+      extending those edges by depth in the negative z direction.
+      The application data will be the data of the face that the edge
+      is part of.
+    """
+
+    for pa in polyareas.polyareas:
+        back_poly = _ExtrudePoly(mdl, pa.poly, depth, pa.data, True)
+        back_holes = []
+        for p in pa.holes:
+            back_holes.append(_ExtrudePoly(mdl, p, depth, pa.data, False))
+        if cap_back:
+            qpa = triquad.QuadrangulateFaceWithHoles(back_poly, back_holes,
+              polyareas.points)
+            # need to reverse each poly to get normals pointing down
+            for i, p in enumerate(qpa):
+                t = list(p)
+                t.reverse()
+                qpa[i] = tuple(t)
+            mdl.faces.extend(qpa)
+            mdl.face_data.extend([pa.data] * len(qpa))
+
+
+def _ExtrudePoly(mdl, poly, depth, data, isccw):
+    """Extrude the poly by -depth in z
+
+    Arguments:
+      mdl: geom.Model - where to do extrusion
+      poly: list of vertex indices
+      depth: float
+      data: application data
+      isccw: True if counter-clockwise
+    Side Effects
+      For all edges in poly, make quads in Model
+      extending those edges by depth in the negative z direction.
+      The application data will be the data of the face that the edge
+      is part of.
+    Returns:
+      list of int - vertices for extruded poly
+    """
+
+    if len(poly) < 2:
+        return
+    extruded_poly = []
+    points = mdl.points
     if isccw:
-      sideface = [v, vextrude, vnextextrude, vnext]
+        incr = 1
     else:
-      sideface = [v, vnext, vnextextrude, vextrude]
-    mdl.faces.append(sideface)
-    mdl.colors.append(color)
-    extruded_poly.append(vextrude)
-  return extruded_poly
+        incr = -1
+    for i, v in enumerate(poly):
+        vnext = poly[(i + incr) % len(poly)]
+        (x0, y0, z0) = points.pos[v]
+        (x1, y1, z1) = points.pos[vnext]
+        vextrude = points.AddPoint((x0, y0, z0 - depth))
+        vnextextrude = points.AddPoint((x1, y1, z1 - depth))
+        if isccw:
+            sideface = [v, vextrude, vnextextrude, vnext]
+        else:
+            sideface = [v, vnext, vnextextrude, vextrude]
+        mdl.faces.append(sideface)
+        mdl.face_data.append(data)
+        extruded_poly.append(vextrude)
+    return extruded_poly
 
 
 def BevelPolyAreaInModel(mdl, polyarea,
-    bevel_amount, bevel_pitch, quadrangulate):
-  """Bevel the interior of polyarea in model.
-
-  This does smart beveling: advancing edges are merged
-  rather than doing an 'overlap'.  Advancing edges that
-  hit an opposite edge result in a split into two beveled areas.
-
-  If the polyarea is not in the xy plane, do the work in a
-  transformed model, and then transfer the changes back.
-
-  Arguments:
-    mdl: geom.Model - where to do bevel
-    polyarea geom.PolyArea - area to bevel into
-    bevel_amount: float - if > 0, amount of bevel
-    bevel_pitch: float - if > 0, angle in radians of bevel
-    quadrangulate: bool - should n-gons be quadrangulated?
-  Side Effects:
-    Faces and points are added to model to model the
-    bevel and the interior of the polyareas.
-  """
-
-  pa_norm = polyarea.Normal()
-  if pa_norm == (0.0, 0.0, 1.0):
-    m = mdl
-    pa_rot = polyarea
-  else:
-    (pa_rot, inv_rot, inv_map) = _RotatedPolyAreaToXY(polyarea, pa_norm)
-    # don't actually have to add the original faces into model, just their points.
-    m = geom.Model()
-    m.points = pa_rot.points
-  vspeed = math.tan(bevel_pitch)
-  off = offset.Offset(pa_rot, 0.0, vspeed)
-  off.Build(bevel_amount)
-  inner_pas = AddOffsetFacesToModel(m, off, polyarea.color)
-  for pa in inner_pas.polyareas:
-    if quadrangulate:
-      if len(pa.poly) == 0:
-        continue
-      qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes, pa.points)
-      m.faces.extend(qpa)
-      m.colors.extend([ pa.color ] * len(qpa))
+    bevel_amount, bevel_pitch, quadrangulate, as_percent):
+    """Bevel the interior of polyarea in model.
+
+    This does smart beveling: advancing edges are merged
+    rather than doing an 'overlap'.  Advancing edges that
+    hit an opposite edge result in a split into two beveled areas.
+
+    If the polyarea is not in the xy plane, do the work in a
+    transformed model, and then transfer the changes back.
+
+    Arguments:
+      mdl: geom.Model - where to do bevel
+      polyarea geom.PolyArea - area to bevel into
+      bevel_amount: float - if > 0, amount of bevel
+      bevel_pitch: float - if > 0, angle in radians of bevel
+      quadrangulate: bool - should n-gons be quadrangulated?
+      as_percent: bool - if True, interpret amount as percent of max
+    Side Effects:
+      Faces and points are added to model to model the
+      bevel and the interior of the polyareas.
+    """
+
+    pa_norm = polyarea.Normal()
+    if pa_norm == (0.0, 0.0, 1.0):
+        m = mdl
+        pa_rot = polyarea
     else:
-      m.faces.append(pa.poly)
-      m.colors.append(pa.color)
-  if m != mdl:
-    _AddTransformedPolysToModel(mdl, m.faces, m.points, inv_rot, inv_map)
-
-
-def AddOffsetFacesToModel(mdl, off, color = (0.0, 0.0, 0.0)):
-  """Add the faces due to an offset into model.
-
-  Returns the remaining interiors of the offset as a PolyAreas.
-
-  Args:
-    mdl: geom.Model - model to add offset faces into
-    off: offset.Offset
-    color: (float, float, float) - color to make the faces
-  Returns:
-    geom.PolyAreas
-  """
-
-  mdl.points = off.polyarea.points
-  assert(len(mdl.points.pos) == 0 or len(mdl.points.pos[0]) == 3)
-  o = off
-  ostack = [ ]
-  while o:
-    if o.endtime != 0.0:
-      for face in o.facespokes:
-        n = len(face)
-        for i, spoke in enumerate(face):
-          nextspoke = face[(i+1) % n]
-          v0 = spoke.origin
-          v1 = nextspoke.origin
-          v2 = nextspoke.dest
-          v3 = spoke.dest
-          if v2 == v3:
-            mface = [v0, v1, v2]
-          else:
-            mface = [v0, v1, v2, v3]
-          mdl.faces.append(mface)
-          mdl.colors.append(color)
-    ostack.extend(o.inneroffsets)
-    if ostack:
-      o = ostack.pop()
+        (pa_rot, inv_rot, inv_map) = _RotatedPolyAreaToXY(polyarea, pa_norm)
+        # don't have to add the original faces into model, just their points.
+        m = geom.Model()
+        m.points = pa_rot.points
+    vspeed = math.tan(bevel_pitch)
+    off = offset.Offset(pa_rot, 0.0, vspeed)
+    if as_percent:
+        bevel_amount = bevel_amount * off.MaxAmount() / 100.0
+    off.Build(bevel_amount)
+    inner_pas = AddOffsetFacesToModel(m, off, polyarea.data)
+    for pa in inner_pas.polyareas:
+        if quadrangulate:
+            if len(pa.poly) == 0:
+                continue
+            qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes,
+                pa.points)
+            m.faces.extend(qpa)
+            m.face_data.extend([pa.data] * len(qpa))
+        else:
+            m.faces.append(pa.poly)
+            m.face_data.append(pa.data)
+    if m != mdl:
+        _AddTransformedPolysToModel(mdl, m.faces, m.points, m.face_data,
+             inv_rot, inv_map)
+
+
+def AddOffsetFacesToModel(mdl, off, data=None):
+    """Add the faces due to an offset into model.
+
+    Returns the remaining interiors of the offset as a PolyAreas.
+
+    Args:
+      mdl: geom.Model - model to add offset faces into
+      off: offset.Offset
+      data: any - application data to be copied to the faces
+    Returns:
+      geom.PolyAreas
+    """
+
+    mdl.points = off.polyarea.points
+    assert(len(mdl.points.pos) == 0 or len(mdl.points.pos[0]) == 3)
+    o = off
+    ostack = []
+    while o:
+        if o.endtime != 0.0:
+            for face in o.facespokes:
+                n = len(face)
+                for i, spoke in enumerate(face):
+                    nextspoke = face[(i + 1) % n]
+                    v0 = spoke.origin
+                    v1 = nextspoke.origin
+                    v2 = nextspoke.dest
+                    v3 = spoke.dest
+                    if v2 == v3:
+                        mface = [v0, v1, v2]
+                    else:
+                        mface = [v0, v1, v2, v3]
+                    mdl.faces.append(mface)
+                    mdl.face_data.append(data)
+        ostack.extend(o.inneroffsets)
+        if ostack:
+            o = ostack.pop()
+        else:
+            o = None
+    return off.InnerPolyAreas()
+
+
+def BevelSelectionInModel(mdl, bevel_amount, bevel_pitch, quadrangulate,
+        as_region, as_percent):
+    """Bevel all the faces in the model, perhaps as one region.
+
+    If as_region is False, each face is beveled individually,
+    otherwise regions of contiguous faces are merged into
+    PolyAreas and beveled as a whole.
+
+    TODO: something if extracted PolyAreas are not approximately
+    planar.
+
+    Args:
+      mdl: geom.Model
+      bevel_amount: float - amount to inset
+      bevel_pitch: float - angle of bevel side
+      quadrangulate: bool - should insides be quadrangulated?
+      as_region: bool - should faces be merged into regions?
+      as_percent: bool - should amount be interpreted as a percent
+          of the maximum amount (if True) or an absolute amount?
+    Side effect:
+      Beveling faces will be added to the model
+    """
+
+    pas = []
+    if as_region:
+        pas = RegionToPolyAreas(mdl.faces, mdl.points, mdl.face_data)
     else:
-      o = None
-  return off.InnerPolyAreas()
-
-
-def BevelSelectionInModel(mdl, selected_faces,
-    bevel_amount, bevel_pitch, quadrangulate, as_region):
-  """Bevel the selected faces in the model.
-
-  If as_region is False, each face is beveled individually,
-  otherwise regions of contiguous faces are merged into
-  PolyAreas and beveled as a whole.
-
-  TODO: something if extracted PolyAreas are not approximately
-  planar.
-
-  Args:
-    mdl: geom.Model
-    selected_faces: list of list of int
-    bevel_amount: float - amount to inset
-    bevel_pitch: float - angle of bevel side
-    quadrangulate: bool - should insides be quadrangulated?
-    as_region: bool - should faces be merged into regions?
-  Side effect:
-    Beveling faces will be added to the model
-  """
-
-  pas = []
-  if as_region:
-    pas = RegionToPolyAreas(selected_faces, mdl.points)
-  else:
-    for face in selected_faces:
-      pas.append(geom.PolyArea(mdl.points, face))
-  for pa in pas:
-    BevelPolyAreaInModel(mdl, pa,
-        bevel_amount, bevel_pitch, quadrangulate)
-
-
-def RegionToPolyAreas(faces, points):
-  """Find polygonal outlines induced by union of faces.
-
-  Finds the polygons formed by boundary edges (those not
-  sharing an edge with another face in region_faces), and
-  turns those into PolyAreas.
-  In the general case, there will be holes inside.
-
-  Args:
-    faces: list of list of int - each sublist is a face (indices into points)
-    points: geom.Points - gives coordinates for vertices
-  Returns:
-    list of geom.PolyArea
-  """
-
-  ans = []
-  (edges, vtoe) = _GetEdgeData(faces)
-  (face_adj, is_interior_edge) = _GetFaceGraph(faces, edges, vtoe, points)
-  (components, ftoc) = _FindFaceGraphComponents(faces, face_adj)
-  for c in range(len(components)):
-    boundary_edges = set()
-    vstobe = dict()
-    for e, ((vs, ve), f) in enumerate(edges):
-      if ftoc[f] != c or is_interior_edge[e]:
-        continue
-      boundary_edges.add(e)
-      vstobe[vs] = e
-    polys = []
-    while boundary_edges:
-      e = boundary_edges.pop()
-      ((vstart, ve), _) = edges[e]
-      poly = [ vstart, ve ]
-      while ve != vstart:
-        if ve not in vstobe:
-          print("whoops, couldn't close boundary")
-          break
-        nexte = vstobe[ve]
-        ((_, ve), _) = edges[nexte]
-        boundary_edges.remove(nexte)
-        if ve != vstart:
-          poly.append(ve)
-      polys.append(poly)
-    if len(polys) == 0:
-      # can happen if an entire closed polytope is given
-      # at least until we do an edge check
-      return []
-    elif len(polys) == 1:
-      ans.append(geom.PolyArea(points, polys[0]))
-    else:
-      outerf = _FindOuterPoly(polys, points)
-      pa = geom.PolyArea(points, polys[outerf])
-      pa.holes = [ polys[i] for i in range(len(polys)) if i != outerf ]
-      ans.append(pa)
-  return ans
+        for f, face in enumerate(mdl.faces):
+            pas.append(geom.PolyArea(mdl.points, face, [],
+                mdl.face_data[f]))
+    for pa in pas:
+        BevelPolyAreaInModel(mdl, pa,
+            bevel_amount, bevel_pitch, quadrangulate, as_percent)
+
+
+def RegionToPolyAreas(faces, points, data):
+    """Find polygonal outlines induced by union of faces.
+
+    Finds the polygons formed by boundary edges (those not
+    sharing an edge with another face in region_faces), and
+    turns those into PolyAreas.
+    In the general case, there will be holes inside.
+    We want to associate data with the region PolyAreas.
+    Just choose a representative element of data[] when
+    more than one face is combined into a PolyArea.
+
+    Args:
+      faces: list of list of int - each sublist is a face (indices into points)
+      points: geom.Points - gives coordinates for vertices
+      data: list of any - parallel to faces, app data to put in PolyAreas
+    Returns:
+      list of geom.PolyArea
+    """
+
+    ans = []
+    (edges, vtoe) = _GetEdgeData(faces)
+    (face_adj, is_interior_edge) = _GetFaceGraph(faces, edges, vtoe, points)
+    (components, ftoc) = _FindFaceGraphComponents(faces, face_adj)
+    for c in range(len(components)):
+        boundary_edges = set()
+        betodata = dict()
+        vstobe = dict()
+        for e, ((vs, ve), f) in enumerate(edges):
+            if ftoc[f] != c or is_interior_edge[e]:
+                continue
+            boundary_edges.add(e)
+            # vstobe[v] is set of edges leaving v
+            # (could be more than one if boundary touches itself at a vertex)
+            if vs in vstobe:
+                vstobe[vs].append(e)
+            else:
+                vstobe[vs] = [e]
+            betodata[(vs, ve)] = data[f]
+        polys = []
+        poly_data = []
+        while boundary_edges:
+            e = boundary_edges.pop()
+            ((vstart, ve), face_i) = edges[e]
+            poly = [vstart, ve]
+            datum = betodata[(vstart, ve)]
+            while ve != vstart:
+                if ve not in vstobe:
+                    print("whoops, couldn't close boundary")
+                    break
+                nextes = vstobe[ve]
+                if len(nextes) == 1:
+                    nexte = nextes[0]
+                else:
+                    # find a next edge with face index face_i
+                    # TODO: this is not guaranteed to work,
+                    # as continuation edge may have been for a different
+                    # face that is now combined with face_i by erasing
+                    # interior edges. Find a better algorithm here.
+                    nexte = -1
+                    for ne_cand in nextes:
+                        if edges[ne_cand][1] == face_i:
+                            nexte = ne_cand
+                            break
+                    if nexte == -1:
+                        # case mentioned in TODO may have happened;
+                        # just choose any nexte - may mess things up
+                        nexte = nextes[0]
+                ((_, ve), face_i) = edges[nexte]
+                if nexte not in boundary_edges:
+                    print("whoops, nexte not a boundary edge", nexte)
+                    break
+                boundary_edges.remove(nexte)
+                if ve != vstart:
+                    poly.append(ve)
+            polys.append(poly)
+            poly_data.append(datum)
+        if len(polys) == 0:
+            # can happen if an entire closed polytope is given
+            # at least until we do an edge check
+            return []
+        elif len(polys) == 1:
+            ans.append(geom.PolyArea(points, polys[0], [], poly_data[0]))
+        else:
+            outerf = _FindOuterPoly(polys, points, faces)
+            pa = geom.PolyArea(points, polys[outerf], [], poly_data[outerf])
+            pa.holes = [polys[i] for i in range(len(polys)) if i != outerf]
+            ans.append(pa)
+    return ans
 
 
 def _GetEdgeData(faces):
-  """Find edges from faces, and some lookup dictionaries.
-
-  Args:
-    faces: list of list of int - each a closed CCW polygon of vertex indices
-  Returns:
-    (list of ((int, int), int), dict{ int->list of int}) -
-      list elements are ((startv, endv), face index)
-      dict maps vertices to edge indices
-  """
-
-  edges = []
-  vtoe = dict()
-  for findex, f in enumerate(faces):
-    nf = len(f)
-    for i, v in enumerate(f):
-      endv = f[(i+1) % nf]
-      edges.append(((v, endv), findex))
-      eindex = len(edges)-1
-      if v in vtoe:
-        vtoe[v].append(eindex)
-      else:
-        vtoe[v] = [ eindex ]
-  return (edges, vtoe)
+    """Find edges from faces, and some lookup dictionaries.
+
+    Args:
+      faces: list of list of int - each a closed CCW polygon of vertex indices
+    Returns:
+      (list of ((int, int), int), dict{ int->list of int}) -
+        list elements are ((startv, endv), face index)
+        dict maps vertices to edge indices
+    """
+
+    edges = []
+    vtoe = dict()
+    for findex, f in enumerate(faces):
+        nf = len(f)
+        for i, v in enumerate(f):
+            endv = f[(i + 1) % nf]
+            edges.append(((v, endv), findex))
+            eindex = len(edges) - 1
+            if v in vtoe:
+                vtoe[v].append(eindex)
+            else:
+                vtoe[v] = [eindex]
+    return (edges, vtoe)
 
 
 def _GetFaceGraph(faces, edges, vtoe, points):
-  """Find the face adjacency graph.
-
-  Faces are adjacent if they share an edge,
-  and the shared edge goes in the reverse direction,
-  and if the angle between them isn't too large.
-
-  Args:
-    faces: list of list of int
-    edges: list of ((int, int), int) - see _GetEdgeData
-    vtoe: dict{ int->list of int } - see _GetEdgeData
-    points: geom.Points
-  Returns:
-    (list of  list of int, list of bool) -
-      first list: each sublist is adjacent face indices for each face
-      second list: maps edge index to True if it separates adjacent faces
-  """
-
-  face_adj = [ [] for i in range(len(faces)) ]
-  is_interior_edge = [ False ] * len(edges)
-  for e, ((vs, ve), f) in enumerate(edges):
-    for othere in vtoe[ve]:
-      ((_, we), g) = edges[othere]
-      if we == vs:
-        # face g is adjacent to face f
-        # TODO: angle check
-        if g not in face_adj[f]:
-          face_adj[f].append(g)
-          is_interior_edge[e] = True
-        # Don't bother with mirror relations, will catch later
-  return (face_adj, is_interior_edge)
+    """Find the face adjacency graph.
+
+    Faces are adjacent if they share an edge,
+    and the shared edge goes in the reverse direction,
+    and if the angle between them isn't too large.
+
+    Args:
+      faces: list of list of int
+      edges: list of ((int, int), int) - see _GetEdgeData
+      vtoe: dict{ int->list of int } - see _GetEdgeData
+      points: geom.Points
+    Returns:
+      (list of  list of int, list of bool) -
+        first list: each sublist is adjacent face indices for each face
+        second list: maps edge index to True if it separates adjacent faces
+    """
+
+    face_adj = [[] for i in range(len(faces))]
+    is_interior_edge = [False] * len(edges)
+    for e, ((vs, ve), f) in enumerate(edges):
+        for othere in vtoe[ve]:
+            ((_, we), g) = edges[othere]
+            if we == vs:
+                # face g is adjacent to face f
+                # TODO: angle check
+                if g not in face_adj[f]:
+                    face_adj[f].append(g)
+                    is_interior_edge[e] = True
+                # Don't bother with mirror relations, will catch later
+    return (face_adj, is_interior_edge)
 
 
 def _FindFaceGraphComponents(faces, face_adj):
-  """Partition faces into connected components.
-
-  Args:
-    faces: list of list of int
-    face_adj: list of list of int - see _GetFaceGraph
-  Returns:
-    (list of list of int, list of int) -
-      first list partitions face indices into separate lists, each a component
-      second list maps face indices into their component index
-  """
-
-  if not faces:
-    return ([], [])
-  components = []
-  ftoc = [ -1 ] * len(faces)
-  for i in range(len(faces)):
-    if ftoc[i] == -1:
-      compi = len(components)
-      comp = []
-      _FFGCSearch(i, faces, face_adj, ftoc, compi, comp)
-      components.append(comp)
-  return (components, ftoc)
+    """Partition faces into connected components.
+
+    Args:
+      faces: list of list of int
+      face_adj: list of list of int - see _GetFaceGraph
+    Returns:
+      (list of list of int, list of int) -
+        first list partitions face indices into separate lists,
+            each a component
+        second list maps face indices into their component index
+    """
+
+    if not faces:
+        return ([], [])
+    components = []
+    ftoc = [-1] * len(faces)
+    for i in range(len(faces)):
+        if ftoc[i] == -1:
+            compi = len(components)
+            comp = []
+            _FFGCSearch(i, faces, face_adj, ftoc, compi, comp)
+            components.append(comp)
+    return (components, ftoc)
 
 
 def _FFGCSearch(findex, faces, face_adj, ftoc, compi, comp):
-  """Depth first search helper function for _FindFaceGraphComponents
-
-  Searches recursively through all faces connected to findex, adding
-  each face found to comp and setting ftoc for that face to compi.
-  """
-
-  comp.append(findex)
-  ftoc[findex] = compi
-  for otherf in face_adj[findex]:
-    if ftoc[otherf] == -1:
-      _FFGCSearch(otherf, faces, face_adj, ftoc, compi, comp)
-
-def _FindOuterPoly(polys, points):
-  """Assuming polys has one that contains the rest, find that one.
-
-  Args:
-    polys: list of list of int - list of polys given by vertex indices
-    points: geom.Points
-  Returns:
-    int - the index in polys of the outermost one
-  """
-
-  if len(polys) < 2:
+    """Depth first search helper function for _FindFaceGraphComponents
+
+    Searches recursively through all faces connected to findex, adding
+    each face found to comp and setting ftoc for that face to compi.
+    """
+
+    comp.append(findex)
+    ftoc[findex] = compi
+    for otherf in face_adj[findex]:
+        if ftoc[otherf] == -1:
+            _FFGCSearch(otherf, faces, face_adj, ftoc, compi, comp)
+
+
+def _FindOuterPoly(polys, points, faces):
+    """Assuming polys has one CCW-oriented face when looking
+    down average normal of faces, return that one.
+
+    Only one of the faces should have a normal whose dot product
+    with the average normal of faces is positive.
+
+    Args:
+      polys: list of list of int - list of polys given by vertex indices
+      points: geom.Points
+      faces: list of list of int - original selected region, used to find
+          average normal
+    Returns:
+      int - the index in polys of the outermost one
+    """
+
+    if len(polys) < 2:
+        return 0
+    fnorm = (0.0, 0.0, 0.0)
+    for face in faces:
+        if len(face) > 2:
+            fnorm = geom.VecAdd(fnorm, geom.Newell(face, points))
+    if fnorm == (0.0, 0.0, 0.0):
+        return 0
+    # fnorm is really a multiple of the normal, but fine for test below
+    for i, poly in enumerate(polys):
+        if len(poly) > 2:
+            pnorm = geom.Newell(poly, points)
+            if geom.VecDot(fnorm, pnorm) > 0:
+                return i
+    print("whoops, couldn't find an outermost poly")
     return 0
-  for i, poly in enumerate(polys):
-    otherpoly = polys[(i+1) % len(polys)]
-    if geom.PointInside(points.pos[otherpoly[0]], poly, points) == 1:
-      return i
-  print("whoops, couldn't find an outermost poly")
-  return 0
 
 
 def _RotatedPolyAreaToXY(polyarea, norm):
-  """Return a  PolyArea rotated to xy plane.
-
-  Only the points in polyarea will be transferred.
-
-  Args:
-    polyarea: geom.PolyArea
-    norm: the normal for polyarea
-  Returns:
-    (geom.PolyArea, (float, ..., float), dict{ int -> int }) - new PolyArea,
-        4x3 inverse transform, dict mapping new verts to old ones
-  """
-
-  # find rotation matrix that takes norm to (0,0,1)
-  (nx, ny, nz) = norm
-  if abs(nx) < abs(ny) and abs(nx) < abs(nz):
-    v = (vx, vy, vz) = geom.Norm3(0.0, nz, - ny)
-  elif abs(ny) < abs(nz):
-    v = (vx, vy, vz) = geom.Norm3(nz, 0.0, - nx)
-  else:
-    v = (vx, vy, vz) = geom.Norm3(ny, - nx, 0.0)
-  (ux, uy, uz) = geom.Cross3(v, norm)
-  rotmat = [ux, vx, nx, uy, vy, ny, uz, vz, nz, 0.0, 0.0, 0.0]
-  # rotation matrices are orthogonal, so inverse is transpose
-  invrotmat = [ux, uy, uz, vx, vy, vz, nx, ny, nz, 0.0, 0.0, 0.0]
-  pointmap = dict()
-  invpointmap = dict()
-  newpoints = geom.Points()
-  for poly in [polyarea.poly] + polyarea.holes:
-    for v in poly:
-      vcoords = polyarea.points.pos[v]
-      newvcoords = geom.MulPoint3(vcoords, rotmat)
-      newv = newpoints.AddPoint(newvcoords)
-      pointmap[v] = newv
-      invpointmap[newv] = v
-  pa = geom.PolyArea(newpoints)
-  pa.poly = [ pointmap[v] for v in polyarea.poly ]
-  pa.holes = [ [ pointmap[v] for v in hole ] for hole in polyarea.holes ]
-  return (pa, invrotmat, invpointmap)
-
-
-def _AddTransformedPolysToModel(mdl, polys, points, transform, pointmap):
-  """Add (transformed) the points and faces to a model.
-
-  Add polys to mdl.  The polys have coordinates given by indices into points.pos;
-  those need to be transformed by multiplying by the transform matrix.
-  The vertices may already exist in mdl.  Rather than relying on AddPoint to detect
-  the duplicate (transform rounding error makes that dicey), the pointmap dictionary
-  is used to map vertex indices in polys into those in mdl - if they exist already.
-
-  Args:
-    mdl: geom.Model - where to put new vertices, faces
-    polys: list of list of int - each sublist a poly
-    points: geom.Points - coords for vertices in polys
-    transform: (float, ..., float) - 12-tuple, a 4x3 transform matrix
-    pointmap: dict { int -> int } - maps new vertex indices to old ones
-  Side Effects:
-    The model gets new faces and vertices, based on those in polys.
-    We are allowed to modify pointmap, as it will be discarded after call.
-  """
-
-  for i, coords in enumerate(points.pos):
-    if i not in pointmap:
-      p = geom.MulPoint3(coords, transform)
-      pointmap[i] = mdl.points.AddPoint(p)
-  for poly in polys:
-    mpoly = [ pointmap[v] for v in poly ]
-    mdl.faces.append(mpoly)
+    """Return a  PolyArea rotated to xy plane.
+
+    Only the points in polyarea will be transferred.
+
+    Args:
+      polyarea: geom.PolyArea
+      norm: the normal for polyarea
+    Returns:
+      (geom.PolyArea, (float, ..., float), dict{ int -> int }) - new PolyArea,
+          4x3 inverse transform, dict mapping new verts to old ones
+    """
+
+    # find rotation matrix that takes norm to (0,0,1)
+    (nx, ny, nz) = norm
+    if abs(nx) < abs(ny) and abs(nx) < abs(nz):
+        v = (vx, vy, vz) = geom.Norm3(0.0, nz, - ny)
+    elif abs(ny) < abs(nz):
+        v = (vx, vy, vz) = geom.Norm3(nz, 0.0, - nx)
+    else:
+        v = (vx, vy, vz) = geom.Norm3(ny, - nx, 0.0)
+    (ux, uy, uz) = geom.Cross3(v, norm)
+    rotmat = [ux, vx, nx, uy, vy, ny, uz, vz, nz, 0.0, 0.0, 0.0]
+    # rotation matrices are orthogonal, so inverse is transpose
+    invrotmat = [ux, uy, uz, vx, vy, vz, nx, ny, nz, 0.0, 0.0, 0.0]
+    pointmap = dict()
+    invpointmap = dict()
+    newpoints = geom.Points()
+    for poly in [polyarea.poly] + polyarea.holes:
+        for v in poly:
+            vcoords = polyarea.points.pos[v]
+            newvcoords = geom.MulPoint3(vcoords, rotmat)
+            newv = newpoints.AddPoint(newvcoords)
+            pointmap[v] = newv
+            invpointmap[newv] = v
+    pa = geom.PolyArea(newpoints)
+    pa.poly = [pointmap[v] for v in polyarea.poly]
+    pa.holes = [[pointmap[v] for v in hole] for hole in polyarea.holes]
+    pa.data = polyarea.data
+    return (pa, invrotmat, invpointmap)
+
+
+def _AddTransformedPolysToModel(mdl, polys, points, poly_data,
+        transform, pointmap):
+    """Add (transformed) the points and faces to a model.
+
+    Add polys to mdl.  The polys have coordinates given by indices
+    into points.pos; those need to be transformed by multiplying by
+    the transform matrix.
+    The vertices may already exist in mdl.  Rather than relying on
+    AddPoint to detect the duplicate (transform rounding error makes
+    that dicey), the pointmap dictionar is used to map vertex indices
+    in polys into those in mdl - if they exist already.
+
+    Args:
+      mdl: geom.Model - where to put new vertices, faces
+      polys: list of list of int - each sublist a poly
+      points: geom.Points - coords for vertices in polys
+      poly_data: list of any - parallel to polys
+      transform: (float, ..., float) - 12-tuple, a 4x3 transform matrix
+      pointmap: dict { int -> int } - maps new vertex indices to old ones
+    Side Effects:
+      The model gets new faces and vertices, based on those in polys.
+      We are allowed to modify pointmap, as it will be discarded after call.
+    """
+
+    for i, coords in enumerate(points.pos):
+        if i not in pointmap:
+            p = geom.MulPoint3(coords, transform)
+            pointmap[i] = mdl.points.AddPoint(p)
+    for i, poly in enumerate(polys):
+        mpoly = [pointmap[v] for v in poly]
+        mdl.faces.append(mpoly)
+        mdl.face_data.append(poly_data[i])
diff --git a/mesh_inset/offset.py b/mesh_inset/offset.py
index 7c1994696dde39ed0b4f80d5c8fee818dfc550d2..3a44b95b7aa1b7711ee5c72e9f52bdf0b20f2bc7 100644
--- a/mesh_inset/offset.py
+++ b/mesh_inset/offset.py
@@ -16,6 +16,8 @@
 #
 # ##### END GPL LICENSE BLOCK #####
 
+# <pep8 compliant>
+
 """Creating offset polygons inside faces."""
 
 __author__ = "howard.trickey@gmail.com"
@@ -29,693 +31,725 @@ from .geom import Points
 
 AREATOL = 1e-4
 
-class Spoke(object):
-  """A Spoke is a line growing from an outer vertex to an inner one.
-
-  A Spoke is contained in an Offset (see below).
-
-  Attributes:
-    origin: int - index of origin point in a Points
-    dest: int - index of dest point
-    is_reflex: bool - True if spoke grows from a reflex angle
-    dir: (float, float, float) - direction vector (normalized)
-    speed: float - at time t, other end of spoke is
-        origin + t*dir.  Speed is such that the wavefront
-        from the face edges moves at speed 1.
-    face: int - index of face containing this Spoke, in Offset
-    index: int - index of this Spoke in its face
-    destindex: int - index of Spoke dest in its face
-  """
-
-  def __init__(self, v, prev, next, face, index, points):
-    """Set attribute of spoke from points making up initial angle.
-
-    The spoke grows from an angle inside a face along the bisector
-    of that angle.  Its speed is 1/sin(.5a), where a is the angle
-    formed by (prev, v, next).  That speed means that the perpendicular
-    from the end of the spoke to either of the prev->v or v->prev
-    edges will grow at speed 1.
-
-    Args:
-      v: int - index of point spoke grows from
-      prev: int - index of point before v on boundary (in CCW order)
-      next: int - index of point after v on boundary (in CCW order)
-      face: int - index of face containing this spoke, in containing offset
-      index: int - index of this spoke in its face
-      points: geom.Points - maps vertex indices to 3d coords
-    """
 
-    self.origin = v
-    self.dest = v
-    self.face = face
-    self.index = index
-    self.destindex = -1
-    vmap = points.pos
-    vp = vmap[v]
-    prevp = vmap[prev]
-    nextp = vmap[next]
-    uin = Normalized2(Sub2(vp, prevp))
-    uout = Normalized2(Sub2(nextp, vp))
-    uavg = Normalized2((0.5*(uin[0]+uout[0]), 0.5*(uin[1]+uout[1])))
-    if abs(Length2(uavg)) < TOL:
-      # in and out vectors are reverse of each other
-      self.dir = (uout[0], uout[1], 0.0)
-      self.is_reflex = False
-      self.speed = 1e7
-    else:
-      # bisector direction is 90 degree CCW rotation of average incoming/outgoing
-      self.dir = (-uavg[1], uavg[0], 0.0)
-      self.is_reflex = Ccw(next, v, prev, points)
-      ang = Angle(prev, v, next, points)  # in range [0, 180)
-      sin_half_ang = math.sin(math.pi*ang / 360.0)
-      if abs(sin_half_ang) < TOL:
-        self.speed = 1e7
-      else:
-        self.speed = 1.0 / sin_half_ang
-
-  def __repr__(self):
-    """Printing representation of a Spoke."""
-
-    return "@%d+%gt%s <%d,%d>" % (self.origin, \
-            self.speed, str(self.dir), \
-            self.face, self.index)
-
-  def EndPoint(self, t, points, vspeed):
-    """Return the coordinates of the non-origin point at time t.
-
-    Args:
-      t: float - time to end of spoke
-      points: geom.Points - coordinate map
-      vspeed: float - speed in z direction
-    Returns:
-      (float, float, float) - coords of spoke's endpoint at time t
+class Spoke(object):
+    """A Spoke is a line growing from an outer vertex to an inner one.
+
+    A Spoke is contained in an Offset (see below).
+
+    Attributes:
+      origin: int - index of origin point in a Points
+      dest: int - index of dest point
+      is_reflex: bool - True if spoke grows from a reflex angle
+      dir: (float, float, float) - direction vector (normalized)
+      speed: float - at time t, other end of spoke is
+          origin + t*dir.  Speed is such that the wavefront
+          from the face edges moves at speed 1.
+      face: int - index of face containing this Spoke, in Offset
+      index: int - index of this Spoke in its face
+      destindex: int - index of Spoke dest in its face
     """
 
-    p = points.pos[self.origin]
-    d = self.dir
-    v = self.speed
-    return (p[0] + v*t*d[0], p[1] + v*t*d[1], p[2] + vspeed*t)
-
+    def __init__(self, v, prev, next, face, index, points):
+        """Set attribute of spoke from points making up initial angle.
+
+        The spoke grows from an angle inside a face along the bisector
+        of that angle.  Its speed is 1/sin(.5a), where a is the angle
+        formed by (prev, v, next).  That speed means that the perpendicular
+        from the end of the spoke to either of the prev->v or v->prev
+        edges will grow at speed 1.
+
+        Args:
+          v: int - index of point spoke grows from
+          prev: int - index of point before v on boundary (in CCW order)
+          next: int - index of point after v on boundary (in CCW order)
+          face: int - index of face containing this spoke, in containing offset
+          index: int - index of this spoke in its face
+          points: geom.Points - maps vertex indices to 3d coords
+        """
+
+        self.origin = v
+        self.dest = v
+        self.face = face
+        self.index = index
+        self.destindex = -1
+        vmap = points.pos
+        vp = vmap[v]
+        prevp = vmap[prev]
+        nextp = vmap[next]
+        uin = Normalized2(Sub2(vp, prevp))
+        uout = Normalized2(Sub2(nextp, vp))
+        uavg = Normalized2((0.5 * (uin[0] + uout[0]), \
+            0.5 * (uin[1] + uout[1])))
+        if abs(Length2(uavg)) < TOL:
+            # in and out vectors are reverse of each other
+            self.dir = (uout[0], uout[1], 0.0)
+            self.is_reflex = False
+            self.speed = 1e7
+        else:
+            # bisector direction is 90 degree CCW rotation of
+            # average incoming/outgoing
+            self.dir = (-uavg[1], uavg[0], 0.0)
+            self.is_reflex = Ccw(next, v, prev, points)
+            ang = Angle(prev, v, next, points)  # in range [0, 180)
+            sin_half_ang = math.sin(math.pi * ang / 360.0)
+            if abs(sin_half_ang) < TOL:
+                self.speed = 1e7
+            else:
+                self.speed = 1.0 / sin_half_ang
+
+    def __repr__(self):
+        """Printing representation of a Spoke."""
+
+        return "@%d+%gt%s <%d,%d>" % (self.origin, \
+                self.speed, str(self.dir), \
+                self.face, self.index)
+
+    def EndPoint(self, t, points, vspeed):
+        """Return the coordinates of the non-origin point at time t.
+
+        Args:
+          t: float - time to end of spoke
+          points: geom.Points - coordinate map
+          vspeed: float - speed in z direction
+        Returns:
+          (float, float, float) - coords of spoke's endpoint at time t
+        """
+
+        p = points.pos[self.origin]
+        d = self.dir
+        v = self.speed
+        return (p[0] + v * t * d[0], p[1] + v * t * d[1], p[2] + vspeed * t)
+
+    def VertexEvent(self, other, points):
+        """Intersect self with other spoke, and return the OffsetEvent, if any.
+
+        A vertex event is with one advancing spoke intersects an adjacent
+        adavancing spoke, forming a new vertex.
+
+        Args:
+          other: Spoke - other spoke to intersect with
+          points: Geom.points
+        Returns:
+          None or OffsetEvent - if there's an intersection in the growing
+            directions of the spokes, will return the OffsetEvent for
+            the intersection;
+            if lines are collinear or parallel, return None
+        """
+
+        vmap = points.pos
+        a = vmap[self.origin]
+        b = Add2(a, self.dir)
+        c = vmap[other.origin]
+        d = Add2(c, other.dir)
+        # find intersection of line ab with line cd
+        u = Sub2(b, a)
+        v = Sub2(d, c)
+        w = Sub2(a, c)
+        pp = Perp2(u, v)
+        if abs(pp) > TOL:
+            # lines or neither parallel nor collinear
+            si = Perp2(v, w) / pp
+            ti = Perp2(u, w) / pp
+            if si >= 0 and ti >= 0:
+                p = LinInterp2(a, b, si)
+                dist_ab = si * Length2(u)
+                dist_cd = ti * Length2(v)
+                time_ab = dist_ab / self.speed
+                time_cd = dist_cd / other.speed
+                time = max(time_ab, time_cd)
+                return OffsetEvent(True, time, p, self, other)
+        return None
 
-  def VertexEvent(self, other, points):
-    """Intersect self with other spoke, and return the OffsetEvent, if any.
+    def EdgeEvent(self, other, offset):
+        """Intersect self with advancing edge and return OffsetEvent, if any.
+
+        An edge event is when one advancing spoke intersects an advancing
+        edge.  Advancing edges start out as face edges and move perpendicular
+        to them, at a rate of 1.  The endpoints of the edge are the advancing
+        spokes on either end of the edge (so the edge shrinks or grows as
+        it advances). At some time, the edge may shrink to nothing and there
+        will be no EdgeEvent after that time.
+
+        We represent an advancing edge by the first spoke (in CCW order
+        of face) of the pair of defining spokes.
+
+        At time t, end of this spoke is at
+            o + d*s*t
+        where o=self.origin, d=self.dir, s= self.speed.
+        The advancing edge line has this equation:
+            oo + od*os*t + p*a
+        where oo, od, os are o, d, s for other spoke, and p is direction
+        vector parallel to advancing edge, and a is a real parameter.
+        Equating x and y of intersection point:
+
+            o.x + d.x*s*t = oo.x + od.x*os*t + p.x*w
+            o.y + d.y*s*t = oo.y + od.y*os*t + p.y*w
+
+        which can be rearranged into the form
+
+            a = bt + cw
+            d = et + fw
+
+        and solved for t, w.
+
+        Args:
+          other: Spoke - the edge out of this spoke's origin is the advancing
+              edge to be checked for intersection
+          offset: Offset - the containing Offset
+        Returns:
+          None or OffsetEvent - with data about the intersection, if any
+        """
+
+        vmap = offset.polyarea.points.pos
+        o = vmap[self.origin]
+        oo = vmap[other.origin]
+        otherface = offset.facespokes[other.face]
+        othernext = otherface[(other.index + 1) % len(otherface)]
+        oonext = vmap[othernext.origin]
+        p = Normalized2(Sub2(oonext, oo))
+        a = o[0] - oo[0]
+        d = o[1] - oo[1]
+        b = other.dir[0] * other.speed - self.dir[0] * self.speed
+        e = other.dir[1] * other.speed - self.dir[1] * self.speed
+        c = p[0]
+        f = p[1]
+        if abs(c) > TOL:
+            dem = e - f * b / c
+            if abs(dem) > TOL:
+                t = (d - f * a / c) / dem
+                w = (a - b * t) / c
+            else:
+                return None
+        elif abs(f) > TOL:
+            dem = b - c * e / f
+            if abs(dem) > TOL:
+                t = (a - c * d / f) / dem
+                w = (d - e * t) / f
+            else:
+                return None
+        else:
+            return None
+        if t < 0.0:
+            # intersection is in backward direction along self spoke
+            return None
+        if w < 0.0:
+            # intersection on wrong side of first end of advancing line segment
+            return None
+        # calculate the equivalent of w for the other end
+        aa = o[0] - oonext[0]
+        dd = o[1] - oonext[1]
+        bb = othernext.dir[0] * othernext.speed - self.dir[0] * self.speed
+        ee = othernext.dir[1] * othernext.speed - self.dir[1] * self.speed
+        cc = -p[0]
+        ff = -p[1]
+        if abs(cc) > TOL:
+            ww = (aa - bb * t) / cc
+        elif abs(ff) > TOL:
+            ww = (dd - ee * t) / ff
+        else:
+            return None
+        if ww < 0.0:
+            return None
+        evertex = (o[0] + self.dir[0] * self.speed * t, \
+                   o[1] + self.dir[1] * self.speed * t)
+        return OffsetEvent(False, t, evertex, self, other)
 
-    A vertex event is with one advancing spoke intersects an adjacent
-    adavancing spoke, forming a new vertex.
 
-    Args:
-      other: Spoke - other spoke to intersect with
-      points: Geom.points
-    Returns:
-      None or OffsetEvent - if there's an intersection in the growing
-        directions of the spokes, will return the OffsetEvent for
-        the intersection;
-        if lines are collinear or parallel, return None
-    """
-
-    vmap = points.pos
-    a = vmap[self.origin]
-    b = Add2(a, self.dir)
-    c = vmap[other.origin]
-    d = Add2(c, other.dir)
-    # find intersection of line ab with line cd
-    u = Sub2(b, a)
-    v = Sub2(d, c)
-    w = Sub2(a, c)
-    pp = Perp2(u, v)
-    if abs(pp) > TOL:
-      # lines or neither parallel nor collinear
-      si = Perp2(v, w) / pp
-      ti = Perp2(u, w) / pp
-      if si >= 0 and ti >= 0:
-        p = LinInterp2(a, b, si)
-        dist_ab = si*Length2(u)
-        dist_cd = ti*Length2(v)
-        time_ab = dist_ab / self.speed
-        time_cd = dist_cd / other.speed
-        time = max(time_ab, time_cd)
-        return OffsetEvent(True, time, p, self, other)
-    return None
-
-  def EdgeEvent(self, other, offset):
-    """Intersect self with advancing edge and return OffsetEvent, if any.
-
-    An edge event is when one advancing spoke intersects an advancing
-    edge.  Advancing edges start out as face edges and move perpendicular
-    to them, at a rate of 1.  The endpoints of the edge are the advancing
-    spokes on either end of the edge (so the edge shrinks or grows as
-    it advances). At some time, the edge may shrink to nothing and there
-    will be no EdgeEvent after that time.
-
-    We represent an advancing edge by the first spoke (in CCW order
-    of face) of the pair of defining spokes.
-
-    At time t, end of this spoke is at
-        o + d*s*t
-    where o=self.origin, d=self.dir, s= self.speed.
-    The advancing edge line has this equation:
-        oo + od*os*t + p*a
-    where oo, od, os are o, d, s for other spoke, and p is direction
-    vector parallel to advancing edge, and a is a real parameter.
-    Equating x and y of intersection point:
-
-        o.x + d.x*s*t = oo.x + od.x*os*t + p.x*w
-        o.y + d.y*s*t = oo.y + od.y*os*t + p.y*w
-
-    which can be rearranged into the form
-
-        a = bt + cw
-        d = et + fw
-
-    and solved for t, w.
-
-    Args:
-      other: Spoke - the edge out of this spoke's origin is the advancing
-          edge to be checked for intersection
-      offset: Offset - the containing Offset
-    Returns:
-      None or OffsetEvent - with data about the intersection, if any
+class OffsetEvent(object):
+    """An event involving a spoke during offset computation.
+
+    The events kinds are:
+      vertex event: the spoke intersects an adjacent spoke and makes a new
+          vertex
+      edge event: the spoke hits an advancing edge and splits it
+
+    Attributes:
+      is_vertex_event: True if this is a vertex event (else it is edge event)
+      time: float - time at which it happens (edges advance at speed 1)
+      event_vertex: (float, float) - intersection point of event
+      spoke: Spoke - the spoke that this event is for
+      other: Spoke - other spoke involved in event; if vertex event, this will
+        be an adjacent spoke that intersects; if an edge event, this is the
+        spoke whose origin's outgoing edge grows to hit this event's spoke
     """
 
-    vmap = offset.polyarea.points.pos
-    o = vmap[self.origin]
-    oo = vmap[other.origin]
-    otherface = offset.facespokes[other.face]
-    othernext = otherface[(other.index+1) % len(otherface)]
-    oonext = vmap[othernext.origin]
-    p = Normalized2(Sub2(oonext, oo))
-    a = o[0] - oo[0]
-    d = o[1] - oo[1]
-    b = other.dir[0]*other.speed - self.dir[0]*self.speed
-    e = other.dir[1]*other.speed - self.dir[1]*self.speed
-    c = p[0]
-    f = p[1]
-    if abs(c) > TOL:
-      dem = e - f*b/c
-      if abs(dem) > TOL:
-        t = (d - f*a/c) / dem
-        w = (a - b*t) / c
-      else:
-        return None
-    elif abs(f) > TOL:
-      dem = b - c*e/f
-      if abs(dem) > TOL:
-        t = (a - c*d/f) / dem
-        w = (d - e*t) / f
-      else:
-        return None
-    else:
-      return None
-    if t < 0.0:
-      # intersection is in backward direction along self spoke
-      return None
-    if w < 0.0:
-      # intersection is on wrong side of first end of advancing line segment
-      return None
-    # calculate the equivalent of w for the other end
-    aa = o[0] - oonext[0]
-    dd = o[1] - oonext[1]
-    bb = othernext.dir[0]*othernext.speed - self.dir[0]*self.speed
-    ee = othernext.dir[1]*othernext.speed - self.dir[1]*self.speed
-    cc = -p[0]
-    ff = -p[1]
-    if abs(cc) > TOL:
-      ww = (aa - bb*t) / cc
-    elif abs(ff) > TOL:
-      ww = (dd - ee*t) / ff
-    else:
-      return None
-    if ww < 0.0:
-      return None
-    evertex = (o[0] + self.dir[0]*self.speed*t, \
-               o[1] + self.dir[1]*self.speed*t)
-    return OffsetEvent(False, t, evertex, self, other)
-    
+    def __init__(self, isv, time, evertex, spoke, other):
+        """Creates and initializes attributes of an OffsetEvent."""
 
-class OffsetEvent(object):
-  """An event involving a spoke during offset computation.
-
-  The events kinds are:
-    vertex event: the spoke intersects an adjacent spoke and makes a new vertex
-    edge event: the spoke hits an advancing edge and splits it
-
-  Attributes:
-    is_vertex_event: True if this is a vertex event (else it is edge event)
-    time: float - time at which it happens (edges advance at speed 1)
-    event_vertex: (float, float) - intersection point of event
-    spoke: Spoke - the spoke that this event is for
-    other: Spoke - other spoke involved in event; if vertex event, this will
-      be an adjacent spoke that intersects; if an edge event, this is the
-      spoke whose origin's outgoing edge grows to hit this event's spoke
-  """
-
-  def __init__(self, isv, time, evertex, spoke, other):
-    """Creates and initializes attributes of an OffsetEvent."""
-
-    self.is_vertex_event = isv
-    self.time = time
-    self.event_vertex = evertex
-    self.spoke = spoke
-    self.other = other
-
-  def __repr__(self):
-    """Printing representation of an event."""
-
-    if self.is_vertex_event:
-      c = "V"
-    else:
-      c = "E"
-    return "%s t=%5f %s %s %s" % (c, self.time, str(self.event_vertex), \
-                                  repr(self.spoke), repr(self.other))
+        self.is_vertex_event = isv
+        self.time = time
+        self.event_vertex = evertex
+        self.spoke = spoke
+        self.other = other
 
+    def __repr__(self):
+        """Printing representation of an event."""
 
-class Offset(object):
-  """Represents an offset polygonal area, and used to construct one.
-
-  Currently, the polygonal area must lie approximately in the XY plane.
-  As well as growing inwards in that plane, the advancing lines also
-  move in the Z direction at the rate of vspeed.
-
-  Attributes:
-    polyarea: geom.PolyArea - the area we are offsetting from.
-        We share the polyarea.points, and add to it as points in
-        the offset polygonal area are computed.
-    facespokes: list of list of Spoke - each sublist is a closed face
-        (oriented CCW); the faces may mutually interfere.
-        These lists are spokes for polyarea.poly + polyarea.holes.
-    endtime: float - time when this offset hits its first
-        event (relative to beginning of this offset), or the amount
-        that takes this offset to the end of the total Build time
-    timesofar: float - sum of times taken by all containing Offsets
-    vspeed: float - speed that edges move perpendicular to offset plane
-    inneroffsets: list of Offset - the offsets that take over after this (inside it)
-  """
-
-  def __init__(self, polyarea, time, vspeed):
-    """Set up initial state of Offset from a polyarea.
-
-    Args:
-      polyarea: geom.PolyArea
-      time: float - time so far
-    """
+        if self.is_vertex_event:
+            c = "V"
+        else:
+            c = "E"
+        return "%s t=%5f %s %s %s" % (c, self.time, str(self.event_vertex), \
+                                      repr(self.spoke), repr(self.other))
 
-    self.polyarea = polyarea
-    self.facespokes = []
-    self.endtime = 0.0
-    self.timesofar = time
-    self.vspeed = vspeed
-    self.inneroffsets = []
-    self.InitFaceSpokes(polyarea.poly)
-    for f in polyarea.holes:
-      self.InitFaceSpokes(f)
-
-  def __repr__(self):
-    ans = ["Offset: endtime=%g" % self.endtime]
-    for i, face in enumerate(self.facespokes):
-      ans.append(("<%d>" % i) + str([ str(spoke) for spoke in face ]))
-    return '\n'.join(ans)
-
-  def PrintNest(self, indent_level=0):
-    indent = " " * indent_level * 4
-    print(indent + "Offset  timesofar=", self.timesofar, "endtime=", self.endtime)
-    print(indent + " polyarea=", self.polyarea.poly, self.polyarea.holes)
-    for o in self.inneroffsets:
-      o.PrintNest(indent_level+1)
-
-  def InitFaceSpokes(self, face_vertices):
-    """Initialize the offset representation of a face from vertex list.
-
-    If the face has no area or too small an area, don't bother making it.
-
-    Args:
-      face_vertices: list of int - point indices for boundary of face
-    Side effect:
-      A new face (list of spokes) may be added to self.facespokes
-    """
 
-    n = len(face_vertices)
-    if n <= 2:
-      return
-    points = self.polyarea.points
-    area = abs(geom.SignedArea(face_vertices, points))
-    if area < AREATOL:
-      return
-    findex = len(self.facespokes)
-    fspokes = [ Spoke(v, face_vertices[(i-1) % n], \
-        face_vertices[(i+1) % n], findex, i, points) \
-        for i, v in enumerate(face_vertices) ]
-    self.facespokes.append(fspokes)
-
-  def NextSpokeEvents(self, spoke):
-    """Return the OffsetEvents that will next happen for a given spoke.
-
-    It might happen that some events happen essentially simultaneously,
-    and also it is convenient to separate Edge and Vertex events, so
-    we return two lists.
-    But, for vertex events, only look at the event with the next Spoke,
-    as the event with the previous spoke will be accounted for when we
-    consider that previous spoke.
-
-    Args:
-      spoke: Spoke - a spoke in one of the faces of this object
-    Returns:
-      (float, list of OffsetEvent, list of OffsetEvent) - time of next event,
-          next Vertex event list and next Edge event list
-    """
-
-    facespokes = self.facespokes[spoke.face]
-    n = len(facespokes)
-    bestt = 1e100
-    bestv = []
-    beste = []
-    # First find vertex event (only the one with next spoke)
-    next_spoke = facespokes[(spoke.index+1) % n]
-    ev = spoke.VertexEvent(next_spoke, self.polyarea.points)
-    if ev:
-      bestv = [ev]
-      bestt = ev.time
-    # Now find edge events, if this is a reflex vertex
-    if spoke.is_reflex:
-      prev_spoke = facespokes[(spoke.index-1) % n]
-      for f in self.facespokes:
-        for other in f:
-          if other == spoke or other == prev_spoke:
-            continue
-          ev = spoke.EdgeEvent(other, self)
-          if ev:
-            if ev.time < bestt - TOL:
-              beste = []
-              bestv = []
-              bestt = ev.time
-            if abs(ev.time - bestt) < TOL:
-              beste.append(ev)
-    return (bestt, bestv, beste)
-
-  def Build(self, target = 2e100):
-    """Build the complete Offset structure or up until target time.
-
-    Find the next event(s), makes the appropriate inner Offsets
-    that are inside this one, and calls Build on those Offsets to continue the
-    process until only a single point is left or time reaches target.
+class Offset(object):
+    """Represents an offset polygonal area, and used to construct one.
+
+    Currently, the polygonal area must lie approximately in the XY plane.
+    As well as growing inwards in that plane, the advancing lines also
+    move in the Z direction at the rate of vspeed.
+
+    Attributes:
+      polyarea: geom.PolyArea - the area we are offsetting from.
+          We share the polyarea.points, and add to it as points in
+          the offset polygonal area are computed.
+      facespokes: list of list of Spoke - each sublist is a closed face
+          (oriented CCW); the faces may mutually interfere.
+          These lists are spokes for polyarea.poly + polyarea.holes.
+      endtime: float - time when this offset hits its first
+          event (relative to beginning of this offset), or the amount
+          that takes this offset to the end of the total Build time
+      timesofar: float - sum of times taken by all containing Offsets
+      vspeed: float - speed that edges move perpendicular to offset plane
+      inneroffsets: list of Offset - the offsets that take over after this
+          (inside it)
     """
 
-    bestt = 1e100
-    bestevs = [[], []]
-    for f in self.facespokes:
-      for s in f:
-        (t, ve, ee) = self.NextSpokeEvents(s)
-        if t < bestt - TOL:
-          bestevs = [[], []]
-          bestt = t
-        if abs(t-bestt) < TOL:
-          bestevs[0].extend(ve)
-          bestevs[1].extend(ee)
-    if bestt == 1e100:
-      # could happen if polygon is oriented wrong
-      # or in other special cases
-      return
-    if abs(bestt) < TOL:
-      # seems to be in a loop, so quit
-      return
-    self.endtime = bestt
-    (ve, ee) = bestevs
-    newfaces = []
-    splitjoin = None
-    if target < self.endtime:
-      self.endtime = target
-      newfaces = self.MakeNewFaces(self.endtime)
-    elif ve and not ee:
-      # Only vertex events.
-      # Merging of successive vertices in inset face will
-      # take care of the vertex events
-      newfaces = self.MakeNewFaces(self.endtime)
-    else:
-      # Edge events too
-      # First make the new faces (handles all vertex events)
-      newfaces = self.MakeNewFaces(self.endtime)
-      # Only do one edge event (handle other simultaneous edge
-      # events in subsequent recursive Build calls)
-      splitjoin = self.SplitJoinFaces(newfaces, ee[0])
-    nexttarget = target - self.endtime
-    if len(newfaces) > 0:
-      pa = geom.PolyArea(points = self.polyarea.points)
-      pa.color = self.polyarea.color
-      newt = self.timesofar+self.endtime
-      pa2 = None  # may make another
-      if not splitjoin:
-        pa.poly = newfaces[0]
-        pa.holes = newfaces[1:]
-      elif splitjoin[0] == 'split':
-        (_, findex, newface0, newface1) = splitjoin
-        if findex == 0:
-          # Outer poly of polyarea was split.
-          # Now there will be two polyareas.
-          # If there were holes, need to allocate according to
-	  # which one contains the holes.
-          pa.poly = newface0
-          pa2 = geom.PolyArea(points = self.polyarea.points)
-          pa2.color = self.polyarea.color
-          pa2.poly = newface1
-          if len(newfaces) > 1:
-            # print("need to allocate holes")
-            for hf in newfaces[1:]:
-              if pa.ContainsPoly(hf, self.polyarea.points):
-                # print("add", hf, "to", pa.poly)
-                pa.holes.append(hf)
-              elif pa2.ContainsPoly(hf, self.polyarea.points):
-                # print("add", hf, "to", pa2.poly)
-                pa2.holes.append(hf)
-              else:
-                print("whoops, hole in neither poly!")
-          self.inneroffsets = [ Offset(pa, newt, self.vspeed), Offset(pa2, newt, self.vspeed) ]
+    def __init__(self, polyarea, time, vspeed):
+        """Set up initial state of Offset from a polyarea.
+
+        Args:
+          polyarea: geom.PolyArea
+          time: float - time so far
+        """
+
+        self.polyarea = polyarea
+        self.facespokes = []
+        self.endtime = 0.0
+        self.timesofar = time
+        self.vspeed = vspeed
+        self.inneroffsets = []
+        self.InitFaceSpokes(polyarea.poly)
+        for f in polyarea.holes:
+            self.InitFaceSpokes(f)
+
+    def __repr__(self):
+        ans = ["Offset: endtime=%g" % self.endtime]
+        for i, face in enumerate(self.facespokes):
+            ans.append(("<%d>" % i) + str([str(spoke) for spoke in face]))
+        return '\n'.join(ans)
+
+    def PrintNest(self, indent_level=0):
+        indent = " " * indent_level * 4
+        print(indent + "Offset  timesofar=", self.timesofar, "endtime=",
+            self.endtime)
+        print(indent + " polyarea=", self.polyarea.poly, self.polyarea.holes)
+        for o in self.inneroffsets:
+            o.PrintNest(indent_level + 1)
+
+    def InitFaceSpokes(self, face_vertices):
+        """Initialize the offset representation of a face from vertex list.
+
+        If the face has no area or too small an area, don't bother making it.
+
+        Args:
+          face_vertices: list of int - point indices for boundary of face
+        Side effect:
+          A new face (list of spokes) may be added to self.facespokes
+        """
+
+        n = len(face_vertices)
+        if n <= 2:
+            return
+        points = self.polyarea.points
+        area = abs(geom.SignedArea(face_vertices, points))
+        if area < AREATOL:
+            return
+        findex = len(self.facespokes)
+        fspokes = [Spoke(v, face_vertices[(i - 1) % n], \
+            face_vertices[(i + 1) % n], findex, i, points) \
+            for i, v in enumerate(face_vertices)]
+        self.facespokes.append(fspokes)
+
+    def NextSpokeEvents(self, spoke):
+        """Return the OffsetEvents that will next happen for a given spoke.
+
+        It might happen that some events happen essentially simultaneously,
+        and also it is convenient to separate Edge and Vertex events, so
+        we return two lists.
+        But, for vertex events, only look at the event with the next Spoke,
+        as the event with the previous spoke will be accounted for when we
+        consider that previous spoke.
+
+        Args:
+          spoke: Spoke - a spoke in one of the faces of this object
+        Returns:
+          (float, list of OffsetEvent, list of OffsetEvent) -
+              time of next event,
+              next Vertex event list and next Edge event list
+        """
+
+        facespokes = self.facespokes[spoke.face]
+        n = len(facespokes)
+        bestt = 1e100
+        bestv = []
+        beste = []
+        # First find vertex event (only the one with next spoke)
+        next_spoke = facespokes[(spoke.index + 1) % n]
+        ev = spoke.VertexEvent(next_spoke, self.polyarea.points)
+        if ev:
+            bestv = [ev]
+            bestt = ev.time
+        # Now find edge events, if this is a reflex vertex
+        if spoke.is_reflex:
+            prev_spoke = facespokes[(spoke.index - 1) % n]
+            for f in self.facespokes:
+                for other in f:
+                    if other == spoke or other == prev_spoke:
+                        continue
+                    ev = spoke.EdgeEvent(other, self)
+                    if ev:
+                        if ev.time < bestt - TOL:
+                            beste = []
+                            bestv = []
+                            bestt = ev.time
+                        if abs(ev.time - bestt) < TOL:
+                            beste.append(ev)
+        return (bestt, bestv, beste)
+
+    def Build(self, target=2e100):
+        """Build the complete Offset structure or up until target time.
+
+        Find the next event(s), makes the appropriate inner Offsets
+        that are inside this one, and calls Build on those Offsets to continue
+        the process until only a single point is left or time reaches target.
+        """
+
+        bestt = 1e100
+        bestevs = [[], []]
+        for f in self.facespokes:
+            for s in f:
+                (t, ve, ee) = self.NextSpokeEvents(s)
+                if t < bestt - TOL:
+                    bestevs = [[], []]
+                    bestt = t
+                if abs(t - bestt) < TOL:
+                    bestevs[0].extend(ve)
+                    bestevs[1].extend(ee)
+        if bestt == 1e100:
+            # could happen if polygon is oriented wrong
+            # or in other special cases
+            return
+        if abs(bestt) < TOL:
+            # seems to be in a loop, so quit
+            return
+        self.endtime = bestt
+        (ve, ee) = bestevs
+        newfaces = []
+        splitjoin = None
+        if target < self.endtime:
+            self.endtime = target
+            newfaces = self.MakeNewFaces(self.endtime)
+        elif ve and not ee:
+            # Only vertex events.
+            # Merging of successive vertices in inset face will
+            # take care of the vertex events
+            newfaces = self.MakeNewFaces(self.endtime)
         else:
-          # A hole was split. New faces just replace the split one.
-          pa.poly = newfaces[0]
-          pa.holes = newfaces[0:findex] + [ newface0, newface1 ] + \
-                     newfaces[findex+1:]
-      else:
-        # A join
-        (_, findex, othfindex, newface0) = splitjoin
-        if findex == 0 or othfindex == 0:
-          # Outer poly was joined to one hole.
-          pa.poly = newface0
-          pa.holes = [ f for f in newfaces if f is not None ] 
+            # Edge events too
+            # First make the new faces (handles all vertex events)
+            newfaces = self.MakeNewFaces(self.endtime)
+            # Only do one edge event (handle other simultaneous edge
+            # events in subsequent recursive Build calls)
+            splitjoin = self.SplitJoinFaces(newfaces, ee[0])
+        nexttarget = target - self.endtime
+        if len(newfaces) > 0:
+            pa = geom.PolyArea(points=self.polyarea.points)
+            pa.data = self.polyarea.data
+            newt = self.timesofar + self.endtime
+            pa2 = None  # may make another
+            if not splitjoin:
+                pa.poly = newfaces[0]
+                pa.holes = newfaces[1:]
+            elif splitjoin[0] == 'split':
+                (_, findex, newface0, newface1) = splitjoin
+                if findex == 0:
+                    # Outer poly of polyarea was split.
+                    # Now there will be two polyareas.
+                    # If there were holes, need to allocate according to
+                    # which one contains the holes.
+                    pa.poly = newface0
+                    pa2 = geom.PolyArea(points=self.polyarea.points)
+                    pa2.data = self.polyarea.data
+                    pa2.poly = newface1
+                    if len(newfaces) > 1:
+                        # print("need to allocate holes")
+                        for hf in newfaces[1:]:
+                            if pa.ContainsPoly(hf, self.polyarea.points):
+                                # print("add", hf, "to", pa.poly)
+                                pa.holes.append(hf)
+                            elif pa2.ContainsPoly(hf, self.polyarea.points):
+                                # print("add", hf, "to", pa2.poly)
+                                pa2.holes.append(hf)
+                            else:
+                                print("whoops, hole in neither poly!")
+                    self.inneroffsets = [Offset(pa, newt, self.vspeed), \
+                        Offset(pa2, newt, self.vspeed)]
+                else:
+                    # A hole was split. New faces just replace the split one.
+                    pa.poly = newfaces[0]
+                    pa.holes = newfaces[0:findex] + [newface0, newface1] + \
+                               newfaces[findex + 1:]
+            else:
+                # A join
+                (_, findex, othfindex, newface0) = splitjoin
+                if findex == 0 or othfindex == 0:
+                    # Outer poly was joined to one hole.
+                    pa.poly = newface0
+                    pa.holes = [f for f in newfaces if f is not None]
+                else:
+                    # Two holes were joined
+                    pa.poly = newfaces[0]
+                    pa.holes = [f for f in newfaces if f is not None] + \
+                        [newface0]
+            self.inneroffsets = [Offset(pa, newt, self.vspeed)]
+            if pa2:
+                self.inneroffsets.append(Offset(pa2, newt, self.vspeed))
+            if nexttarget > TOL:
+                for o in self.inneroffsets:
+                    o.Build(nexttarget)
+
+    def FaceAtSpokeEnds(self, f, t):
+        """Return a new face that is at the spoke ends of face f at time t.
+
+        Also merges any adjacent approximately equal vertices into one vertex,
+        so returned list may be smaller than len(f).
+        Also sets the destindex fields of the spokes to the vertex they
+        will now end at.
+
+        Args:
+          f: list of Spoke - one of self.faces
+          t: float - time in this offset
+        Returns:
+          list of int - indices into self.polyarea.points
+          (which has been extended with new ones)
+        """
+
+        newface = []
+        points = self.polyarea.points
+        for i in range(0, len(f)):
+            s = f[i]
+            vcoords = s.EndPoint(t, points, self.vspeed)
+            v = points.AddPoint(vcoords)
+            if newface:
+                if v == newface[-1]:
+                    s.destindex = len(newface) - 1
+                elif i == len(f) - 1 and v == newface[0]:
+                    s.destindex = 0
+                else:
+                    newface.append(v)
+                    s.destindex = len(newface) - 1
+            else:
+                newface.append(v)
+                s.destindex = 0
+            s.dest = v
+        return newface
+
+    def MakeNewFaces(self, t):
+        """For each face in this offset, make new face extending spokes
+        to time t.
+
+        Args:
+          t: double - time
+        Returns:
+          list of list of int - list of new faces
+        """
+
+        ans = []
+        for f in self.facespokes:
+            newf = self.FaceAtSpokeEnds(f, t)
+            if len(newf) > 2:
+                ans.append(newf)
+        return ans
+
+    def SplitJoinFaces(self, newfaces, ev):
+        """Use event ev to split or join faces.
+
+        Given ev, an edge event, use the ev spoke to split the
+        other spoke's inner edge.
+        If the ev spoke's face and other's face are the same, this splits the
+        face into two; if the faces are different, it joins them into one.
+        We have just made faces at the end of the spokes.
+        We have to remove the edge going from the other spoke to its
+        next spoke, and replace it with two edges, going to and from
+        the event spoke's destination.
+        General situation:
+             __  s  ____
+        c\     b\ | /a       /e
+          \      \|/        /
+          f----------------g
+         /        d        \
+       o/                   \h
+
+        where sd is the event spoke and of is the "other spoke",
+        hg is a spoke, and cf, fg. ge, ad, and db are edges in
+        the new inside face.
+        What we are to do is to split fg into two edges, with the
+        joining point attached where b,s,a join.
+        There are a bunch of special cases:
+         - one of split fg edges might have zero length because end points
+           are already coincident or nearly coincident.
+         - maybe c==b or e==a
+
+        Args:
+          newfaces: list of list of int - the new faces
+          ev: OffsetEvent - an edge event
+        Side Effects:
+          faces in newfaces that are involved in split or join are
+          set to None
+        Returns: one of:
+          ('split', int, list of int, list of int) - int is the index in
+              newfaces of the face that was split, two lists are the
+              split faces
+          ('join', int, int, list of int) - two ints are the indices in
+              newfaces of the faces that were joined, and the list is
+              the joined face
+        """
+
+        # print("SplitJoinFaces", newfaces, ev)
+        spoke = ev.spoke
+        other = ev.other
+        findex = spoke.face
+        othfindex = other.face
+        newface = newfaces[findex]
+        othface = newfaces[othfindex]
+        nnf = len(newface)
+        nonf = len(othface)
+        d = spoke.destindex
+        f = other.destindex
+        c = (f - 1) % nonf
+        g = (f + 1) % nonf
+        e = (f + 2) % nonf
+        a = (d - 1) % nnf
+        b = (d + 1) % nnf
+        # print("newface=", newface)
+        # if findex != othfindex: print("othface=", othface)
+        # print("d=", d, "f=", f, "c=", c, "g=", g, "e=", e, "a=", a, "b=", b)
+        newface0 = []
+        newface1 = []
+        # The two new faces put spoke si's dest on edge between
+        # pi's dest and qi (edge after pi)'s dest in original face.
+        # These are indices in the original face; the current dest face
+        # may have fewer elements because of merging successive points
+        if findex == othfindex:
+            # Case where splitting one new face into two.
+            # The new new faces are:
+            # [d, g, e, ..., a] and [d, b, ..., c, f]
+            # (except we actually want the vertex numbers at those positions)
+            newface0 = [newface[d]]
+            i = g
+            while i != d:
+                newface0.append(newface[i])
+                i = (i + 1) % nnf
+            newface1 = [newface[d]]
+            i = b
+            while i != f:
+                newface1.append(newface[i])
+                i = (i + 1) % nnf
+            newface1.append(newface[f])
+            # print("newface0=", newface0, "newface1=", newface1)
+            # now the destindex values for the spokes are messed up
+            # but I don't think we need them again
+            newfaces[findex] = None
+            return ('split', findex, newface0, newface1)
         else:
-          # Two holes were joined
-          pa.poly = newfaces[0]
-          pa.holes = [ f for f in newfaces if f is not None ] + [ newface0 ]
-      self.inneroffsets = [ Offset(pa, newt, self.vspeed) ]
-      if pa2:
-        self.inneroffsets.append(Offset(pa2, newt, self.vspeed))
-      if nexttarget > TOL:
-        for o in self.inneroffsets:
-          o.Build(nexttarget)
-
-  def FaceAtSpokeEnds(self, f, t):
-    """Return a new face that is at the spoke ends of face f at time t.
+            # Case where joining two faces into one.
+            # The new face is splicing d's face between
+            # f and g in other face (or the reverse of all of that).
+            newface0 = [othface[i] for i in range(0, f + 1)]
+            newface0.append(newface[d])
+            i = b
+            while i != d:
+                newface0.append(newface[i])
+                i = (i + 1) % nnf
+            newface0.append(newface[d])
+            if g != 0:
+                newface0.extend([othface[i] for i in range(g, nonf)])
+            # print("newface0=", newface0)
+            newfaces[findex] = None
+            newfaces[othfindex] = None
+            return ('join', findex, othfindex, newface0)
+
+    def InnerPolyAreas(self):
+        """Return the interior of the offset (and contained offsets) as
+        PolyAreas.
+
+        Returns:
+          geom.PolyAreas
+        """
+
+        ans = geom.PolyAreas()
+        ans.points = self.polyarea.points
+        _AddInnerAreas(self, ans)
+        return ans
+
+    def MaxAmount(self):
+        """Returns the maximum offset amount possible.
+        Returns:
+          float - maximum amount
+        """
+
+        # Need to do Build on a copy of points
+        # so don't add points that won't be used when
+        # really do a Build with a smaller amount
+        test_points = geom.Points()
+        test_points.AddPoints(self.polyarea.points)
+        save_points = self.polyarea.points
+        self.polyarea.points = test_points
+        self.Build()
+        max_amount = self._MaxTime()
+        self.polyarea.points = save_points
+        return max_amount
+
+    def _MaxTime(self):
+        if self.inneroffsets:
+            return max([o._MaxTime() for o in self.inneroffsets])
+        else:
+            return self.timesofar + self.endtime
 
-    Also merges any adjacent approximately equal vertices into one vertex,
-    so returned list may be smaller than len(f).
-    Also sets the destindex fields of the spokes to the vertex they
-    will now end at.
 
-    Args:
-      f: list of Spoke - one of self.faces
-      t: float - time in this offset
-    Returns:
-      list of int - indices into self.polyarea.points (which has been extended with new ones)
-    """
+def _AddInnerAreas(off, polyareas):
+    """Add the innermost areas of offset off to polyareas.
 
-    newface = []
-    points = self.polyarea.points
-    for i in range(0, len(f)):
-      s = f[i]
-      vcoords = s.EndPoint(t, points, self.vspeed)
-      v = points.AddPoint(vcoords)
-      if newface:
-        if v == newface[-1]:
-          s.destindex = len(newface) - 1
-        elif i == len(f)-1 and v == newface[0]:
-          s.destindex = 0
-        else:
-          newface.append(v)
-          s.destindex = len(newface) - 1
-      else:
-        newface.append(v)
-        s.destindex = 0
-      s.dest = v
-    return newface
-
-  def MakeNewFaces(self, t):
-    """For each face in this offset, make new face extending spokes to time t.
-
-    Args:
-      t: double - time
-    Returns:
-      list of list of int - list of new faces
-    """
+    Assume that polyareas is already using the proper shared points.
 
-    ans = []
-    for f in self.facespokes:
-      newf = self.FaceAtSpokeEnds(f, t)
-      if len(newf) > 2:
-        ans.append(newf)
-    return ans
-
-  def SplitJoinFaces(self, newfaces, ev):
-    """Use event ev to split or join faces.
-    
-    Given ev, an edge event, use the ev spoke to split the
-    other spoke's inner edge.
-    If the ev spoke's face and other's face are the same, this splits the
-    face into two; if the faces are different, it joins them into one.
-    We have just made faces at the end of the spokes.
-    We have to remove the edge going from the other spoke to its
-    next spoke, and replace it with two edges, going to and from
-    the event spoke's destination.
-    General situation:
-         __  s  ____
-    c\     b\ | /a       /e
-      \      \|/        /
-      f----------------g
-     /        d        \
-   o/                   \h
-  
-    where sd is the event spoke and of is the "other spoke",
-    hg is a spoke, and cf, fg. ge, ad, and db are edges in
-    the new inside face.
-    What we are to do is to split fg into two edges, with the
-    joining point attached where b,s,a join.
-    There are a bunch of special cases:
-     - one of split fg edges might have zero length because end points
-       are already coincident or nearly coincident.
-     - maybe c==b or e==a
-
-    Args:
-      newfaces: list of list of int - the new faces
-      ev: OffsetEvent - an edge event
+    Arguments:
+      off: Offset
+      polyareas: geom.PolyAreas
     Side Effects:
-      faces in newfaces that are involved in split or join are
-      set to None
-    Returns: one of:
-      ('split', int, list of int, list of int) - int is the index in
-          newfaces of the face that was split, two lists are the
-	  split faces
-      ('join', int, int, list of int) - two ints are the indices in
-          newfaces of the faces that were joined, and the list is
-	  the joined face
+      Any non-zero-area faces in the very inside of off are
+      added to polyareas.
     """
 
-    # print("SplitJoinFaces", newfaces, ev)
-    spoke = ev.spoke
-    other = ev.other
-    findex = spoke.face
-    othfindex = other.face
-    newface = newfaces[findex]
-    othface = newfaces[othfindex]
-    nnf = len(newface)
-    nonf = len(othface)
-    d = spoke.destindex
-    f = other.destindex 
-    c = (f-1) % nonf
-    g = (f+1) % nonf
-    e = (f+2) % nonf
-    a = (d-1) % nnf
-    b = (d+1) % nnf
-    # print("newface=", newface)
-    # if findex != othfindex: print("othface=", othface)
-    # print("d=", d, "f=", f, "c=", c, "g=", g, "e=", e, "a=", a, "b=", b)
-    newface0 = []
-    newface1 = []
-    # The two new faces put spoke si's dest on edge between
-    # pi's dest and qi (edge after pi)'s dest in original face.
-    # These are indices in the original face; the current dest face
-    # may have fewer elements because of merging successive points
-    if findex == othfindex:
-      # Case where splitting one new face into two.
-      # The new new faces are:
-      # [d, g, e, ..., a] and [d, b, ..., c, f]
-      # (except we actually want the vertex numbers at those positions)
-      newface0 = [ newface[d] ]
-      i = g
-      while i != d:
-        newface0.append(newface[i])
-        i = (i+1) % nnf
-      newface1 = [ newface[d] ]
-      i = b
-      while i != f:
-        newface1.append(newface[i])
-        i = (i+1) % nnf
-      newface1.append(newface[f])
-      # print("newface0=", newface0, "newface1=", newface1)
-      # now the destindex values for the spokes are messed up
-      # but I don't think we need them again
-      newfaces[findex] = None
-      return ('split', findex, newface0, newface1)
+    if off.inneroffsets:
+        for o in off.inneroffsets:
+            _AddInnerAreas(o, polyareas)
     else:
-      # Case where joining two faces into one.
-      # The new face is splicing d's face between
-      # f and g in other face (or the reverse of all of that).
-      newface0 = [ othface[i] for i in range(0, f+1) ]
-      newface0.append(newface[d])
-      i = b
-      while i != d:
-        newface0.append(newface[i])
-        i = (i+1) % nnf
-      newface0.append(newface[d])
-      if g != 0:
-        newface0.extend([ othface[i] for i in range(g, nonf) ])
-      # print("newface0=", newface0)
-      newfaces[findex] = None
-      newfaces[othfindex] = None
-      return ('join', findex, othfindex, newface0)
-      
-    
-  def InnerPolyAreas(self):
-    """Return the interior of the offset (and contained offsets) as PolyAreas.
-
-    Returns:
-      geom.PolyAreas
-    """
-
-    ans = geom.PolyAreas()
-    ans.points = self.polyarea.points
-    _AddInnerAreas(self, ans)
-    return ans
-
-
-def _AddInnerAreas(off, polyareas):
-  """Add the innermost areas of offset off to polyareas.
-
-  Assume that polyareas is already using the proper shared points.
-
-  Arguments:
-    off: Offset
-    polyareas: geom.PolyAreas
-  Side Effects:
-    Any non-zero-area faces in the very inside of off are
-    added to polyareas.
-  """
-
-  if off.inneroffsets:
-    for o in off.inneroffsets:
-      _AddInnerAreas(o, polyareas)
-  else:
-    newpa = geom.PolyArea(polyareas.points)
-    for i, f in enumerate(off.facespokes):
-      newface = off.FaceAtSpokeEnds(f, off.endtime)
-      area = abs(geom.SignedArea(newface, polyareas.points))
-      if area < AREATOL:
-        if i == 0:
-         break
-        else:
-         continue
-      if i == 0:
-        newpa.poly = newface
-        newpa.color = off.polyarea.color
-      else:
-        newpa.holes.append(newface)
-    if newpa.poly:
-      polyareas.polyareas.append(newpa)
-
-        
+        newpa = geom.PolyArea(polyareas.points)
+        for i, f in enumerate(off.facespokes):
+            newface = off.FaceAtSpokeEnds(f, off.endtime)
+            area = abs(geom.SignedArea(newface, polyareas.points))
+            if area < AREATOL:
+                if i == 0:
+                    break
+                else:
+                    continue
+            if i == 0:
+                newpa.poly = newface
+                newpa.data = off.polyarea.data
+            else:
+                newpa.holes.append(newface)
+        if newpa.poly:
+            polyareas.polyareas.append(newpa)
diff --git a/mesh_inset/triquad.py b/mesh_inset/triquad.py
index 628fbdceaf3728f733eaf6162a3c5e34062fdfd9..88affa8985e4b04f4f880da071ab81e7886b572b 100644
--- a/mesh_inset/triquad.py
+++ b/mesh_inset/triquad.py
@@ -16,6 +16,8 @@
 #
 # ##### END GPL LICENSE BLOCK #####
 
+# <pep8 compliant>
+
 
 from . import geom
 import math
@@ -28,10 +30,10 @@ from math import sqrt
 # be tuples instead of lists.
 # Vmaps are lists taking vertex index -> Point
 
-TOL = 1e-7    # a tolerance for fuzzy equality
-GTHRESH = 75  # threshold above which use greedy to _Quandrangulate
-ANGFAC = 1.0  # weighting for angles in quad goodness measure
-DEGFAC = 10.0 # weighting for degree in quad goodness measure
+TOL = 1e-7     # a tolerance for fuzzy equality
+GTHRESH = 75   # threshold above which use greedy to _Quandrangulate
+ANGFAC = 1.0   # weighting for angles in quad goodness measure
+DEGFAC = 10.0  # weighting for degree in quad goodness measure
 
 # Angle kind constants
 Ang0 = 1
@@ -42,1112 +44,1129 @@ Ang360 = 5
 
 
 def TriangulateFace(face, points):
-  """Triangulate the given face.
+    """Triangulate the given face.
 
-  Uses an easy triangulation first, followed by a constrained delauney
-  triangulation to get better shaped triangles.
+    Uses an easy triangulation first, followed by a constrained delauney
+    triangulation to get better shaped triangles.
 
-  Args:
-    face: list of int - indices in points, assumed CCW-oriented
-    points: geom.Points - holds coordinates for vertices
-  Returns:
-    list of (int, int, int) - 3-tuples are CCW-oriented vertices of
-        triangles making up the triangulation
-  """
+    Args:
+      face: list of int - indices in points, assumed CCW-oriented
+      points: geom.Points - holds coordinates for vertices
+    Returns:
+      list of (int, int, int) - 3-tuples are CCW-oriented vertices of
+          triangles making up the triangulation
+    """
 
-  if len(face) <= 3:
-    return [ tuple(face) ]
-  tris = EarChopTriFace(face, points)
-  bord = _BorderEdges([face])
-  triscdt = _CDT(tris, bord, points)
-  return triscdt
+    if len(face) <= 3:
+        return [tuple(face)]
+    tris = EarChopTriFace(face, points)
+    bord = _BorderEdges([face])
+    triscdt = _CDT(tris, bord, points)
+    return triscdt
 
 
 def TriangulateFaceWithHoles(face, holes, points):
-  """Like TriangulateFace, but with holes inside the face.
-
-  Works by making one complex polygon that has segments to
-  and from the holes ("islands"), and then using the same method
-  as TriangulateFace.
-
-  Args:
-    face: list of int - indices in points, assumed CCW-oriented
-    holes: list of list of int - each sublist is like face
-        but CW-oriented and assumed to be inside face
-    points: geom.Points - holds coordinates for vertices
-  Returns:
-    list of (int, int, int) - 3-tuples are CCW-oriented vertices of
-        triangles making up the triangulation
-  """
-
-  if len(holes) == 0:
-    return TriangulateFace(face, points)
-  allfaces = [face] + holes
-  sholes = [ _SortFace(h, points) for h in holes ]
-  joinedface = _JoinIslands(face, sholes, points)
-  tris = EarChopTriFace(joinedface, points)
-  bord = _BorderEdges(allfaces)
-  triscdt = _CDT(tris, bord, points)
-  return triscdt
+    """Like TriangulateFace, but with holes inside the face.
+
+    Works by making one complex polygon that has segments to
+    and from the holes ("islands"), and then using the same method
+    as TriangulateFace.
+
+    Args:
+      face: list of int - indices in points, assumed CCW-oriented
+      holes: list of list of int - each sublist is like face
+          but CW-oriented and assumed to be inside face
+      points: geom.Points - holds coordinates for vertices
+    Returns:
+      list of (int, int, int) - 3-tuples are CCW-oriented vertices of
+          triangles making up the triangulation
+    """
+
+    if len(holes) == 0:
+        return TriangulateFace(face, points)
+    allfaces = [face] + holes
+    sholes = [_SortFace(h, points) for h in holes]
+    joinedface = _JoinIslands(face, sholes, points)
+    tris = EarChopTriFace(joinedface, points)
+    bord = _BorderEdges(allfaces)
+    triscdt = _CDT(tris, bord, points)
+    return triscdt
 
 
 def QuadrangulateFace(face, points):
-  """Quadrangulate the face (subdivide into convex quads and tris).
+    """Quadrangulate the face (subdivide into convex quads and tris).
 
-  Like TriangulateFace, but after triangulating, join as many pairs
-  of triangles as possible into convex quadrilaterals.
+    Like TriangulateFace, but after triangulating, join as many pairs
+    of triangles as possible into convex quadrilaterals.
 
-  Args:
-    face: list of int - indices in points, assumed CCW-oriented
-    points: geom.Points - holds coordinates for vertices
-  Returns:
-    list of 3-tuples or 4-tuples of ints - CCW-oriented vertices of
-        quadrilaterals and triangles making up the quadrangulation.
-  """
+    Args:
+      face: list of int - indices in points, assumed CCW-oriented
+      points: geom.Points - holds coordinates for vertices
+    Returns:
+      list of 3-tuples or 4-tuples of ints - CCW-oriented vertices of
+          quadrilaterals and triangles making up the quadrangulation.
+    """
 
-  if len(face) <= 3:
-    return [ tuple(face) ]
-  tris = EarChopTriFace(face, points)
-  bord = _BorderEdges([face])
-  triscdt = _CDT(tris, bord, points)
-  qs = _Quandrangulate(triscdt, bord, points)
-  return qs
+    if len(face) <= 3:
+        return [tuple(face)]
+    tris = EarChopTriFace(face, points)
+    bord = _BorderEdges([face])
+    triscdt = _CDT(tris, bord, points)
+    qs = _Quandrangulate(triscdt, bord, points)
+    return qs
 
 
 def QuadrangulateFaceWithHoles(face, holes, points):
-  """Like QuadrangulateFace, but with holes inside the faces.
-
-  Args:
-    face: list of int - indices in points, assumed CCW-oriented
-    holes: list of list of int - each sublist is like face
-        but CW-oriented and assumed to be inside face
-    points: geom.Points - holds coordinates for vertices
-  Returns:
-    list of 3-tuples or 4-tuples of ints - CCW-oriented vertices of
-        quadrilaterals and triangles making up the quadrangulation.
-  """
-
-  if len(holes) == 0:
-    return QuadrangulateFace(face, points)
-  allfaces = [face] + holes
-  sholes = [ _SortFace(h, points) for h in holes ]
-  joinedface = _JoinIslands(face, sholes, points)
-  tris = EarChopTriFace(joinedface, points)
-  bord = _BorderEdges(allfaces)
-  triscdt = _CDT(tris, bord, points)
-  qs = _Quandrangulate(triscdt, bord, points)
-  return qs
+    """Like QuadrangulateFace, but with holes inside the faces.
+
+    Args:
+      face: list of int - indices in points, assumed CCW-oriented
+      holes: list of list of int - each sublist is like face
+          but CW-oriented and assumed to be inside face
+      points: geom.Points - holds coordinates for vertices
+    Returns:
+      list of 3-tuples or 4-tuples of ints - CCW-oriented vertices of
+          quadrilaterals and triangles making up the quadrangulation.
+    """
+
+    if len(holes) == 0:
+        return QuadrangulateFace(face, points)
+    allfaces = [face] + holes
+    sholes = [_SortFace(h, points) for h in holes]
+    joinedface = _JoinIslands(face, sholes, points)
+    tris = EarChopTriFace(joinedface, points)
+    bord = _BorderEdges(allfaces)
+    triscdt = _CDT(tris, bord, points)
+    qs = _Quandrangulate(triscdt, bord, points)
+    return qs
 
 
 def _SortFace(face, points):
-  """Rotate face so leftmost vertex is first, where face is
-  list of indices in points."""
+    """Rotate face so leftmost vertex is first, where face is
+    list of indices in points."""
 
-  n = len(face)
-  if n <= 1:
-    return face
-  lefti = 0
-  leftv = face[0]
-  for i in range(1, n):
-    # following comparison is lexicographic on n-tuple
-    # so sorts on x first, using lower y as tie breaker.
-    if points.pos[face[i]] < points.pos[leftv]:
-      lefti = i
-      leftv = face[i]
-  return face[lefti:] + face[0:lefti]
+    n = len(face)
+    if n <= 1:
+        return face
+    lefti = 0
+    leftv = face[0]
+    for i in range(1, n):
+        # following comparison is lexicographic on n-tuple
+        # so sorts on x first, using lower y as tie breaker.
+        if points.pos[face[i]] < points.pos[leftv]:
+            lefti = i
+            leftv = face[i]
+    return face[lefti:] + face[0:lefti]
 
 
 def EarChopTriFace(face, points):
-  """Triangulate given face, with coords given by indexing into points.
-  Return list of faces, each of which will be a triangle.
-  Use the ear-chopping method."""
-
-  # start with lowest coord in 2d space to try
-  # to get a pleasing uniform triangulation if starting with
-  # a regular structure (like a grid)
-  start = _GetLeastIndex(face, points)
-  ans = []
-  incr = 1
-  n = len(face)
-  while n > 3:
-    i = _FindEar(face, n, start, incr, points)
-    vm1 = face[(i - 1) % n]
-    v0 = face[i]
-    v1 = face[(i + 1) % n]
-    face = _ChopEar(face, i)
+    """Triangulate given face, with coords given by indexing into points.
+    Return list of faces, each of which will be a triangle.
+    Use the ear-chopping method."""
+
+    # start with lowest coord in 2d space to try
+    # to get a pleasing uniform triangulation if starting with
+    # a regular structure (like a grid)
+    start = _GetLeastIndex(face, points)
+    ans = []
+    incr = 1
     n = len(face)
-    incr = - incr
-    if incr == 1:
-      start = i % n
-    else:
-      start = (i - 1) % n
-    ans.append((vm1, v0, v1))
-  ans.append(tuple(face))
-  return ans
+    while n > 3:
+        i = _FindEar(face, n, start, incr, points)
+        vm1 = face[(i - 1) % n]
+        v0 = face[i]
+        v1 = face[(i + 1) % n]
+        face = _ChopEar(face, i)
+        n = len(face)
+        incr = - incr
+        if incr == 1:
+            start = i % n
+        else:
+            start = (i - 1) % n
+        ans.append((vm1, v0, v1))
+    ans.append(tuple(face))
+    return ans
 
 
 def _GetLeastIndex(face, points):
-  """Return index of coordinate that is leftmost, lowest in face."""
+    """Return index of coordinate that is leftmost, lowest in face."""
 
-  bestindex = 0
-  bestpos = points.pos[face[0]]
-  for i in range(1, len(face)):
-    pos = points.pos[face[i]]
-    if pos[0] < bestpos[0] or (pos[0] == bestpos[0] and pos[1] < bestpos[1]):
-      bestindex = i
-      bestpos = pos
-  return bestindex
+    bestindex = 0
+    bestpos = points.pos[face[0]]
+    for i in range(1, len(face)):
+        pos = points.pos[face[i]]
+        if pos[0] < bestpos[0] or \
+                (pos[0] == bestpos[0] and pos[1] < bestpos[1]):
+            bestindex = i
+            bestpos = pos
+    return bestindex
 
 
 def _FindEar(face, n, start, incr, points):
-  """An ear of a polygon consists of three consecutive vertices
-  v(-1), v0, v1 such that v(-1) can connect to v1 without intersecting
-  the polygon.
-  Finds an ear, starting at index 'start' and moving
-  in direction incr. (We attempt to alternate directions, to find
-  'nice' triangulations for simple convex polygons.)
-  Returns index into faces of v0 (will always find one, because
-  uses a desperation mode if fails to find one with above rule)."""
-
-  angk = _ClassifyAngles(face, n, points)
-  for mode in range(0, 5):
-    i = start
-    while True:
-      if _IsEar(face, i, n, angk, points, mode):
-        return i
-      i = (i + incr) % n
-      if i == start:
-        break  # try next higher desperation mode
+    """An ear of a polygon consists of three consecutive vertices
+    v(-1), v0, v1 such that v(-1) can connect to v1 without intersecting
+    the polygon.
+    Finds an ear, starting at index 'start' and moving
+    in direction incr. (We attempt to alternate directions, to find
+    'nice' triangulations for simple convex polygons.)
+    Returns index into faces of v0 (will always find one, because
+    uses a desperation mode if fails to find one with above rule)."""
+
+    angk = _ClassifyAngles(face, n, points)
+    for mode in range(0, 5):
+        i = start
+        while True:
+            if _IsEar(face, i, n, angk, points, mode):
+                return i
+            i = (i + incr) % n
+            if i == start:
+                break  # try next higher desperation mode
 
 
 def _IsEar(face, i, n, angk, points, mode):
-  """Return true, false depending on ear status of vertices
-  with indices i-1, i, i+1.
-  mode is amount of desperation: 0 is Normal mode,
-  mode 1 allows degenerate triangles (with repeated vertices)
-  mode 2 allows local self crossing (folded) ears
-  mode 3 allows any convex vertex (should always be one)
-  mode 4 allows anything (just to be sure loop terminates!)"""
-
-  k = angk[i]
-  vm2 = face[(i - 2) % n]
-  vm1 = face[(i - 1) % n]
-  v0 = face[i]
-  v1 = face[(i + 1) % n]
-  v2 = face[(i + 2) % n]
-  if vm1 == v0 or v0 == v1:
-    return (mode > 0)
-  b = (k == Angconvex or k == Angtangential or k == Ang0)
-  c = _InCone(vm1, v0, v1, v2, angk[(i + 1) % n], points) and \
-      _InCone(v1, vm2, vm1, v0, angk[(i - 1) % n], points)
-  if b and c:
-    return _EarCheck(face, n, angk, vm1, v0, v1, points)
-  if mode < 2:
-    return False
-  if mode == 3:
-    return SegsIntersect(vm2, vm1, v0, v1, points)
-  if mode == 4:
-    return b
-  return True
+    """Return true, false depending on ear status of vertices
+    with indices i-1, i, i+1.
+    mode is amount of desperation: 0 is Normal mode,
+    mode 1 allows degenerate triangles (with repeated vertices)
+    mode 2 allows local self crossing (folded) ears
+    mode 3 allows any convex vertex (should always be one)
+    mode 4 allows anything (just to be sure loop terminates!)"""
+
+    k = angk[i]
+    vm2 = face[(i - 2) % n]
+    vm1 = face[(i - 1) % n]
+    v0 = face[i]
+    v1 = face[(i + 1) % n]
+    v2 = face[(i + 2) % n]
+    if vm1 == v0 or v0 == v1:
+        return (mode > 0)
+    b = (k == Angconvex or k == Angtangential or k == Ang0)
+    c = _InCone(vm1, v0, v1, v2, angk[(i + 1) % n], points) and \
+        _InCone(v1, vm2, vm1, v0, angk[(i - 1) % n], points)
+    if b and c:
+        return _EarCheck(face, n, angk, vm1, v0, v1, points)
+    if mode < 2:
+        return False
+    if mode == 3:
+        return SegsIntersect(vm2, vm1, v0, v1, points)
+    if mode == 4:
+        return b
+    return True
 
 
 def _EarCheck(face, n, angk, vm1, v0, v1, points):
-  """Return True if the successive vertices vm1, v0, v1
-  forms an ear.  We already know that it is not a reflex
-  Angle, and that the local cone containment is ok.
-  What remains to check is that the edge vm1-v1 doesn't
-  intersect any other edge of the face (besides vm1-v0
-  and v0-v1).  Equivalently, there can't be a reflex Angle
-  inside the triangle vm1-v0-v1.  (Well, there are
-  messy cases when other points of the face coincide with
-  v0 or touch various lines involved in the ear.)"""
-  for j in range(0, n):
-    fv = face[j]
-    k = angk[j]
-    b = (k == Angreflex or k == Ang360) \
-	and not(fv == vm1 or fv == v0 or fv == v1)
-    if b:
-      # Is fv inside closure of triangle (vm1,v0,v1)?
-      c = not(Ccw(v0, vm1, fv, points) \
-		    or Ccw(vm1, v1, fv, points) \
-		    or Ccw(v1, v0, fv, points))
-      fvm1 = face[(j - 1) % n]
-      fv1 = face[(j + 1) % n]
-      # To try to deal with some degenerate cases,
-      # also check to see if either segment attached to fv
-      # intersects either segment of potential ear.
-      d = SegsIntersect(fvm1, fv, vm1, v0, points) or \
-		SegsIntersect(fvm1, fv, v0, v1, points) or \
-		SegsIntersect(fv, fv1, vm1, v0, points) or \
-		SegsIntersect(fv, fv1, v0, v1, points)
-      if c or d:
-        return False
-  return True
+    """Return True if the successive vertices vm1, v0, v1
+    forms an ear.  We already know that it is not a reflex
+    Angle, and that the local cone containment is ok.
+    What remains to check is that the edge vm1-v1 doesn't
+    intersect any other edge of the face (besides vm1-v0
+    and v0-v1).  Equivalently, there can't be a reflex Angle
+    inside the triangle vm1-v0-v1.  (Well, there are
+    messy cases when other points of the face coincide with
+    v0 or touch various lines involved in the ear.)"""
+    for j in range(0, n):
+        fv = face[j]
+        k = angk[j]
+        b = (k == Angreflex or k == Ang360) \
+            and not(fv == vm1 or fv == v0 or fv == v1)
+        if b:
+            # Is fv inside closure of triangle (vm1,v0,v1)?
+            c = not(Ccw(v0, vm1, fv, points) \
+                          or Ccw(vm1, v1, fv, points) \
+                          or Ccw(v1, v0, fv, points))
+            fvm1 = face[(j - 1) % n]
+            fv1 = face[(j + 1) % n]
+            # To try to deal with some degenerate cases,
+            # also check to see if either segment attached to fv
+            # intersects either segment of potential ear.
+            d = SegsIntersect(fvm1, fv, vm1, v0, points) or \
+                      SegsIntersect(fvm1, fv, v0, v1, points) or \
+                      SegsIntersect(fv, fv1, vm1, v0, points) or \
+                      SegsIntersect(fv, fv1, v0, v1, points)
+            if c or d:
+                return False
+    return True
 
 
 def _ChopEar(face, i):
-  """Return a copy of face (of length n), omitting element i."""
+    """Return a copy of face (of length n), omitting element i."""
 
-  return face[0:i] + face[i + 1:]
+    return face[0:i] + face[i + 1:]
 
 
 def _InCone(vtest, a, b, c, bkind, points):
-  """Return true if point with index vtest is in Cone of points with
-  indices a, b, c, where Angle ABC has AngleKind Bkind.
-  The Cone is the set of points inside the left face defined by
-  segments ab and bc, disregarding all other segments of polygon for
-  purposes of inside test."""
-
-  if bkind == Angreflex or bkind == Ang360:
-    if _InCone(vtest, c, b, a, Angconvex, points):
-      return False
-    return not((not(Ccw(b, a, vtest, points)) \
-			 and not(Ccw(b, vtest, a, points)) \
-			 and Ccw(b, a, vtest, points))
-			or
-			(not(Ccw(b, c, vtest, points)) \
-			 and not(Ccw(b, vtest, c, points)) \
-			 and Ccw(b, a, vtest, points)))
-  else:
-    return Ccw(a, b, vtest, points) and Ccw(b, c, vtest, points)
+    """Return true if point with index vtest is in Cone of points with
+    indices a, b, c, where Angle ABC has AngleKind Bkind.
+    The Cone is the set of points inside the left face defined by
+    segments ab and bc, disregarding all other segments of polygon for
+    purposes of inside test."""
+
+    if bkind == Angreflex or bkind == Ang360:
+        if _InCone(vtest, c, b, a, Angconvex, points):
+            return False
+        return not((not(Ccw(b, a, vtest, points)) \
+                             and not(Ccw(b, vtest, a, points)) \
+                             and Ccw(b, a, vtest, points))
+                            or
+                            (not(Ccw(b, c, vtest, points)) \
+                             and not(Ccw(b, vtest, c, points)) \
+                             and Ccw(b, a, vtest, points)))
+    else:
+        return Ccw(a, b, vtest, points) and Ccw(b, c, vtest, points)
 
 
 def _JoinIslands(face, holes, points):
-  """face is a CCW face containing the CW faces in the holes list,
-  where each hole is sorted so the leftmost-lowest vertex is first.
-  faces and holes are given as lists of indices into points.
-  The holes should be sorted by softface.
-  Add edges to make a new face that includes the holes (a Ccw traversal
-  of the new face will have the inside always on the left),
-  and return the new face."""
-
-  while len(holes) > 0:
-    (hole, holeindex) = _LeftMostFace(holes, points)
-    holes = holes[0:holeindex] + holes[holeindex + 1:]
-    face = _JoinIsland(face, hole, points)
-  return face
+    """face is a CCW face containing the CW faces in the holes list,
+    where each hole is sorted so the leftmost-lowest vertex is first.
+    faces and holes are given as lists of indices into points.
+    The holes should be sorted by softface.
+    Add edges to make a new face that includes the holes (a Ccw traversal
+    of the new face will have the inside always on the left),
+    and return the new face."""
+
+    while len(holes) > 0:
+        (hole, holeindex) = _LeftMostFace(holes, points)
+        holes = holes[0:holeindex] + holes[holeindex + 1:]
+        face = _JoinIsland(face, hole, points)
+    return face
 
 
 def _JoinIsland(face, hole, points):
-  """Return a modified version of face that splices in the
-  vertices of hole (which should be sorted)."""
+    """Return a modified version of face that splices in the
+    vertices of hole (which should be sorted)."""
 
-  if len(hole) == 0:
-    return face
-  hv0 = hole[0]
-  d = _FindDiag(face, hv0, points)
-  newface = face[0:d + 1] + hole + [hv0] + face[d:]
-  return newface
+    if len(hole) == 0:
+        return face
+    hv0 = hole[0]
+    d = _FindDiag(face, hv0, points)
+    newface = face[0:d + 1] + hole + [hv0] + face[d:]
+    return newface
 
 
 def _LeftMostFace(holes, points):
-  """Return (hole,index of hole in holes) where hole has
-  the leftmost first vertex.  To be able to handle empty
-  holes gracefully, call an empty hole 'leftmost'.
-  Assumes holes are sorted by softface."""
-
-  assert(len(holes) > 0)
-  lefti = 0
-  lefthole = holes[0]
-  if len(lefthole) == 0:
+    """Return (hole,index of hole in holes) where hole has
+    the leftmost first vertex.  To be able to handle empty
+    holes gracefully, call an empty hole 'leftmost'.
+    Assumes holes are sorted by softface."""
+
+    assert(len(holes) > 0)
+    lefti = 0
+    lefthole = holes[0]
+    if len(lefthole) == 0:
+        return (lefthole, lefti)
+    leftv = lefthole[0]
+    for i in range(1, len(holes)):
+        ihole = holes[i]
+        if len(ihole) == 0:
+            return (ihole, i)
+        iv = ihole[0]
+        if points.pos[iv] < points.pos[leftv]:
+            (lefti, lefthole, leftv) = (i, ihole, iv)
     return (lefthole, lefti)
-  leftv = lefthole[0]
-  for i in range(1, len(holes)):
-    ihole = holes[i]
-    if len(ihole) == 0:
-      return (ihole, i)
-    iv = ihole[0]
-    if points.pos[iv] < points.pos[leftv]:
-      (lefti, lefthole, leftv) = (i, ihole, iv)
-  return (lefthole, lefti)
 
 
 def _FindDiag(face, hv, points):
-  """Find a vertex in face that can see vertex hv, if possible,
-  and return the index into face of that vertex.
-  Should be able to find a diagonal that connects a vertex of face
-  left of v to hv without crossing face, but try two
-  more desperation passes after that to get SOME diagonal, even if
-  it might cross some edge somewhere.
-  First desperation pass (mode == 1): allow points right of hv.
-  Second desperation pass (mode == 2): allow crossing boundary poly"""
-
-  besti = - 1
-  bestdist = 1e30
-  for mode in range(0, 3):
-    for i in range(0, len(face)):
-      v = face[i]
-      if mode == 0 and points.pos[v] > points.pos[hv]:
-        continue  # in mode 0, only want points left of hv
-      dist = _DistSq(v, hv, points)
-      if dist < bestdist:
-        if _IsDiag(i, v, hv, face, points) or mode == 2:
-          (besti, bestdist) = (i, dist)
-    if besti >= 0:
-      break  # found one, so don't need other modes
-  assert(besti >= 0)
-  return besti
+    """Find a vertex in face that can see vertex hv, if possible,
+    and return the index into face of that vertex.
+    Should be able to find a diagonal that connects a vertex of face
+    left of v to hv without crossing face, but try two
+    more desperation passes after that to get SOME diagonal, even if
+    it might cross some edge somewhere.
+    First desperation pass (mode == 1): allow points right of hv.
+    Second desperation pass (mode == 2): allow crossing boundary poly"""
+
+    besti = - 1
+    bestdist = 1e30
+    for mode in range(0, 3):
+        for i in range(0, len(face)):
+            v = face[i]
+            if mode == 0 and points.pos[v] > points.pos[hv]:
+                continue  # in mode 0, only want points left of hv
+            dist = _DistSq(v, hv, points)
+            if dist < bestdist:
+                if _IsDiag(i, v, hv, face, points) or mode == 2:
+                    (besti, bestdist) = (i, dist)
+        if besti >= 0:
+            break  # found one, so don't need other modes
+    assert(besti >= 0)
+    return besti
 
 
 def _IsDiag(i, v, hv, face, points):
-  """Return True if vertex v (at index i in face) can see vertex hv.
-  v and hv are indices into points.
-  (v, hv) is a diagonal if hv is in the cone of the Angle at index i on face
-  and no segment in face intersects (h, hv).
-  """
-
-  n = len(face)
-  vm1 = face[(i - 1) % n]
-  v1 = face[(i + 1) % n]
-  k = _AngleKind(vm1, v, v1, points)
-  if not _InCone(hv, vm1, v, v1, k, points):
-    return False
-  for j in range(0, n):
-    vj = face[j]
-    vj1 = face[(j + 1) % n]
-    if SegsIntersect(v, hv, vj, vj1, points):
-      return False
-  return True
+    """Return True if vertex v (at index i in face) can see vertex hv.
+    v and hv are indices into points.
+    (v, hv) is a diagonal if hv is in the cone of the Angle at index i on face
+    and no segment in face intersects (h, hv).
+    """
+
+    n = len(face)
+    vm1 = face[(i - 1) % n]
+    v1 = face[(i + 1) % n]
+    k = _AngleKind(vm1, v, v1, points)
+    if not _InCone(hv, vm1, v, v1, k, points):
+        return False
+    for j in range(0, n):
+        vj = face[j]
+        vj1 = face[(j + 1) % n]
+        if SegsIntersect(v, hv, vj, vj1, points):
+            return False
+    return True
 
 
 def _DistSq(a, b, points):
-  """Return distance squared between coords with indices a and b in points."""
+    """Return distance squared between coords with indices a and b in points.
+    """
 
-  diff = Sub2(points.pos[a], points.pos[b])
-  return Dot2(diff, diff)
+    diff = Sub2(points.pos[a], points.pos[b])
+    return Dot2(diff, diff)
 
 
 def _BorderEdges(facelist):
-  """Return a set of (u,v) where u and v are successive vertex indices
-  in some face in the list in facelist."""
+    """Return a set of (u,v) where u and v are successive vertex indices
+    in some face in the list in facelist."""
 
-  ans = set()
-  for i in range(0, len(facelist)):
-    f = facelist[i]
-    for j in range(1, len(f)):
-      ans.add((f[j - 1], f[j]))
-    ans.add((f[ - 1], f[0]))
-  return ans
+    ans = set()
+    for i in range(0, len(facelist)):
+        f = facelist[i]
+        for j in range(1, len(f)):
+            ans.add((f[j - 1], f[j]))
+        ans.add((f[-1], f[0]))
+    return ans
 
 
 def _CDT(tris, bord, points):
-  """Tris is a list of triangles ((a,b,c), CCW-oriented indices into points)
-  Bord is a set of border edges (u,v), oriented so that tris
-  is a triangulation of the left face of the border(s).
-  Make the triangulation "Constrained Delaunay" by flipping "reversed"
-  quadrangulaterals until can flip no more.
-  Return list of triangles in new triangulation."""
-
-  td = _TriDict(tris)
-  re = _ReveresedEdges(tris, td, bord, points)
-  ts = set(tris)
-  # reverse the reversed edges until done.
-  # reversing and edge adds new edges, which may or
-  # may not be reversed or border edges, to re for
-  # consideration, but the process will stop eventually.
-  while len(re) > 0:
-    (a, b) = e = re.pop()
-    if e in bord or not _IsReversed(e, td, points):
-      continue
-    # rotate e in quad adbc to get other diagonal
-    erev = (b, a)
-    tl = td.get(e)
-    tr = td.get(erev)
-    if not tl or not tr:
-      continue  # shouldn't happen
-    c = _OtherVert(tl, a, b)
-    d = _OtherVert(tr, a, b)
-    if c is None or d is None:
-      continue  # shouldn't happen
-    newt1 = (c, d, b)
-    newt2 = (c, a, d)
-    del td[e]
-    del td[erev]
-    td[(c, d)] = newt1
-    td[(d, b)] = newt1
-    td[(b, c)] = newt1
-    td[(d, c)] = newt2
-    td[(c, a)] = newt2
-    td[(a, d)] = newt2
-    if tl in ts:
-      ts.remove(tl)
-    if tr in ts:
-      ts.remove(tr)
-    ts.add(newt1)
-    ts.add(newt2)
-    re.extend([(d, b), (b, c), (c, a), (a, d)])
-  return list(ts)
+    """Tris is a list of triangles ((a,b,c), CCW-oriented indices into points)
+    Bord is a set of border edges (u,v), oriented so that tris
+    is a triangulation of the left face of the border(s).
+    Make the triangulation "Constrained Delaunay" by flipping "reversed"
+    quadrangulaterals until can flip no more.
+    Return list of triangles in new triangulation."""
+
+    td = _TriDict(tris)
+    re = _ReveresedEdges(tris, td, bord, points)
+    ts = set(tris)
+    # reverse the reversed edges until done.
+    # reversing and edge adds new edges, which may or
+    # may not be reversed or border edges, to re for
+    # consideration, but the process will stop eventually.
+    while len(re) > 0:
+        (a, b) = e = re.pop()
+        if e in bord or not _IsReversed(e, td, points):
+            continue
+        # rotate e in quad adbc to get other diagonal
+        erev = (b, a)
+        tl = td.get(e)
+        tr = td.get(erev)
+        if not tl or not tr:
+            continue  # shouldn't happen
+        c = _OtherVert(tl, a, b)
+        d = _OtherVert(tr, a, b)
+        if c is None or d is None:
+            continue  # shouldn't happen
+        newt1 = (c, d, b)
+        newt2 = (c, a, d)
+        del td[e]
+        del td[erev]
+        td[(c, d)] = newt1
+        td[(d, b)] = newt1
+        td[(b, c)] = newt1
+        td[(d, c)] = newt2
+        td[(c, a)] = newt2
+        td[(a, d)] = newt2
+        if tl in ts:
+            ts.remove(tl)
+        if tr in ts:
+            ts.remove(tr)
+        ts.add(newt1)
+        ts.add(newt2)
+        re.extend([(d, b), (b, c), (c, a), (a, d)])
+    return list(ts)
 
 
 def _TriDict(tris):
-  """tris is a list of triangles (a,b,c), CCW-oriented indices.
-  Return dict mapping all edges in the triangles to the containing
-  triangle list."""
+    """tris is a list of triangles (a,b,c), CCW-oriented indices.
+    Return dict mapping all edges in the triangles to the containing
+    triangle list."""
 
-  ans = dict()
-  for i in range(0, len(tris)):
-    (a, b, c) = t = tris[i]
-    ans[(a, b)] = t
-    ans[(b, c)] = t
-    ans[(c, a)] = t
-  return ans
+    ans = dict()
+    for i in range(0, len(tris)):
+        (a, b, c) = t = tris[i]
+        ans[(a, b)] = t
+        ans[(b, c)] = t
+        ans[(c, a)] = t
+    return ans
 
 
 def _ReveresedEdges(tris, td, bord, points):
-  """Return list of reversed edges in tris.
-  Only want edges not in bord, and only need one representative
-  of (u,v)/(v,u), so choose the one with u < v.
-  td is dictionary from _TriDict, and is used to find left and right
-  triangles of edges."""
-
-  ans = []
-  for i in range(0, len(tris)):
-    (a, b, c) = tris[i]
-    for e in [(a, b), (b, c), (c, a)]:
-      if e in bord:
-        continue
-      (u, v) = e
-      if u < v:
-        if _IsReversed(e, td, points):
-          ans.append(e)
-  return ans
+    """Return list of reversed edges in tris.
+    Only want edges not in bord, and only need one representative
+    of (u,v)/(v,u), so choose the one with u < v.
+    td is dictionary from _TriDict, and is used to find left and right
+    triangles of edges."""
+
+    ans = []
+    for i in range(0, len(tris)):
+        (a, b, c) = tris[i]
+        for e in [(a, b), (b, c), (c, a)]:
+            if e in bord:
+                continue
+            (u, v) = e
+            if u < v:
+                if _IsReversed(e, td, points):
+                    ans.append(e)
+    return ans
 
 
 def _IsReversed(e, td, points):
-  """If e=(a,b) is a non-border edge, with left-face triangle tl and
-  right-face triangle tr, then it is 'reversed' if the circle through
-  a, b, and (say) the other vertex of tl containts the other vertex of tr.
-  td is a _TriDict, for finding triangles containing edges, and points
-  gives the coordinates for vertex indices used in edges."""
-
-  tl = td.get(e)
-  if not tl:
-    return False
-  (a, b) = e
-  tr = td.get((b, a))
-  if not tr:
-    return False
-  c = _OtherVert(tl, a, b)
-  d = _OtherVert(tr, a, b)
-  if c is None or d is None:
-    return False
-  return InCircle(a, b, c, d, points)
+    """If e=(a,b) is a non-border edge, with left-face triangle tl and
+    right-face triangle tr, then it is 'reversed' if the circle through
+    a, b, and (say) the other vertex of tl containts the other vertex of tr.
+    td is a _TriDict, for finding triangles containing edges, and points
+    gives the coordinates for vertex indices used in edges."""
+
+    tl = td.get(e)
+    if not tl:
+        return False
+    (a, b) = e
+    tr = td.get((b, a))
+    if not tr:
+        return False
+    c = _OtherVert(tl, a, b)
+    d = _OtherVert(tr, a, b)
+    if c is None or d is None:
+        return False
+    return InCircle(a, b, c, d, points)
 
 
 def _OtherVert(tri, a, b):
-  """tri should be a tuple of 3 vertex indices, two of which are a and b.
-  Return the third index, or None if all vertices are a or b"""
+    """tri should be a tuple of 3 vertex indices, two of which are a and b.
+    Return the third index, or None if all vertices are a or b"""
 
-  for v in tri:
-    if v != a and v != b:
-      return v
-  return None
+    for v in tri:
+        if v != a and v != b:
+            return v
+    return None
 
 
 def _ClassifyAngles(face, n, points):
-  """Return vector of anglekinds of the Angle around each point in face."""
+    """Return vector of anglekinds of the Angle around each point in face."""
 
-  return [_AngleKind(face[(i - 1) % n], face[i], face[(i + 1) % n], points) for i in list(range(0, n))]
+    return [_AngleKind(face[(i - 1) % n], face[i], face[(i + 1) % n], points) \
+        for i in list(range(0, n))]
 
 
 def _AngleKind(a, b, c, points):
-  """Return one of the Ang... constants to classify Angle formed by ABC,
-  in a counterclockwise traversal of a face,
-  where a, b, c are indices into points."""
-
-  if Ccw(a, b, c, points):
-    return Angconvex
-  elif Ccw(a, c, b, points):
-    return Angreflex
-  else:
-    vb = points.pos[b]
-    udotv = Dot2(Sub2(vb, points.pos[a]), Sub2(points.pos[c], vb))
-    if udotv > 0.0:
-      return Angtangential
+    """Return one of the Ang... constants to classify Angle formed by ABC,
+    in a counterclockwise traversal of a face,
+    where a, b, c are indices into points."""
+
+    if Ccw(a, b, c, points):
+        return Angconvex
+    elif Ccw(a, c, b, points):
+        return Angreflex
     else:
-      return Ang0   # to fix: return Ang360 if "inside" spur
+        vb = points.pos[b]
+        udotv = Dot2(Sub2(vb, points.pos[a]), Sub2(points.pos[c], vb))
+        if udotv > 0.0:
+            return Angtangential
+        else:
+            return Ang0   # to fix: return Ang360 if "inside" spur
 
 
 def _Quandrangulate(tris, bord, points):
-  """Tris is list of triangles, forming a triangulation of region whose
-  border edges are in set bord.
-  Combine adjacent triangles to make quads, trying for "good" quads where
-  possible. Some triangles will probably remain uncombined"""
-
-  (er, td) = _ERGraph(tris, bord, points)
-  if len(er) == 0:
-    return tris
-  if len(er) > GTHRESH:
-    match = _GreedyMatch(er)
-  else:
-    match = _MaxMatch(er)
-  return _RemoveEdges(tris, match)
+    """Tris is list of triangles, forming a triangulation of region whose
+    border edges are in set bord.
+    Combine adjacent triangles to make quads, trying for "good" quads where
+    possible. Some triangles will probably remain uncombined"""
+
+    (er, td) = _ERGraph(tris, bord, points)
+    if len(er) == 0:
+        return tris
+    if len(er) > GTHRESH:
+        match = _GreedyMatch(er)
+    else:
+        match = _MaxMatch(er)
+    return _RemoveEdges(tris, match)
 
 
 def _RemoveEdges(tris, match):
-  """tris is list of triangles.  er is as returned from _MaxMatch or _GreedyMatch.
-  Return list of (A,D,B,C) resulting from deleting edge (A,B) causing a merge
-  of two triangles; append to that list the remaining unmatched triangles."""
-
-  ans = []
-  triset = set(tris)
-  while len(match) > 0:
-    (_, e, tl, tr) = match.pop()
-    (a, b) = e
-    if tl in triset:
-      triset.remove(tl)
-    if tr in triset:
-      triset.remove(tr)
-    c = _OtherVert(tl, a, b)
-    d = _OtherVert(tr, a, b)
-    if c is None or d is None:
-      continue
-    ans.append((a, d, b, c))
-  return ans + list(triset)
+    """tris is list of triangles.
+    er is as returned from _MaxMatch or _GreedyMatch.
+
+    Return list of (A,D,B,C) resulting from deleting edge (A,B) causing a merge
+    of two triangles; append to that list the remaining unmatched triangles."""
+
+    ans = []
+    triset = set(tris)
+    while len(match) > 0:
+        (_, e, tl, tr) = match.pop()
+        (a, b) = e
+        if tl in triset:
+            triset.remove(tl)
+        if tr in triset:
+            triset.remove(tr)
+        c = _OtherVert(tl, a, b)
+        d = _OtherVert(tr, a, b)
+        if c is None or d is None:
+            continue
+        ans.append((a, d, b, c))
+    return ans + list(triset)
 
 
 def _ERGraph(tris, bord, points):
-  """Make an 'Edge Removal Graph'.
-
-  Given a list of triangles, the 'Edge Removal Graph' is a graph whose
-  nodes are the triangles (think of a point in the center of them),
-  and whose edges go between adjacent triangles (they share a non-border
-  edge), such that it would be possible to remove the shared edge
-  and form a convex quadrilateral.  Forming a quadrilateralization
-  is then a matter of finding a matching (set of edges that don't
-  share a vertex - remember, these are the 'face' vertices).
-  For better quadrilaterlization, we'll make the Edge Removal Graph
-  edges have weights, with higher weights going to the edges that
-  are more desirable to remove.  Then we want a maximum weight matching
-  in this graph.
-
-  We'll return the graph in a kind of implicit form, using edges of
-  the original triangles as a proxy for the edges between the faces
-  (i.e., the edge of the triangle is the shared edge). We'll arbitrarily
-  pick the triangle graph edge with lower-index start vertex.
-  Also, to aid in traversing the implicit graph, we'll keep the left
-  and right triangle triples with edge 'ER edge'.
-  Finally, since we calculate it anyway, we'll return a dictionary
-  mapping edges of the triangles to the triangle triples they're in.
-
-  Args:
-    tris: list of (int, int, int) giving a triple of vertex indices for
-        triangles, assumed CCW oriented
-    bord: set of (int, int) giving vertex indices for border edges
-    points: geom.Points - for mapping vertex indices to coords
-  Returns:
-    (list of (weight,e,tl,tr), dict)
-      where edge e=(a,b) is non-border edge
-      with left face tl and right face tr (each a triple (i,j,k)),
-      where removing the edge would form an "OK" quad (no concave angles),
-      with weight representing the desirability of removing the edge
-      The dict maps int pairs (a,b) to int triples (i,j,k), that is,
-      mapping edges to their containing triangles.
-  """
-
-  td = _TriDict(tris)
-  dd = _DegreeDict(tris)
-  ans = []
-  ctris = tris[:]  # copy, so argument not affected
-  while len(ctris) > 0:
-    (i, j, k) = tl = ctris.pop()
-    for e in [(i, j), (j, k), (k, i)]:
-      if e in bord:
-        continue
-      (a, b) = e
-      # just consider one of (a,b) and (b,a), to avoid dups
-      if a > b:
-        continue
-      erev = (b, a)
-      tr = td.get(erev)
-      if not tr:
-        continue
-      c = _OtherVert(tl, a, b)
-      d = _OtherVert(tr, a, b)
-      if c is None or d is None:
-        continue
-      # calculate amax, the max of the new angles that would
-      # be formed at a and b if tl and tr were combined
-      amax = max(Angle(c, a, b, points) + Angle(d, a, b, points),
-                 Angle(c, b, a, points) + Angle(d, b, a, points))
-      if amax > 180.0:
-        continue
-      weight = ANGFAC * (180.0 - amax) + DEGFAC * (dd[a] + dd[b])
-      ans.append((weight, e, tl, tr))
-  return (ans, td)
+    """Make an 'Edge Removal Graph'.
+
+    Given a list of triangles, the 'Edge Removal Graph' is a graph whose
+    nodes are the triangles (think of a point in the center of them),
+    and whose edges go between adjacent triangles (they share a non-border
+    edge), such that it would be possible to remove the shared edge
+    and form a convex quadrilateral.  Forming a quadrilateralization
+    is then a matter of finding a matching (set of edges that don't
+    share a vertex - remember, these are the 'face' vertices).
+    For better quadrilaterlization, we'll make the Edge Removal Graph
+    edges have weights, with higher weights going to the edges that
+    are more desirable to remove.  Then we want a maximum weight matching
+    in this graph.
+
+    We'll return the graph in a kind of implicit form, using edges of
+    the original triangles as a proxy for the edges between the faces
+    (i.e., the edge of the triangle is the shared edge). We'll arbitrarily
+    pick the triangle graph edge with lower-index start vertex.
+    Also, to aid in traversing the implicit graph, we'll keep the left
+    and right triangle triples with edge 'ER edge'.
+    Finally, since we calculate it anyway, we'll return a dictionary
+    mapping edges of the triangles to the triangle triples they're in.
+
+    Args:
+      tris: list of (int, int, int) giving a triple of vertex indices for
+          triangles, assumed CCW oriented
+      bord: set of (int, int) giving vertex indices for border edges
+      points: geom.Points - for mapping vertex indices to coords
+    Returns:
+      (list of (weight,e,tl,tr), dict)
+        where edge e=(a,b) is non-border edge
+        with left face tl and right face tr (each a triple (i,j,k)),
+        where removing the edge would form an "OK" quad (no concave angles),
+        with weight representing the desirability of removing the edge
+        The dict maps int pairs (a,b) to int triples (i,j,k), that is,
+        mapping edges to their containing triangles.
+    """
+
+    td = _TriDict(tris)
+    dd = _DegreeDict(tris)
+    ans = []
+    ctris = tris[:]  # copy, so argument not affected
+    while len(ctris) > 0:
+        (i, j, k) = tl = ctris.pop()
+        for e in [(i, j), (j, k), (k, i)]:
+            if e in bord:
+                continue
+            (a, b) = e
+            # just consider one of (a,b) and (b,a), to avoid dups
+            if a > b:
+                continue
+            erev = (b, a)
+            tr = td.get(erev)
+            if not tr:
+                continue
+            c = _OtherVert(tl, a, b)
+            d = _OtherVert(tr, a, b)
+            if c is None or d is None:
+                continue
+            # calculate amax, the max of the new angles that would
+            # be formed at a and b if tl and tr were combined
+            amax = max(Angle(c, a, b, points) + Angle(d, a, b, points),
+                       Angle(c, b, a, points) + Angle(d, b, a, points))
+            if amax > 180.0:
+                continue
+            weight = ANGFAC * (180.0 - amax) + DEGFAC * (dd[a] + dd[b])
+            ans.append((weight, e, tl, tr))
+    return (ans, td)
 
 
 def _GreedyMatch(er):
-  """er is list of (weight,e,tl,tr).
-  Find maximal set so that each triangle appears in at most one member of set"""
+    """er is list of (weight,e,tl,tr).
+
+    Find maximal set so that each triangle appears in at most
+    one member of set"""
 
-  er.sort(key = lambda v: v[0], reverse=True)  # sort in order of decreasing weight
-  match = set()
-  ans = []
-  while len(er) > 0:
-    (_, _, tl, tr) = q = er.pop()
-    if tl not in match and tr not in match:
-      match.add(tl)
-      match.add(tr)
-      ans.append(q)
-  return ans
+    # sort in order of decreasing weight
+    er.sort(key=lambda v: v[0], reverse=True)
+    match = set()
+    ans = []
+    while len(er) > 0:
+        (_, _, tl, tr) = q = er.pop()
+        if tl not in match and tr not in match:
+            match.add(tl)
+            match.add(tr)
+            ans.append(q)
+    return ans
 
 
 def _MaxMatch(er):
-  """Like _GreedyMatch, but use divide and conquer to find best possible set.
+    """Like _GreedyMatch, but use divide and conquer to find best possible set.
 
-  Args:
-    er: list of (weight,e,tl,tr)  - see _ERGraph
-  Returns:
-    list that is a subset of er giving a maximum weight match
-  """
+    Args:
+      er: list of (weight,e,tl,tr)  - see _ERGraph
+    Returns:
+      list that is a subset of er giving a maximum weight match
+    """
 
-  (ans, _) = _DCMatch(er)
-  return ans
+    (ans, _) = _DCMatch(er)
+    return ans
 
 
 def _DCMatch(er):
-  """Recursive helper for _MaxMatch.
-
-  Divide and Conquer approach to finding max weight matching.
-  If we're lucky, there's an edge in er that separates the edge removal
-  graph into (at least) two separate components.  Then the max weight
-  is either one that includes that edge or excludes it - and we can
-  use a recursive call to _DCMatch to handle each component separately
-  on what remains of the graph after including/excluding the separating edge.
-  If we're not lucky, we fall back on _EMatch (see below).
-
-  Args:
-    er: list of (weight, e, tl, tr) (see _ERGraph)
-  Returns:
-    (list of (weight, e, tl, tr), float) - the subset forming a maximum
-        matching, and the total weight of the match.
-  """
-
-  if not er:
-    return ([], 0.0)
-  if len(er) == 1:
-    return (er, er[0][0])
-  match = []
-  matchw = 0.0
-  for i in range(0, len(er)):
-    (nc, comp) = _FindComponents(er, i)
-    if nc == 1:
-      # er[i] doesn't separate er
-      continue
-    (wi, _, tl, tr) = er[i]
-    if comp[tl] != comp[tr]:
-      # case 1: er separates graph
-      # compare the matches that include er[i] versus those that exclude it
-      (a, b) = _PartitionComps(er, comp, i, comp[tl], comp[tr])
-      ax = _CopyExcluding(a, tl, tr)
-      bx = _CopyExcluding(b, tl, tr)
-      (axmatch, wax) = _DCMatch(ax)
-      (bxmatch, wbx) = _DCMatch(bx)
-      if len(ax) == len(a):
-        wa = wax
-        amatch = axmatch
-      else:
-        (amatch, wa) = _DCMatch(a)
-      if len(bx) == len(b):
-        wb = wbx
-        bmatch = bxmatch
-      else:
-        (bmatch, wb) = _DCMatch(b)
-      w = wa + wb
-      wx = wax + wbx + wi
-      if w > wx:
-        match = amatch + bmatch
-        matchw = w
-      else:
-        match = [er[i]] + axmatch + bxmatch
-        matchw = wx
-    else: 
-      # case 2: er not needed to separate graph
-      (a, b) = _PartitionComps(er, comp, -1, 0, 0)
-      (amatch, wa) = _DCMatch(a)
-      (bmatch, wb) = _DCMatch(b)
-      match = amatch + bmatch
-      matchw = wa + wb
-    if match:
-      break
-  if not match:
-    return _EMatch(er)
-  return (match, matchw)
+    """Recursive helper for _MaxMatch.
+
+    Divide and Conquer approach to finding max weight matching.
+    If we're lucky, there's an edge in er that separates the edge removal
+    graph into (at least) two separate components.  Then the max weight
+    is either one that includes that edge or excludes it - and we can
+    use a recursive call to _DCMatch to handle each component separately
+    on what remains of the graph after including/excluding the separating edge.
+    If we're not lucky, we fall back on _EMatch (see below).
+
+    Args:
+      er: list of (weight, e, tl, tr) (see _ERGraph)
+    Returns:
+      (list of (weight, e, tl, tr), float) - the subset forming a maximum
+          matching, and the total weight of the match.
+    """
+
+    if not er:
+        return ([], 0.0)
+    if len(er) == 1:
+        return (er, er[0][0])
+    match = []
+    matchw = 0.0
+    for i in range(0, len(er)):
+        (nc, comp) = _FindComponents(er, i)
+        if nc == 1:
+            # er[i] doesn't separate er
+            continue
+        (wi, _, tl, tr) = er[i]
+        if comp[tl] != comp[tr]:
+            # case 1: er separates graph
+            # compare the matches that include er[i] versus
+            # those that exclude it
+            (a, b) = _PartitionComps(er, comp, i, comp[tl], comp[tr])
+            ax = _CopyExcluding(a, tl, tr)
+            bx = _CopyExcluding(b, tl, tr)
+            (axmatch, wax) = _DCMatch(ax)
+            (bxmatch, wbx) = _DCMatch(bx)
+            if len(ax) == len(a):
+                wa = wax
+                amatch = axmatch
+            else:
+                (amatch, wa) = _DCMatch(a)
+            if len(bx) == len(b):
+                wb = wbx
+                bmatch = bxmatch
+            else:
+                (bmatch, wb) = _DCMatch(b)
+            w = wa + wb
+            wx = wax + wbx + wi
+            if w > wx:
+                match = amatch + bmatch
+                matchw = w
+            else:
+                match = [er[i]] + axmatch + bxmatch
+                matchw = wx
+        else:
+            # case 2: er not needed to separate graph
+            (a, b) = _PartitionComps(er, comp, -1, 0, 0)
+            (amatch, wa) = _DCMatch(a)
+            (bmatch, wb) = _DCMatch(b)
+            match = amatch + bmatch
+            matchw = wa + wb
+        if match:
+            break
+    if not match:
+        return _EMatch(er)
+    return (match, matchw)
 
 
 def _EMatch(er):
-  """Exhaustive match helper for _MaxMatch.
-
-  This is the case when we were unable to find a single edge
-  separating the edge removal graph into two components.
-  So pick a single edge and try _DCMatch on the two cases of
-  including or excluding that edge.  We may be lucky in these
-  subcases (say, if the graph is currently a simple cycle, so
-  only needs one more edge after the one we pick here to separate
-  it into components).  Otherwise, we'll end up back in _EMatch
-  again, and the worse case will be exponential.
-
-  Pick a random edge rather than say, the first, to hopefully
-  avoid some pathological cases.
-
-  Args:
-    er: list of (weight, el, tl, tr) (see _ERGraph)
-  Returns:
-     (list of (weight, e, tl, tr), float) - the subset forming a maximum
-        matching, and the total weight of the match.
-  """
-
-  if not er:
-    return ([], 0.0)
-  if len(er) == 1:
-    return (er, er[1][1])
-  i = random.randint(0, len(er)-1)
-  eri = (wi, _, tl, tr) = er[i]
-  # case a: include eri.  exlude other edges that touch tl or tr
-  a = _CopyExcluding(er, tl, tr)
-  a.append(eri)
-  (amatch, wa) = _DCMatch(a)
-  wa += wi
-  if len(a) == len(er)-1:
-    # if a excludes only eri, then er didn't touch anything else
-    # in the graph, and the best match will always include er
-    # and we can skip the call for case b
-    wb = -1.0
-    bmatch = []
-  else:
-    b = er[:i] + er[i+1:]
-    (bmatch, wb) = _DCMatch(b)
-  if wa > wb:
-    match = amatch
-    match.append(eri)
-    matchw = wa
-  else:
-    match = bmatch
-    matchw = wb
-  return (match, matchw)
+    """Exhaustive match helper for _MaxMatch.
+
+    This is the case when we were unable to find a single edge
+    separating the edge removal graph into two components.
+    So pick a single edge and try _DCMatch on the two cases of
+    including or excluding that edge.  We may be lucky in these
+    subcases (say, if the graph is currently a simple cycle, so
+    only needs one more edge after the one we pick here to separate
+    it into components).  Otherwise, we'll end up back in _EMatch
+    again, and the worse case will be exponential.
+
+    Pick a random edge rather than say, the first, to hopefully
+    avoid some pathological cases.
+
+    Args:
+      er: list of (weight, el, tl, tr) (see _ERGraph)
+    Returns:
+       (list of (weight, e, tl, tr), float) - the subset forming a maximum
+          matching, and the total weight of the match.
+    """
+
+    if not er:
+        return ([], 0.0)
+    if len(er) == 1:
+        return (er, er[1][1])
+    i = random.randint(0, len(er) - 1)
+    eri = (wi, _, tl, tr) = er[i]
+    # case a: include eri.  exlude other edges that touch tl or tr
+    a = _CopyExcluding(er, tl, tr)
+    a.append(eri)
+    (amatch, wa) = _DCMatch(a)
+    wa += wi
+    if len(a) == len(er) - 1:
+        # if a excludes only eri, then er didn't touch anything else
+        # in the graph, and the best match will always include er
+        # and we can skip the call for case b
+        wb = -1.0
+        bmatch = []
+    else:
+        b = er[:i] + er[i + 1:]
+        (bmatch, wb) = _DCMatch(b)
+    if wa > wb:
+        match = amatch
+        match.append(eri)
+        matchw = wa
+    else:
+        match = bmatch
+        matchw = wb
+    return (match, matchw)
 
 
 def _FindComponents(er, excepti):
-  """Find connected components induced by edges, excluding one edge.
-
-  Args:
-    er: list of (weight, el, tl, tr) (see _ERGraph)
-    excepti: index in er of edge to be excluded
-  Returns:
-    (int, dict): int is number of connected components found,
-        dict maps triangle triple -> connected component index (starting at 1)
-   """
-
-  ncomps = 0
-  comp = dict()
-  for i in range(0, len(er)):
-    (_, _, tl, tr) = er[i]
-    for t in [tl,tr]:
-      if t not in comp:
-        ncomps += 1
-        _FCVisit(er, excepti, comp, t, ncomps)
-  return (ncomps, comp)
+    """Find connected components induced by edges, excluding one edge.
+
+    Args:
+      er: list of (weight, el, tl, tr) (see _ERGraph)
+      excepti: index in er of edge to be excluded
+    Returns:
+      (int, dict): int is number of connected components found,
+          dict maps triangle triple ->
+              connected component index (starting at 1)
+     """
+
+    ncomps = 0
+    comp = dict()
+    for i in range(0, len(er)):
+        (_, _, tl, tr) = er[i]
+        for t in [tl, tr]:
+            if t not in comp:
+                ncomps += 1
+                _FCVisit(er, excepti, comp, t, ncomps)
+    return (ncomps, comp)
 
 
 def _FCVisit(er, excepti, comp, t, compnum):
-  """Helper for _FindComponents depth-first-search."""
+    """Helper for _FindComponents depth-first-search."""
 
-  comp[t] = compnum
-  for i in range(0, len(er)):
-    if i == excepti:
-      continue
-    (_, _, tl, tr) = er[i]
-    if tl == t or tr == t:
-      s = tl
-      if s == t:
-        s = tr
-      if s not in comp:
-        _FCVisit(er, excepti, comp, s, compnum)
+    comp[t] = compnum
+    for i in range(0, len(er)):
+        if i == excepti:
+            continue
+        (_, _, tl, tr) = er[i]
+        if tl == t or tr == t:
+            s = tl
+            if s == t:
+                s = tr
+            if s not in comp:
+                _FCVisit(er, excepti, comp, s, compnum)
 
 
 def _PartitionComps(er, comp, excepti, compa, compb):
-  """Partition the edges of er by component number, into two lists.
-
-  Generally, put odd components into first list and even into second,
-  except that component compa goes in the first and compb goes in the second,
-  and we ignore edge er[excepti].
-
-  Args:
-    er: list of (weight, el, tl, tr) (see _ERGraph)
-    comp: dict - mapping triangle triple -> connected component index
-    excepti: int - index in er to ignore (unless excepti==-1)
-    compa: int - component to go in first list of answer (unless 0)
-    compb: int - component to go in second list of answer (unless 0)
-  Returns:
-    (list, list) - a partition of er according to above rules
-  """
-
-  parta = []
-  partb = []
-  for i in range(0, len(er)):
-
-    if i == excepti:
-      continue
-    tl = er[i][2]
-    c = comp[tl]
-    if c == compa or (c != compb and (c&1) == 1):
-      parta.append(er[i])
-    else:
-      partb.append(er[i])
-  return (parta, partb)
+    """Partition the edges of er by component number, into two lists.
+
+    Generally, put odd components into first list and even into second,
+    except that component compa goes in the first and compb goes in the second,
+    and we ignore edge er[excepti].
+
+    Args:
+      er: list of (weight, el, tl, tr) (see _ERGraph)
+      comp: dict - mapping triangle triple -> connected component index
+      excepti: int - index in er to ignore (unless excepti==-1)
+      compa: int - component to go in first list of answer (unless 0)
+      compb: int - component to go in second list of answer (unless 0)
+    Returns:
+      (list, list) - a partition of er according to above rules
+    """
+
+    parta = []
+    partb = []
+    for i in range(0, len(er)):
+
+        if i == excepti:
+            continue
+        tl = er[i][2]
+        c = comp[tl]
+        if c == compa or (c != compb and (c & 1) == 1):
+            parta.append(er[i])
+        else:
+            partb.append(er[i])
+    return (parta, partb)
 
 
 def _CopyExcluding(er, s, t):
-  """Return a copy of er, excluding all those involving triangles s and t.
+    """Return a copy of er, excluding all those involving triangles s and t.
 
-  Args:
-    er: list of (weight, e, tl, tr) - see _ERGraph
-    s: 3-tuple of int - a triangle
-    t: 3-tuple of int - a triangle
-  Returns:
-    Copy of er excluding those with tl or tr == s or t
-  """
+    Args:
+      er: list of (weight, e, tl, tr) - see _ERGraph
+      s: 3-tuple of int - a triangle
+      t: 3-tuple of int - a triangle
+    Returns:
+      Copy of er excluding those with tl or tr == s or t
+    """
 
-  ans = []
-  for e in er:
-    (_, _, tl, tr) = e
-    if tl == s or tr == s or tl == t or tr == t:
-      continue
-    ans.append(e)
-  return ans
+    ans = []
+    for e in er:
+        (_, _, tl, tr) = e
+        if tl == s or tr == s or tl == t or tr == t:
+            continue
+        ans.append(e)
+    return ans
 
 
 def _DegreeDict(tris):
-  """Return a dictionary mapping vertices in tris to the number of triangles
-  that they are touch."""
+    """Return a dictionary mapping vertices in tris to the number of triangles
+    that they are touch."""
 
-  ans = dict()
-  for t in tris:
-    for v in t:
-      if v in ans:
-        ans[v] = ans[v] + 1
-      else:
-        ans[v] = 1
-  return ans
+    ans = dict()
+    for t in tris:
+        for v in t:
+            if v in ans:
+                ans[v] = ans[v] + 1
+            else:
+                ans[v] = 1
+    return ans
 
 
 def PolygonPlane(face, points):
-  """Return a Normal vector for the face with 3d coords given by indexing into points."""
+    """Return a Normal vector for the face with 3d coords given by indexing
+    into points."""
 
-  if len(face) < 3:
-    return (0.0, 0.0, 1.0)    # arbitrary, we really have no idea
-  else:
-    coords = [ points.pos[i] for i in face ]
-    return Normal(coords)
+    if len(face) < 3:
+        return (0.0, 0.0, 1.0)    # arbitrary, we really have no idea
+    else:
+        coords = [points.pos[i] for i in face]
+        return Normal(coords)
 
 
 # This Normal appears to be on the CCW-traversing side of a polygon
 def Normal(coords):
-  """Return an average Normal vector for the point list, 3d coords."""
-
-  if len(coords) < 3:
-    return (0.0, 0.0, 1.0)    # arbitrary
-
-  (ax, ay, az) = coords[0]
-  (bx, by, bz) = coords[1]
-  (cx, cy, cz) = coords[2]
-
-  if len(coords) == 3:
-    sx = (ay - by) * (az + bz) + (by - cy) * (bz + cz) + (cy - ay) * (cz + az)
-    sy = (az - bz) * (ax + bx) + (bz - cz) * (bx + cx) + (cz - az) * (cx + ax)
-    sz = (ax - bx) * (by + by) + (bx - cx) * (by + cy) + (cx - ax) * (cy + ay)
-    return Norm3(sx, sy, sz)
-  else:
-    sx = (ay - by) * (az + bz) + (by - cy) * (bz + cz)
-    sy = (az - bz) * (ax + bx) + (bz - cz) * (bx + cx)
-    sz = (ax - bx) * (ay + by) + (bx - cx) * (by + cy)
-    return _NormalAux(coords[3:], coords[0], sx, sy, sz)
+    """Return an average Normal vector for the point list, 3d coords."""
+
+    if len(coords) < 3:
+        return (0.0, 0.0, 1.0)    # arbitrary
+
+    (ax, ay, az) = coords[0]
+    (bx, by, bz) = coords[1]
+    (cx, cy, cz) = coords[2]
+
+    if len(coords) == 3:
+        sx = (ay - by) * (az + bz) + \
+            (by - cy) * (bz + cz) + \
+            (cy - ay) * (cz + az)
+        sy = (az - bz) * (ax + bx) + \
+            (bz - cz) * (bx + cx) + \
+            (cz - az) * (cx + ax)
+        sz = (ax - bx) * (by + by) + \
+            (bx - cx) * (by + cy) + \
+            (cx - ax) * (cy + ay)
+        return Norm3(sx, sy, sz)
+    else:
+        sx = (ay - by) * (az + bz) + (by - cy) * (bz + cz)
+        sy = (az - bz) * (ax + bx) + (bz - cz) * (bx + cx)
+        sz = (ax - bx) * (ay + by) + (bx - cx) * (by + cy)
+        return _NormalAux(coords[3:], coords[0], sx, sy, sz)
 
 
 def _NormalAux(rest, first, sx, sy, sz):
-  (ax, ay, az) = rest[0]
-  if len(rest) == 1:
-    (bx, by, bz) = first
-  else:
-    (bx, by, bz) = rest[1]
-  nx = sx + (ay - by) * (az + bz)
-  ny = sy + (az - bz) * (ax + bx)
-  nz = sz + (ax - bx) * (ay + by)
-  if len(rest) == 1:
-    return Norm3(nx, ny, nz)
-  else:
-    return _NormalAux(rest[1:], first, nx, ny, nz)
+    (ax, ay, az) = rest[0]
+    if len(rest) == 1:
+        (bx, by, bz) = first
+    else:
+        (bx, by, bz) = rest[1]
+    nx = sx + (ay - by) * (az + bz)
+    ny = sy + (az - bz) * (ax + bx)
+    nz = sz + (ax - bx) * (ay + by)
+    if len(rest) == 1:
+        return Norm3(nx, ny, nz)
+    else:
+        return _NormalAux(rest[1:], first, nx, ny, nz)
 
 
 def Norm3(x, y, z):
-  """Return vector (x,y,z) normalized by dividing by squared length.
-  Return (0.0, 0.0, 1.0) if the result is undefined."""
-  sqrlen = x * x + y * y + z * z
-  if sqrlen < 1e-100:
-    return (0.0, 0.0, 1.0)
-  else:
-    try:
-      d = sqrt(sqrlen)
-      return (x / d, y / d, z / d)
-    except:
-      return (0.0, 0.0, 1.0)
+    """Return vector (x,y,z) normalized by dividing by squared length.
+    Return (0.0, 0.0, 1.0) if the result is undefined."""
+    sqrlen = x * x + y * y + z * z
+    if sqrlen < 1e-100:
+        return (0.0, 0.0, 1.0)
+    else:
+        try:
+            d = sqrt(sqrlen)
+            return (x / d, y / d, z / d)
+        except:
+            return (0.0, 0.0, 1.0)
 
 
 # We're using right-hand coord system, where
 # forefinger=x, middle=y, thumb=z on right hand.
 # Then, e.g., (1,0,0) x (0,1,0) = (0,0,1)
 def Cross3(a, b):
-  """Return the cross product of two vectors, a x b."""
+    """Return the cross product of two vectors, a x b."""
 
-  (ax, ay, az) = a
-  (bx, by, bz) = b
-  return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
+    (ax, ay, az) = a
+    (bx, by, bz) = b
+    return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
 
 
 def Dot2(a, b):
-  """Return the dot product of two 2d vectors, a . b."""
+    """Return the dot product of two 2d vectors, a . b."""
 
-  return a[0] * b[0] + a[1] * b[1]
+    return a[0] * b[0] + a[1] * b[1]
 
 
 def Perp2(a, b):
-  """Return a sort of 2d cross product."""
+    """Return a sort of 2d cross product."""
 
-  return a[0] * b[1] - a[1] * b[0]
+    return a[0] * b[1] - a[1] * b[0]
 
 
 def Sub2(a, b):
-  """Return difference of 2d vectors, a-b."""
+    """Return difference of 2d vectors, a-b."""
 
-  return (a[0] - b[0], a[1] - b[1])
+    return (a[0] - b[0], a[1] - b[1])
 
 
 def Add2(a, b):
-  """Return the sum of 2d vectors, a+b."""
+    """Return the sum of 2d vectors, a+b."""
 
-  return (a[0] + b[0], a[1] + b[1])
+    return (a[0] + b[0], a[1] + b[1])
 
 
 def Length2(v):
-  """Return length of vector v=(x,y)."""
+    """Return length of vector v=(x,y)."""
 
-  return sqrt(v[0]*v[0] + v[1]*v[1])
+    return sqrt(v[0] * v[0] + v[1] * v[1])
 
 
 def LinInterp2(a, b, alpha):
-  """Return the point alpha of the way from a to b."""
+    """Return the point alpha of the way from a to b."""
 
-  beta = 1-alpha
-  return (beta*a[0]+alpha*b[0], beta*a[1]+alpha*b[1])
+    beta = 1 - alpha
+    return (beta * a[0] + alpha * b[0], beta * a[1] + alpha * b[1])
 
 
 def Normalized2(p):
-  """Return vector p normlized by dividing by its squared length.
-  Return (0.0, 1.0) if the result is undefined."""
+    """Return vector p normlized by dividing by its squared length.
+    Return (0.0, 1.0) if the result is undefined."""
 
-  (x, y) = p
-  sqrlen = x * x + y * y
-  if sqrlen < 1e-100:
-    return (0.0, 1.0)
-  else:
-    try:
-      d = sqrt(sqrlen)
-      return (x / d, y / d)
-    except:
-      return (0.0, 1.0)
+    (x, y) = p
+    sqrlen = x * x + y * y
+    if sqrlen < 1e-100:
+        return (0.0, 1.0)
+    else:
+        try:
+            d = sqrt(sqrlen)
+            return (x / d, y / d)
+        except:
+            return (0.0, 1.0)
 
 
 def Angle(a, b, c, points):
-  """Return Angle abc in degrees, in range [0,180),
-  where a,b,c are indices into points."""
-
-  u = Sub2(points.pos[c], points.pos[b])
-  v = Sub2(points.pos[a], points.pos[b])
-  n1 = Length2(u)
-  n2 = Length2(v)
-  if n1 == 0.0 or n2 == 0.0:
-    return 0.0
-  else:
-    costheta = Dot2(u, v) / (n1 * n2)
-    if costheta > 1.0:
-      costheta = 1.0
-    if costheta < - 1.0:
-      costheta = - 1.0
-    return math.acos(costheta) * 180.0 / math.pi
+    """Return Angle abc in degrees, in range [0,180),
+    where a,b,c are indices into points."""
+
+    u = Sub2(points.pos[c], points.pos[b])
+    v = Sub2(points.pos[a], points.pos[b])
+    n1 = Length2(u)
+    n2 = Length2(v)
+    if n1 == 0.0 or n2 == 0.0:
+        return 0.0
+    else:
+        costheta = Dot2(u, v) / (n1 * n2)
+        if costheta > 1.0:
+            costheta = 1.0
+        if costheta < - 1.0:
+            costheta = - 1.0
+        return math.acos(costheta) * 180.0 / math.pi
 
 
 def SegsIntersect(ixa, ixb, ixc, ixd, points):
-  """Return true if segment AB intersects CD,
-  false if they just touch.  ixa, ixb, ixc, ixd are indices
-  into points."""
-
-  a = points.pos[ixa]
-  b = points.pos[ixb]
-  c = points.pos[ixc]
-  d = points.pos[ixd]
-  u = Sub2(b, a)
-  v = Sub2(d, c)
-  w = Sub2(a, c)
-  pp = Perp2(u, v)
-  if abs(pp) > TOL:
-    si = Perp2(v, w) / pp
-    ti = Perp2(u, w) / pp
-    return 0.0 < si < 1.0 and 0.0 < ti < 1.0
-  else:
-    # parallel or overlapping
-    if Dot2(u, u) == 0.0 or Dot2(v, v) == 0.0:
-      return False
+    """Return true if segment AB intersects CD,
+    false if they just touch.  ixa, ixb, ixc, ixd are indices
+    into points."""
+
+    a = points.pos[ixa]
+    b = points.pos[ixb]
+    c = points.pos[ixc]
+    d = points.pos[ixd]
+    u = Sub2(b, a)
+    v = Sub2(d, c)
+    w = Sub2(a, c)
+    pp = Perp2(u, v)
+    if abs(pp) > TOL:
+        si = Perp2(v, w) / pp
+        ti = Perp2(u, w) / pp
+        return 0.0 < si < 1.0 and 0.0 < ti < 1.0
     else:
-      pp2 = Perp2(w, v)
-      if abs(pp2) > TOL:
-        return False  # parallel, not collinear
-      z = Sub2(b, c)
-      (vx, vy) = v
-      (wx, wy) = w
-      (zx, zy) = z
-      if vx == 0.0:
-        (t0, t1) = (wy / vy, zy / vy)
-      else:
-        (t0, t1) = (wx / vx, zx / vx)
-      return 0.0 < t0 < 1.0 and 0.0 < t1 < 1.0
+        # parallel or overlapping
+        if Dot2(u, u) == 0.0 or Dot2(v, v) == 0.0:
+            return False
+        else:
+            pp2 = Perp2(w, v)
+            if abs(pp2) > TOL:
+                return False  # parallel, not collinear
+            z = Sub2(b, c)
+            (vx, vy) = v
+            (wx, wy) = w
+            (zx, zy) = z
+            if vx == 0.0:
+                (t0, t1) = (wy / vy, zy / vy)
+            else:
+                (t0, t1) = (wx / vx, zx / vx)
+            return 0.0 < t0 < 1.0 and 0.0 < t1 < 1.0
 
 
 def Ccw(a, b, c, points):
-  """Return true if ABC is a counterclockwise-oriented triangle,
-  where a, b, and c are indices into points.
-  Returns false if not, or if colinear within TOL."""
+    """Return true if ABC is a counterclockwise-oriented triangle,
+    where a, b, and c are indices into points.
+    Returns false if not, or if colinear within TOL."""
 
-  (ax, ay) = (points.pos[a][0], points.pos[a][1])
-  (bx, by) = (points.pos[b][0], points.pos[b][1])
-  (cx, cy) = (points.pos[c][0], points.pos[c][1])
-  d = ax * by - bx * ay - ax * cy + cx * ay + bx * cy - cx * by
-  return d > TOL
+    (ax, ay) = (points.pos[a][0], points.pos[a][1])
+    (bx, by) = (points.pos[b][0], points.pos[b][1])
+    (cx, cy) = (points.pos[c][0], points.pos[c][1])
+    d = ax * by - bx * ay - ax * cy + cx * ay + bx * cy - cx * by
+    return d > TOL
 
 
 def InCircle(a, b, c, d, points):
-  """Return true if circle through points with indices a, b, c
-  contains point with index d (indices into points).
-  Except: if ABC forms a counterclockwise oriented triangle
-  then the test is reversed: return true if d is outside the circle.
-  Will get false, no matter what orientation, if d is cocircular, with TOL^2.
-    | xa ya xa^2+ya^2 1 |
-    | xb yb xb^2+yb^2 1 | > 0
-    | xc yc xc^2+yc^2 1 |
-    | xd yd xd^2+yd^2 1 |
-  """
-
-  (xa, ya, za) = _Icc(points.pos[a])
-  (xb, yb, zb) = _Icc(points.pos[b])
-  (xc, yc, zc) = _Icc(points.pos[c])
-  (xd, yd, zd) = _Icc(points.pos[d])
-  det = xa * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc) \
-	- xb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc) \
-	+ xc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb) \
-	- xd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)
-  return det > TOL * TOL
+    """Return true if circle through points with indices a, b, c
+    contains point with index d (indices into points).
+    Except: if ABC forms a counterclockwise oriented triangle
+    then the test is reversed: return true if d is outside the circle.
+    Will get false, no matter what orientation, if d is cocircular, with TOL^2.
+      | xa ya xa^2+ya^2 1 |
+      | xb yb xb^2+yb^2 1 | > 0
+      | xc yc xc^2+yc^2 1 |
+      | xd yd xd^2+yd^2 1 |
+    """
+
+    (xa, ya, za) = _Icc(points.pos[a])
+    (xb, yb, zb) = _Icc(points.pos[b])
+    (xc, yc, zc) = _Icc(points.pos[c])
+    (xd, yd, zd) = _Icc(points.pos[d])
+    det = xa * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc) \
+          - xb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc) \
+          + xc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb) \
+          - xd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)
+    return det > TOL * TOL
 
 
 def _Icc(p):
-  (x, y) = (p[0], p[1])
-  return (x, y, x * x + y * y)
+    (x, y) = (p[0], p[1])
+    return (x, y, x * x + y * y)