'''
Copyright (C) 2014 CG Cookie
http://cgcookie.com
hello@cgcookie.com

Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
'''

bl_info = {
    "name":        "Polystrips Retopology Tool",
    "description": "A tool to retopologize complex forms with drawn strips of polygons.",
    "author":      "Jonathan Denning, Jonathan Williamson, Patrick Moore",
    "version":     (1, 0, 2),
    "blender":     (2, 7, 1),
    "location":    "View 3D > Tool Shelf",
    "warning":     "",  # used for warning icon and text in addons panel
    "wiki_url":    "http://cgcookiemarkets.com/blender/all-products/polystrips-retopology-tool/?view=docs",
    "tracker_url": "https://github.com/CGCookie/retopology-polystrips/issues",
    "category":    "3D View"
    }

# Add the current __file__ path to the search path
import sys
import os
sys.path.append(os.path.dirname(__file__))

import math
import copy
import random
import time
from math import sqrt

import bpy
import bmesh
import blf
import bgl
from bpy.props import EnumProperty, StringProperty, BoolProperty, IntProperty, FloatVectorProperty, FloatProperty
from bpy.types import Operator, AddonPreferences
from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d, region_2d_to_location_3d
from mathutils import Vector, Matrix, Quaternion
from mathutils.geometry import intersect_line_plane, intersect_point_line

from lib import common_utilities
from lib.common_utilities import get_object_length_scale, dprint, profiler, frange
from lib.common_classes import SketchBrush

from lib import common_drawing

from polystrips import *
import polystrips_utilities
from polystrips_draw import *


# Used to store keymaps for addon
polystrips_keymaps = []

# Used to store undo snapshots
polystrips_undo_cache = []


class PolystripsToolsAddonPreferences(AddonPreferences):
    bl_idname = __name__

    def update_theme(self, context):
        print('theme updated to ' + str(theme))

    debug = IntProperty(
        name="Debug Level",
        default=1,
        min=0,
        max=4,
        )

    theme = EnumProperty(
        items=[
            ('blue', 'Blue', 'Blue color scheme'),
            ('green', 'Green', 'Green color scheme'),
            ('orange', 'Orange', 'Orange color scheme'),
            ],
        name='theme',
        default='blue'
        )

    def rgba_to_float(r, g, b, a):
        return (r/255.0, g/255.0, b/255.0, a/255.0)

    theme_colors_selection = {
        'blue': rgba_to_float(105, 246, 113, 255),
        'green': rgba_to_float(102, 165, 240, 255),
        'orange': rgba_to_float(102, 165, 240, 255)
    }
    theme_colors_mesh = {
        'blue': rgba_to_float(102, 165, 240, 255),
        'green': rgba_to_float(105, 246, 113, 255),
        'orange': rgba_to_float(254, 145, 0, 255)
    }

    show_segment_count = BoolProperty(
        name='Show Selected Segment Count',
        description='Show segment count on selection',
        default=True
        )

    quad_prev_radius = IntProperty(
        name="Pixel Brush Radius",
        description="Pixel brush size",
        default=15,
        )

    undo_depth = IntProperty(
        name="Undo Depth",
        description="Max number of undo steps",
        default=15,
        )

    def draw(self, context):
        layout = self.layout

        row = layout.row(align=True)
        row.prop(self, "theme", "Theme")

        row = layout.row(align=True)
        row.prop(self, "show_segment_count")

        row = layout.row(align=True)
        row.prop(self, "debug")


class CGCOOKIE_OT_retopo_polystrips_panel(bpy.types.Panel):
    '''Retopologize Forms with polygon strips'''
    bl_category = "Retopology"
    bl_label = "Polystrips"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'

    @classmethod
    def poll(cls, context):
        mode = bpy.context.mode
        obj = context.active_object
        return (obj and obj.type == 'MESH' and mode in ('OBJECT', 'EDIT_MESH'))

    def draw(self, context):
        layout = self.layout

        col = layout.column(align=True)

        if 'EDIT' in context.mode and len(context.selected_objects) != 2:
            col.label(text='No 2nd Object!')
        col.operator("cgcookie.polystrips", icon="IPO_BEZIER")


class CGCOOKIE_OT_polystrips(bpy.types.Operator):
    bl_idname = "cgcookie.polystrips"
    bl_label = "Polystrips"

    @classmethod
    def poll(cls, context):
        if context.mode not in {'EDIT_MESH', 'OBJECT'}:
            return False

        if context.active_object:
            if context.mode == 'EDIT_MESH':
                if len(context.selected_objects) > 1:
                    return True
                else:
                    return False
            else:
                return context.object.type == 'MESH'
        else:
            return False

    def draw_callback(self, context):
        return self.ui.draw_callback(context)

    def modal(self, context, event):
        ret = self.ui.modal(context, event)
        if 'FINISHED' in ret or 'CANCELLED' in ret:
            self.ui.cleanup(context)
            common_utilities.callback_cleanup(self, context)
        return ret

    def invoke(self, context, event):
        self.ui = PolystripsUI(context, event)

        # Switch to modal
        self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, (context, ), 'WINDOW', 'POST_PIXEL')
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


def register():
    bpy.utils.register_class(CGCOOKIE_OT_polystrips)
    bpy.utils.register_class(CGCOOKIE_OT_retopo_polystrips_panel)
    bpy.utils.register_class(PolystripsToolsAddonPreferences)


def unregister():
    bpy.utils.unregister_class(PolystripsToolsAddonPreferences)
    bpy.utils.unregister_class(CGCOOKIE_OT_retopo_polystrips_panel)
    bpy.utils.unregister_class(CGCOOKIE_OT_polystrips)


class PolystripsUI:
    def __init__(self, context, event):
        settings = common_utilities.get_settings()

        self.mode = 'main'

        self.mode_pos = (0, 0)
        self.cur_pos = (0, 0)
        self.mode_radius = 0
        self.action_center = (0, 0)
        self.action_radius = 0
        self.is_navigating = False
        self.sketch_curpos = (0, 0)
        self.sketch_pressure = 1
        self.sketch = []

        self.post_update = True

        self.footer = ''
        self.footer_last = ''

        self.last_matrix = None

        self._timer = context.window_manager.event_timer_add(0.1, context.window)

        self.stroke_smoothing = 0.75          # 0: no smoothing. 1: no change

        if context.mode == 'OBJECT':

            self.obj_orig = context.object
            # duplicate selected objected to temporary object but with modifiers applied
            self.me = self.obj_orig.to_mesh(scene=context.scene, apply_modifiers=True, settings='PREVIEW')
            self.me.update()
            self.obj = bpy.data.objects.new('PolystripsTmp', self.me)
            bpy.context.scene.objects.link(self.obj)
            self.obj.hide = True
            self.obj.matrix_world = self.obj_orig.matrix_world
            self.me.update()

            # HACK
            bpy.ops.object.mode_set(mode='EDIT')
            bpy.ops.object.mode_set(mode='OBJECT')

            self.bme = bmesh.new()
            self.bme.from_mesh(self.me)

            self.to_obj = None
            self.to_bme = None
            self.snap_eds = []
            self.snap_eds_vis = []
            self.hover_ed = None

        if context.mode == 'EDIT_MESH':
            self.obj_orig = [ob for ob in context.selected_objects if ob != context.object][0]
            self.me = self.obj_orig.to_mesh(scene=context.scene, apply_modifiers=True, settings='PREVIEW')
            self.me.update()
            self.bme = bmesh.new()
            self.bme.from_mesh(self.me)

            self.obj = bpy.data.objects.new('PolystripsTmp', self.me)
            bpy.context.scene.objects.link(self.obj)
            self.obj.hide = True
            self.obj.matrix_world = self.obj_orig.matrix_world
            self.me.update()

            bpy.ops.object.mode_set(mode='OBJECT')
            bpy.ops.object.mode_set(mode='EDIT')

            self.to_obj = context.object
            self.to_bme = bmesh.from_edit_mesh(context.object.data)
            self.snap_eds = [ed for ed in self.to_bme.edges if not ed.is_manifold]
            region, r3d = context.region, context.space_data.region_3d
            mx = self.to_obj.matrix_world
            rv3d = context.space_data.region_3d
            self.snap_eds_vis = [False not in common_utilities.ray_cast_visible([mx * ed.verts[0].co, mx * ed.verts[1].co], self.obj, rv3d) for ed in self.snap_eds]
            self.hover_ed = None

        self.scale = self.obj.scale[0]
        self.length_scale = get_object_length_scale(self.obj)
        # World stroke radius
        self.stroke_radius = 0.01 * self.length_scale
        self.stroke_radius_pressure = 0.01 * self.length_scale
        # Screen_stroke_radius
        self.screen_stroke_radius = 20  # TODO, hood to settings

        self.sketch_brush = SketchBrush(context,
                                        settings,
                                        event.mouse_region_x, event.mouse_region_y,
                                        15,  # settings.quad_prev_radius,
                                        self.obj)

        self.sel_gedge = None                           # selected gedge
        self.sel_gvert = None                           # selected gvert
        self.act_gvert = None                           # active gvert (operated upon)

        self.polystrips = PolyStrips(context, self.obj)

        polystrips_undo_cache = []  # Clear the cache in case any is left over
        if self.obj.grease_pencil:
            self.create_polystrips_from_greasepencil()
        elif 'BezierCurve' in bpy.data.objects:
            self.create_polystrips_from_bezier(bpy.data.objects['BezierCurve'])

        context.area.header_text_set('PolyStrips')

    ###############################
    def create_undo_snapshot(self, action):
        '''
        unsure about all the _timers get deep copied
        and if sel_gedges and verts get copied as references
        or also duplicated, making them no longer valid.
        '''

        settings = common_utilities.get_settings()
        repeated_actions = {'count', 'zip count'}

        if action in repeated_actions and len(polystrips_undo_cache):
            if action == polystrips_undo_cache[-1][1]:
                print('repeatable...dont take snapshot')
                return

        p_data = copy.deepcopy(self.polystrips)

        if self.sel_gedge:
            sel_gedge = self.polystrips.gedges.index(self.sel_gedge)
        else:
            sel_gedge = None

        if self.sel_gvert:
            sel_gvert = self.polystrips.gverts.index(self.sel_gvert)
        else:
            sel_gvert = None

        if self.act_gvert:
            act_gvert = self.polystrips.gverts.index(self.sel_gvert)
        else:
            act_gvert = None

        polystrips_undo_cache.append(([p_data, sel_gvert, sel_gedge, act_gvert], action))

        if len(polystrips_undo_cache) > settings.undo_depth:
            polystrips_undo_cache.pop(0)

    def undo_action(self):
        '''
        '''
        if len(polystrips_undo_cache) > 0:
            data, action = polystrips_undo_cache.pop()

            self.polystrips = data[0]

            if data[1]:
                self.sel_gvert = self.polystrips.gverts[data[1]]
            else:
                self.sel_gvert = None

            if data[2]:
                self.sel_gedge = self.polystrips.gedges[data[2]]
            else:
                self.sel_gedge = None

            if data[3]:
                self.act_gvert = self.polystrips.gverts[data[3]]
            else:
                self.act_gvert = None

    def cleanup(self, context):
        '''
        remove temporary object
        '''
        dprint('cleaning up!')

        tmpobj = self.obj  # Not always, sometimes if duplicate remains...will be .001
        meobj  = tmpobj.data

        # Delete object
        context.scene.objects.unlink(tmpobj)
        tmpobj.user_clear()
        if tmpobj.name in bpy.data.objects:
            bpy.data.objects.remove(tmpobj)

        bpy.context.scene.update()
        bpy.data.meshes.remove(meobj)

    ################################
    # Draw functions

    def draw_callback(self, context):
        settings = common_utilities.get_settings()
        region,r3d = context.region,context.space_data.region_3d

        new_matrix = [v for l in r3d.view_matrix for v in l]
        if self.post_update or self.last_matrix != new_matrix:
            for gv in self.polystrips.gverts:
                gv.update_visibility(r3d)
            for ge in self.polystrips.gedges:
                ge.update_visibility(r3d)
            if self.sel_gedge:
                for gv in [self.sel_gedge.gvert1, self.sel_gedge.gvert2]:
                    gv.update_visibility(r3d)
            if self.sel_gvert:
                for gv in self.sel_gvert.get_inner_gverts():
                    gv.update_visibility(r3d)

            if len(self.snap_eds):
                mx = self.obj.matrix_world
                self.snap_eds_vis = [False not in common_utilities.ray_cast_visible([mx * ed.verts[0].co, mx * ed.verts[1].co], self.obj, r3d) for ed in self.snap_eds]

            self.post_update = False
            self.last_matrix = new_matrix


        if settings.debug < 3:
            self.draw_callback_themed(context)
        else:
            self.draw_callback_debug(context)

    def draw_callback_themed(self, context):
        settings = common_utilities.get_settings()
        region,r3d = context.region,context.space_data.region_3d

        # theme_number = int(settings.theme)


        color_inactive = PolystripsToolsAddonPreferences.theme_colors_mesh[settings.theme]
        color_selection = PolystripsToolsAddonPreferences.theme_colors_selection[settings.theme]
        color_active = PolystripsToolsAddonPreferences.theme_colors_selection[settings.theme] # Not used at the moment

        bgl.glEnable(bgl.GL_POINT_SMOOTH)

        for i_ge,gedge in enumerate(self.polystrips.gedges):
            if gedge == self.sel_gedge:
                color_border = (color_selection[0], color_selection[1], color_selection[2], 1.00)
                color_fill = (color_selection[0], color_selection[1], color_selection[2], 0.20)
            else:
                color_border = (color_inactive[0], color_inactive[1], color_inactive[2], 1.00)
                color_fill = (color_inactive[0], color_inactive[1], color_inactive[2], 0.20)

            for c0,c1,c2,c3 in gedge.iter_segments(only_visible=True):
                common_drawing.draw_quads_from_3dpoints(context, [c0,c1,c2,c3], color_fill)
                common_drawing.draw_polyline_from_3dpoints(context, [c0,c1,c2,c3,c0], color_border, 1, "GL_LINE_STIPPLE")

            if settings.debug >= 2:
                # draw bezier
                p0,p1,p2,p3 = gedge.gvert0.snap_pos, gedge.gvert1.snap_pos, gedge.gvert2.snap_pos, gedge.gvert3.snap_pos
                p3d = [cubic_bezier_blend_t(p0,p1,p2,p3,t/16.0) for t in range(17)]
                common_drawing.draw_polyline_from_3dpoints(context, p3d, (1,1,1,0.5),1, "GL_LINE_STIPPLE")

        for i_gv,gv in enumerate(self.polystrips.gverts):
            if not gv.is_visible(): continue
            p0,p1,p2,p3 = gv.get_corners()

            if gv.is_unconnected(): continue

            is_selected = False
            is_selected |= gv == self.sel_gvert
            is_selected |= self.sel_gedge!=None and (self.sel_gedge.gvert0 == gv or self.sel_gedge.gvert1 == gv)
            is_selected |= self.sel_gedge!=None and (self.sel_gedge.gvert2 == gv or self.sel_gedge.gvert3 == gv)
            if is_selected:
                color_border = (color_selection[0], color_selection[1], color_selection[2], 1.00)
                color_fill   = (color_selection[0], color_selection[1], color_selection[2], 0.20)
            else:
                color_border = (color_inactive[0], color_inactive[1], color_inactive[2], 1.00)
                color_fill   = (color_inactive[0], color_inactive[1], color_inactive[2], 0.20)

            p3d = [p0,p1,p2,p3,p0]
            common_drawing.draw_quads_from_3dpoints(context, [p0,p1,p2,p3], color_fill)
            common_drawing.draw_polyline_from_3dpoints(context, p3d, color_border, 1, "GL_LINE_STIPPLE")

        p3d = [gvert.position for gvert in self.polystrips.gverts if not gvert.is_unconnected() and gvert.is_visible()]
        color = (color_inactive[0], color_inactive[1], color_inactive[2], 1.00)
        common_drawing.draw_3d_points(context, p3d, color, 4)

        if self.sel_gvert:
            color = (color_selection[0], color_selection[1], color_selection[2], 1.00)
            gv = self.sel_gvert
            p0 = gv.position
            if gv.is_inner():
                p1 = gv.gedge_inner.get_outer_gvert_at(gv).position
                common_drawing.draw_3d_points(context, [p0], color, 8)
                common_drawing.draw_polyline_from_3dpoints(context, [p0,p1], color, 2, "GL_LINE_SMOOTH")
            else:
                p3d = [ge.get_inner_gvert_at(gv).position for ge in gv.get_gedges_notnone() if not ge.is_zippered()]
                common_drawing.draw_3d_points(context, [p0] + p3d, color, 8)
                for p1 in p3d:
                    common_drawing.draw_polyline_from_3dpoints(context, [p0,p1], color, 2, "GL_LINE_SMOOTH")

        if self.sel_gedge:
            color = (color_selection[0], color_selection[1], color_selection[2], 1.00)
            ge = self.sel_gedge
            if self.sel_gedge.is_zippered():
                p3d = [ge.gvert0.position, ge.gvert3.position]
                common_drawing.draw_3d_points(context, p3d, color, 8)
            else:
                p3d = [gv.position for gv in ge.gverts()]
                common_drawing.draw_3d_points(context, p3d, color, 8)
                common_drawing.draw_polyline_from_3dpoints(context, [p3d[0], p3d[1]], color, 2, "GL_LINE_SMOOTH")
                common_drawing.draw_polyline_from_3dpoints(context, [p3d[2], p3d[3]], color, 2, "GL_LINE_SMOOTH")

            if settings.show_segment_count:
                draw_gedge_info(self.sel_gedge, context)

        if self.act_gvert:
            color = (color_active[0], color_active[1], color_active[2], 1.00)
            gv = self.act_gvert
            p0 = gv.position
            common_drawing.draw_3d_points(context, [p0], color, 8)

        if self.mode == 'sketch':
            # Draw smoothing line (end of sketch to current mouse position)
            common_drawing.draw_polyline_from_points(context, [self.sketch_curpos, self.sketch[-1][0]], color_active, 1, "GL_LINE_SMOOTH")

            # Draw sketching stroke
            common_drawing.draw_polyline_from_points(context, [co[0] for co in self.sketch], color_selection, 2, "GL_LINE_STIPPLE")

            # Report pressure reading
            info = str(round(self.sketch_pressure,3))
            txt_width, txt_height = blf.dimensions(0, info)
            d = self.sketch_brush.pxl_rad
            blf.position(0, self.sketch_curpos[0] - txt_width/2, self.sketch_curpos[1] + d + txt_height, 0)
            blf.draw(0, info)

        if self.mode in {'scale tool','rotate tool'}:
            # Draw a scale/rotate line from tool origin to current mouse position
            common_drawing.draw_polyline_from_points(context, [self.action_center, self.mode_pos], (0, 0, 0, 0.5), 1, "GL_LINE_STIPPLE")

        bgl.glLineWidth(1)

        if self.mode == 'brush scale tool':
            # scaling brush size
            self.sketch_brush.draw(context, color=(1, 1, 1, .5), linewidth=1, color_size=(1, 1, 1, 1))
        elif self.mode not in {'grab tool','scale tool','rotate tool'} and not self.is_navigating:
            # draw the brush oriented to surface
            ray,hit = common_utilities.ray_cast_region2d(region, r3d, self.cur_pos, self.obj, settings)
            hit_p3d,hit_norm,hit_idx = hit
            if hit_idx != -1: # and not self.hover_ed:
                mx = self.obj.matrix_world
                mxnorm = mx.transposed().inverted().to_3x3()
                hit_p3d = mx * hit_p3d
                hit_norm = mxnorm * hit_norm
                common_drawing.draw_circle(context, hit_p3d, hit_norm.normalized(), self.stroke_radius_pressure, (1,1,1,.5))
            if self.mode == 'sketch':
                ray,hit = common_utilities.ray_cast_region2d(region, r3d, self.sketch[0][0], self.obj, settings)
                hit_p3d,hit_norm,hit_idx = hit
                if hit_idx != -1:
                    mx = self.obj.matrix_world
                    mxnorm = mx.transposed().inverted().to_3x3()
                    hit_p3d = mx * hit_p3d
                    hit_norm = mxnorm * hit_norm
                    common_drawing.draw_circle(context, hit_p3d, hit_norm.normalized(), self.stroke_radius_pressure, (1,1,1,.5))

        if self.hover_ed and False:
            color = (color_selection[0], color_selection[1], color_selection[2], 1.00)
            common_drawing.draw_bmedge(context, self.hover_ed, self.to_obj.matrix_world, 2, color)


    def draw_callback_debug(self, context):
        settings = common_utilities.get_settings()
        region = context.region
        r3d = context.space_data.region_3d

        draw_original_strokes   = False
        draw_gedge_directions   = True
        draw_gvert_orientations = False
        draw_unconnected_gverts = False
        draw_gvert_unsnapped    = False
        draw_gedge_bezier       = False
        draw_gedge_index        = False
        draw_gedge_igverts      = False

        cols = [(1,.5,.5,.8),(.5,1,.5,.8),(.5,.5,1,.8),(1,1,.5,.8)]

        color_selected          = (.5,1,.5,.8)

        color_gedge             = (1,.5,.5,.8)
        color_gedge_nocuts      = (.5,.2,.2,.8)
        color_gedge_zipped      = (.5,.7,.7,.8)

        color_gvert_unconnected = (.2,.2,.2,.8)
        color_gvert_endpoint    = (.2,.2,.5,.8)
        color_gvert_endtoend    = (.5,.5,1,.8)
        color_gvert_ljunction   = (1,.5,1,.8)
        color_gvert_tjunction   = (1,1,.5,.8)
        color_gvert_cross       = (1,1,1,.8)
        color_gvert_midpoints   = (.7,1,.7,.8)

        t = time.time()
        tf = t - int(t)
        tb = tf*2 if tf < 0.5 else 2-(tf*2)
        tb1 = 1-tb
        sel_fn = lambda c: tuple(cv*tb+cs*tb1 for cv,cs in zip(c,color_selected))

        if draw_original_strokes:
            for stroke in self.strokes_original:
                #p3d = [pt for pt,pr in stroke]
                #common_drawing.draw_polyline_from_3dpoints(context, p3d, (.7,.7,.7,.8), 3, "GL_LINE_SMOOTH")
                common_drawing.draw_circle(context, stroke[0][0], Vector((0,0,1)),0.003,(.2,.2,.2,.8))
                common_drawing.draw_circle(context, stroke[-1][0], Vector((0,1,0)),0.003,(.5,.5,.5,.8))

        for i_ge,gedge in enumerate(self.polystrips.gedges):
            if draw_gedge_directions:
                p0,p1,p2,p3 = gedge.gvert0.snap_pos, gedge.gvert1.snap_pos, gedge.gvert2.snap_pos, gedge.gvert3.snap_pos
                n0,n1,n2,n3 = gedge.gvert0.snap_norm, gedge.gvert1.snap_norm, gedge.gvert2.snap_norm, gedge.gvert3.snap_norm
                pm = cubic_bezier_blend_t(p0,p1,p2,p3,0.5)
                px = cubic_bezier_derivative(p0,p1,p2,p3,0.5).normalized()
                pn = (n0+n3).normalized()
                py = pn.cross(px).normalized()
                rs = (gedge.gvert0.radius+gedge.gvert3.radius) * 0.35
                rl = rs * 0.75
                p3d = [pm-px*rs,pm+px*rs,pm+px*(rs-rl)+py*rl,pm+px*rs,pm+px*(rs-rl)-py*rl]
                common_drawing.draw_polyline_from_3dpoints(context, p3d, (0.8,0.8,0.8,0.8),1, "GL_LINE_SMOOTH")

            if draw_gedge_bezier:
                p0,p1,p2,p3 = gedge.gvert0.snap_pos, gedge.gvert1.snap_pos, gedge.gvert2.snap_pos, gedge.gvert3.snap_pos
                p3d = [cubic_bezier_blend_t(p0,p1,p2,p3,t/16.0) for t in range(17)]
                common_drawing.draw_polyline_from_3dpoints(context, p3d, (0.5,0.5,0.5,0.8),1, "GL_LINE_SMOOTH")

            col = color_gedge if len(gedge.cache_igverts) else color_gedge_nocuts
            if gedge.zip_to_gedge: col = color_gedge_zipped
            if gedge == self.sel_gedge: col = sel_fn(col)
            w = 2 if len(gedge.cache_igverts) else 5
            for c0,c1,c2,c3 in gedge.iter_segments(only_visible=True):
                common_drawing.draw_polyline_from_3dpoints(context, [c0,c1,c2,c3,c0], col, w, "GL_LINE_SMOOTH")

            if draw_gedge_index:
                draw_gedge_text(gedge, context, str(i_ge))

            if draw_gedge_igverts:
                rm = (gedge.gvert0.radius + gedge.gvert3.radius)*0.1
                for igv in gedge.cache_igverts:
                    common_drawing.common_drawing.draw_circle(context, igv.position, igv.normal, rm, (1,1,1,.3))

        for i_gv,gv in enumerate(self.polystrips.gverts):
            if not gv.is_visible(): continue
            p0,p1,p2,p3 = gv.get_corners()

            if not draw_unconnected_gverts and gv.is_unconnected() and gv != self.sel_gvert: continue

            col = color_gvert_unconnected
            if gv.is_endpoint(): col = color_gvert_endpoint
            elif gv.is_endtoend(): col = color_gvert_endtoend
            elif gv.is_ljunction(): col = color_gvert_ljunction
            elif gv.is_tjunction(): col = color_gvert_tjunction
            elif gv.is_cross(): col = color_gvert_cross

            if gv == self.sel_gvert: col = sel_fn(col)

            p3d = [p0,p1,p2,p3,p0]
            common_drawing.draw_polyline_from_3dpoints(context, p3d, col, 2, "GL_LINE_SMOOTH")

            if draw_gvert_orientations:
                p,x,y = gv.snap_pos,gv.snap_tanx,gv.snap_tany
                common_drawing.draw_polyline_from_3dpoints(context, [p,p+x*0.005], (1,0,0,1), 1, "GL_LINE_SMOOTH")
                common_drawing.draw_polyline_from_3dpoints(context, [p,p+y*0.005], (0,1,0,1), 1, "GL_LINE_SMOOTH")

        if draw_gvert_unsnapped:
            for gv in self.polystrips.gverts:
                p,x,y,n = gv.position,gv.snap_tanx,gv.snap_tany,gv.snap_norm
                common_drawing.draw_polyline_from_3dpoints(context, [p,p+x*0.01], (1,0,0,1), 1, "GL_LINE_SMOOTH")
                common_drawing.draw_polyline_from_3dpoints(context, [p,p+y*0.01], (0,1,0,1), 1, "GL_LINE_SMOOTH")
                common_drawing.draw_polyline_from_3dpoints(context, [p,p+n*0.01], (0,0,1,1), 1, "GL_LINE_SMOOTH")

        if self.sel_gedge:
            if not self.sel_gedge.zip_to_gedge:
                col = color_gvert_midpoints
                for gv in self.sel_gedge.get_inner_gverts():
                    if not gv.is_visible(): continue
                    p0,p1,p2,p3 = gv.get_corners()
                    p3d = [p0,p1,p2,p3,p0]
                    common_drawing.draw_polyline_from_3dpoints(context, p3d, col, 2, "GL_LINE_SMOOTH")
            draw_gedge_info(self.sel_gedge, context)

        if self.sel_gvert:
            col = color_gvert_midpoints
            for ge in self.sel_gvert.get_gedges_notnone():
                if ge.zip_to_gedge: continue
                gv = ge.get_inner_gvert_at(self.sel_gvert)
                if not gv.is_visible(): continue
                p0,p1,p2,p3 = gv.get_corners()
                p3d = [p0,p1,p2,p3,p0]
                common_drawing.draw_polyline_from_3dpoints(context, p3d, col, 2, "GL_LINE_SMOOTH")

        if self.mode == 'sketch':
            common_drawing.draw_polyline_from_points(context, [self.sketch_curpos, self.sketch[-1][0]], (0.5,0.5,0.2,0.8), 1, "GL_LINE_SMOOTH")
            common_drawing.draw_polyline_from_points(context, [co[0] for co in self.sketch], (1,1,.5,.8), 2, "GL_LINE_SMOOTH")

            info = str(round(self.sketch_pressure,3))
            ''' draw text '''
            txt_width, txt_height = blf.dimensions(0, info)
            d = self.sketch_brush.pxl_rad
            blf.position(0, self.sketch_curpos[0] - txt_width/2, self.sketch_curpos[1] + d + txt_height, 0)
            blf.draw(0, info)

        if self.mode in {'scale tool','rotate tool'}:
            common_drawing.draw_polyline_from_points(context, [self.action_center, self.mode_pos], (0,0,0,0.5), 1, "GL_LINE_STIPPLE")

        bgl.glLineWidth(1)

        if self.mode not in {'grab tool','scale tool','rotate tool','brush scale tool'}:
            ray,hit = common_utilities.ray_cast_region2d(region, r3d, self.cur_pos, self.obj, settings)
            hit_p3d,hit_norm,hit_idx = hit
            if hit_idx != -1:
                mx = self.obj.matrix_world
                hit_p3d = mx * hit_p3d
                common_drawing.draw_circle(context, hit_p3d, hit_norm.normalized(), self.stroke_radius_pressure, (1,1,1,.5))

        if not self.hover_ed:
            self.sketch_brush.draw(context)
        else:
            common_drawing.draw_bmedge(context, self.hover_ed, self.to_obj.matrix_world, 2, color_selected)

    ############################
    # function to convert polystrips => mesh

    def create_mesh(self, context):
        verts,quads = self.polystrips.create_mesh()

        if self.to_bme and self.to_obj:  #EDIT MDOE on Existing Mesh
            bm = self.to_bme
            mx = self.to_obj.matrix_world
            imx = mx.inverted()

            mx2 = self.obj.matrix_world
            imx2 = mx2.inverted()

        else:
            bm = bmesh.new()
            mx2 = Matrix.Identity(4)
            imx = Matrix.Identity(4)

            nm_polystrips = self.obj.name + "_polystrips"

            dest_me  = bpy.data.meshes.new(nm_polystrips)
            dest_obj = bpy.data.objects.new(nm_polystrips, dest_me)

            dest_obj.matrix_world = self.obj.matrix_world
            dest_obj.update_tag()
            dest_obj.show_all_edges = True
            dest_obj.show_wire      = True
            dest_obj.show_x_ray     = True

            context.scene.objects.link(dest_obj)
            dest_obj.select = True
            context.scene.objects.active = dest_obj

        bmverts = [bm.verts.new(imx * mx2 * v) for v in verts]
        bm.verts.index_update()
        for q in quads: 
            bm.faces.new([bmverts[i] for i in q])

        bm.faces.index_update()

        if self.to_bme and self.to_obj:
            bmesh.update_edit_mesh(self.to_obj.data, tessface=False, destructive=True)
            bm.free()
        else: 
            bm.to_mesh(dest_me)
            bm.free()

    ###########################
    # hover functions

    def hover_geom(self,eventd):
        if not len(self.snap_eds): 
            return 
        context = eventd['context']
        region,r3d = context.region,context.space_data.region_3d
        new_matrix = [v for l in r3d.view_matrix for v in l]
        x, y = eventd['mouse']
        mouse_loc = Vector((x,y))
        mx = self.to_obj.matrix_world

        if self.post_update or self.last_matrix != new_matrix:
            # Update all the visibility stuff
            self.snap_eds_vis = [False not in common_utilities.ray_cast_visible([mx * ed.verts[0].co, mx * ed.verts[1].co], self.obj, r3d) for ed in self.snap_eds]

        # Sticky highlight...check the hovered edge first
        if self.hover_ed:
            a = location_3d_to_region_2d(region, r3d, mx * self.hover_ed.verts[0].co)
            b = location_3d_to_region_2d(region, r3d, mx * self.hover_ed.verts[1].co)

            if a and b:
                intersect = intersect_point_line(mouse_loc, a, b)
                dist = (intersect[0] - mouse_loc).length_squared
                bound = intersect[1]
                if (dist < 100) and (bound < 1) and (bound > 0):
                    return

        self.hover_ed = None
        for i,ed in enumerate(self.snap_eds):
            if self.snap_eds_vis[i]:
                a = location_3d_to_region_2d(region, r3d, mx * ed.verts[0].co)
                b = location_3d_to_region_2d(region, r3d, mx * ed.verts[1].co)
                if a and b:
                    intersect = intersect_point_line(mouse_loc, a, b)

                    dist = (intersect[0] - mouse_loc).length_squared
                    bound = intersect[1]
                    if (dist < 100) and (bound < 1) and (bound > 0):
                        self.hover_ed = ed
                        break

    ###########################
    # tool functions

    def ready_tool(self, eventd, tool_fn):
        rgn   = eventd['context'].region
        r3d   = eventd['context'].space_data.region_3d
        mx,my = eventd['mouse']
        if self.sel_gvert:
            loc   = self.sel_gvert.position
            cx,cy = location_3d_to_region_2d(rgn, r3d, loc)
        elif self.sel_gedge:
            loc   = (self.sel_gedge.gvert0.position + self.sel_gedge.gvert3.position) / 2.0
            cx,cy = location_3d_to_region_2d(rgn, r3d, loc)
        else:
            cx,cy = mx-100,my
        rad   = math.sqrt((mx-cx)**2 + (my-cy)**2)

        self.action_center = (cx,cy)
        self.mode_start    = (mx,my)
        self.action_radius = rad
        self.mode_radius   = rad
        
        self.prev_pos      = (mx,my)

        # spc = bpy.data.window_managers['WinMan'].windows[0].screen.areas[4].spaces[0]
        # r3d = spc.region_3d
        vrot = r3d.view_rotation
        self.tool_x = (vrot * Vector((1,0,0))).normalized()
        self.tool_y = (vrot * Vector((0,1,0))).normalized()

        self.tool_rot = 0.0

        self.tool_fn = tool_fn
        self.tool_fn('init', eventd)

    def scale_tool_gvert(self, command, eventd):
        if command == 'init':
            self.footer = 'Scaling GVerts'
            sgv = self.sel_gvert
            lgv = [ge.gvert1 if ge.gvert0==sgv else ge.gvert2 for ge in sgv.get_gedges() if ge]
            self.tool_data = [(gv,Vector(gv.position)) for gv in lgv]
        elif command == 'commit':
            pass
        elif command == 'undo':
            for gv,p in self.tool_data:
                gv.position = p
                gv.update()
            self.sel_gvert.update()
            self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)
        else:
            m = command
            sgv = self.sel_gvert
            p = sgv.position
            for ge in sgv.get_gedges():
                if not ge: continue
                gv = ge.gvert1 if ge.gvert0 == self.sel_gvert else ge.gvert2
                gv.position = p + (gv.position-p) * m
                gv.update()
            sgv.update()
            self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)

    def scale_tool_gvert_radius(self, command, eventd):
        if command == 'init':
            self.footer = 'Scaling GVert radius'
            self.tool_data = self.sel_gvert.radius
        elif command == 'commit':
            pass
        elif command == 'undo':
            self.sel_gvert.radius = self.tool_data
            self.sel_gvert.update()
            self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)
        else:
            m = command
            self.sel_gvert.radius *= m
            self.sel_gvert.update()
            self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)

    def scale_tool_stroke_radius(self, command, eventd):
        if command == 'init':
            self.footer = 'Scaling Stroke radius'
            self.tool_data = self.stroke_radius
        elif command == 'commit':
            pass
        elif command == 'undo':
            self.stroke_radius = self.tool_data
        else:
            m = command
            self.stroke_radius *= m

    def grab_tool_gvert_list(self, command, eventd, lgv):
        '''
        translates list of gverts
        note: translation is relative to first gvert
        '''

        def l3dr2d(p): return location_3d_to_region_2d(eventd['region'], eventd['r3d'], p)

        if command == 'init':
            self.footer = 'Translating GVert position(s)'
            s2d = l3dr2d(lgv[0].position)
            self.tool_data = [(gv, Vector(gv.position), l3dr2d(gv.position)-s2d) for gv in lgv]
        elif command == 'commit':
            pass
        elif command == 'undo':
            for gv,p,_ in self.tool_data: gv.position = p
            for gv,_,_ in self.tool_data:
                gv.update()
                gv.update_visibility(eventd['r3d'], update_gedges=True)
        else:
            factor_slow,factor_fast = 0.2,1.0
            dv = Vector(command) * (factor_slow if eventd['shift'] else factor_fast)
            s2d = l3dr2d(self.tool_data[0][0].position)
            lgv2d = [s2d+relp+dv for _,_,relp in self.tool_data]
            pts = common_utilities.ray_cast_path(eventd['context'], self.obj, lgv2d)
            if len(pts) != len(lgv2d): return ''
            for d,p2d in zip(self.tool_data, pts):
                d[0].position = p2d
            for gv,_,_ in self.tool_data:
                gv.update()
                gv.update_visibility(eventd['r3d'], update_gedges=True)

    def grab_tool_gvert(self, command, eventd):
        '''
        translates selected gvert
        '''
        if command == 'init':
            lgv = [self.sel_gvert]
        else:
            lgv = None
        self.grab_tool_gvert_list(command, eventd, lgv)

    def grab_tool_gvert_neighbors(self, command, eventd):
        '''
        translates selected gvert and its neighbors
        note: translation is relative to selected gvert
        '''
        if command == 'init':
            sgv = self.sel_gvert
            lgv = [sgv] + [ge.get_inner_gvert_at(sgv) for ge in sgv.get_gedges_notnone()]
        else:
            lgv = None
        self.grab_tool_gvert_list(command, eventd, lgv)

    def grab_tool_gedge(self, command, eventd):
        if command == 'init':
            sge = self.sel_gedge
            lgv = [sge.gvert0, sge.gvert3]
            lgv += [ge.get_inner_gvert_at(gv) for gv in lgv for ge in gv.get_gedges_notnone()]
        else:
            lgv = None
        self.grab_tool_gvert_list(command, eventd, lgv)

    def rotate_tool_gvert_neighbors(self, command, eventd):
        if command == 'init':
            self.footer = 'Rotating GVerts'
            self.tool_data = [(gv,Vector(gv.position)) for gv in self.sel_gvert.get_inner_gverts()]
        elif command == 'commit':
            pass
        elif command == 'undo':
            for gv,p in self.tool_data:
                gv.position = p
                gv.update()
        else:
            ang = command
            q = Quaternion(self.sel_gvert.snap_norm, ang)
            p = self.sel_gvert.position
            for gv,up in self.tool_data:
                gv.position = p+q*(up-p)
                gv.update()

    def scale_brush_pixel_radius(self,command, eventd):
        if command == 'init':
            self.footer = 'Scale Brush Pixel Size'
            self.tool_data = self.stroke_radius
            x,y = eventd['mouse']
            self.sketch_brush.brush_pix_size_init(eventd['context'], x, y)
        elif command == 'commit':
            self.sketch_brush.brush_pix_size_confirm(eventd['context'])
            if self.sketch_brush.world_width:
                self.stroke_radius = self.sketch_brush.world_width
        elif command == 'undo':
            self.sketch_brush.brush_pix_size_cancel(eventd['context'])
            self.stroke_radius = self.tool_data
        else:
            x,y = command
            self.sketch_brush.brush_pix_size_interact(x, y, precise = eventd['shift'])


    ##############################
    # modal state functions

    def modal_nav(self, eventd):
        events_numpad = {
            'NUMPAD_1',       'NUMPAD_2',       'NUMPAD_3',
            'NUMPAD_4',       'NUMPAD_5',       'NUMPAD_6',
            'NUMPAD_7',       'NUMPAD_8',       'NUMPAD_9',
            'CTRL+NUMPAD_1',  'CTRL+NUMPAD_2',  'CTRL+NUMPAD_3',
            'CTRL+NUMPAD_4',  'CTRL+NUMPAD_5',  'CTRL+NUMPAD_6',
            'CTRL+NUMPAD_7',  'CTRL+NUMPAD_8',  'CTRL+NUMPAD_9',
            'SHIFT+NUMPAD_1', 'SHIFT+NUMPAD_2', 'SHIFT+NUMPAD_3',
            'SHIFT+NUMPAD_4', 'SHIFT+NUMPAD_5', 'SHIFT+NUMPAD_6',
            'SHIFT+NUMPAD_7', 'SHIFT+NUMPAD_8', 'SHIFT+NUMPAD_9',
            'NUMPAD_PLUS', 'NUMPAD_MINUS', # CTRL+NUMPAD_PLUS and CTRL+NUMPAD_MINUS are used elsewhere
            'NUMPAD_PERIOD',
        }

        handle_nav = False
        handle_nav |= eventd['type'] == 'MIDDLEMOUSE'
        handle_nav |= eventd['type'] == 'MOUSEMOVE' and self.is_navigating
        handle_nav |= eventd['type'].startswith('NDOF_')
        handle_nav |= eventd['type'].startswith('TRACKPAD')
        handle_nav |= eventd['ftype'] in events_numpad
        handle_nav |= eventd['ftype'] in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}

        if handle_nav:
            self.post_update = True
            self.is_navigating = True

            # x,y = eventd['mouse']
            # self.sketch_brush.update_mouse_move_hover(eventd['context'], x,y)
            # self.sketch_brush.make_circles()
            # self.sketch_brush.get_brush_world_size(eventd['context'])
            
            # if self.sketch_brush.world_width:
            #     self.stroke_radius = self.sketch_brush.world_width
            #     self.stroke_radius_pressure = self.sketch_brush.world_width
                
            return 'nav' if eventd['value']=='PRESS' else 'main'

        self.is_navigating = False
        return ''

    def modal_main(self, eventd):
        self.footer = 'LMB: draw, RMB: select, G: grab, R: rotate, S: scale, F: brush size, K: knife, M: merge, X: delete, CTRL+D: dissolve, CTRL+Wheel Up/Down: adjust segments, CTRL+C: change selected junction type'

        #############################################
        # General navigation

        nmode = self.modal_nav(eventd)
        if nmode:
            return nmode

        ########################################
        # accept / cancel

        if eventd['press'] in {'RET', 'NUMPAD_ENTER'}:
            self.create_mesh(eventd['context'])
            eventd['context'].area.header_text_set()
            return 'finish'

        if eventd['press'] in {'ESC'}:
            eventd['context'].area.header_text_set()
            return 'cancel'

        #####################################
        # General

        if eventd['type'] == 'MOUSEMOVE':  #mouse movement/hovering
            #update brush and brush size
            x,y = eventd['mouse']
            self.sketch_brush.update_mouse_move_hover(eventd['context'], x,y)
            self.sketch_brush.make_circles()
            self.sketch_brush.get_brush_world_size(eventd['context'])

            if self.sketch_brush.world_width:
                self.stroke_radius = self.sketch_brush.world_width
                self.stroke_radius_pressure = self.sketch_brush.world_width

            self.hover_geom(eventd)

        if eventd['press'] == 'CTRL+Z':
            self.undo_action()
            return ''

        if eventd['press'] == 'F':
            self.ready_tool(eventd, self.scale_brush_pixel_radius)
            return 'brush scale tool'

        if eventd['press'] == 'Q':                                                  # profiler printout
            profiler.printout()
            return ''

        if eventd['press'] == 'P':                                                  # grease pencil => strokes
            # TODO: only convert gpencil strokes that are visible and prevent duplicate conversion
            for gpl in self.obj.grease_pencil.layers: gpl.hide = True
            for stroke in self.strokes_original:
                self.polystrips.insert_gedge_from_stroke(stroke, True)
            self.polystrips.remove_unconnected_gverts()
            self.polystrips.update_visibility(eventd['r3d'])
            return ''

        if eventd['press'] in {'LEFTMOUSE', 'SHIFT+LEFTMOUSE'}:
            self.create_undo_snapshot('sketch')                   
            # start sketching
            self.footer = 'Sketching'
            x,y = eventd['mouse']
            p   = eventd['pressure']
            r   = eventd['mradius']

            self.sketch_curpos = (x,y)

            if eventd['shift'] and self.sel_gvert:
                # continue sketching from selected gvert position
                gvx,gvy = location_3d_to_region_2d(eventd['region'], eventd['r3d'], self.sel_gvert.position)
                self.sketch = [((gvx,gvy),self.sel_gvert.radius), ((x,y),r)]
            else:
                self.sketch = [((x,y),r)]

            self.sel_gvert = None
            self.sel_gedge = None
            return 'sketch'

        if eventd['press'] == 'RIGHTMOUSE':                                         # picking
            x,y = eventd['mouse']
            pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
            if not pts:
                self.sel_gvert,self.sel_gedge,self.act_gvert = None,None,None
                return ''
            pt = pts[0]

            if self.sel_gvert or self.sel_gedge:
                # check if user is picking an inner control point
                if self.sel_gedge and not self.sel_gedge.zip_to_gedge:
                    lcpts = [self.sel_gedge.gvert1,self.sel_gedge.gvert2]
                elif self.sel_gvert:
                    sgv = self.sel_gvert
                    lge = self.sel_gvert.get_gedges()
                    lcpts = [ge.get_inner_gvert_at(sgv) for ge in lge if ge and not ge.zip_to_gedge] + [sgv]
                else:
                    lcpts = []

                for cpt in lcpts:
                    if not cpt.is_picked(pt): continue
                    self.sel_gvert = cpt
                    self.sel_gedge = None
                    return ''

            for gv in self.polystrips.gverts:
                if gv.is_unconnected(): continue
                if not gv.is_picked(pt): continue
                self.sel_gvert = gv
                self.sel_gedge = None
                return ''

            for ge in self.polystrips.gedges:
                if not ge.is_picked(pt): continue
                self.sel_gvert = None
                self.sel_gedge = ge
                return ''

            self.sel_gedge,self.sel_gvert = None,None
            return ''

        if eventd['press'] == 'CTRL+LEFTMOUSE':                                     # delete/dissolve
            x,y = eventd['mouse']
            pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
            if not pts:
                self.sel_gvert,self.sel_gedge,self.act_gvert = None,None,None
                return ''
            pt = pts[0]

            for gv in self.polystrips.gverts:
                if not gv.is_picked(pt): continue
                if not (gv.is_endpoint() or gv.is_endtoend() or gv.is_ljunction()): continue

                if gv.is_endpoint():
                    self.polystrips.disconnect_gvert(gv)
                else:
                    self.polystrips.dissolve_gvert(gv)

                self.polystrips.remove_unconnected_gverts()
                self.polystrips.update_visibility(eventd['r3d'])

                self.sel_gvert = None
                self.sel_gedge = None
                return ''

            for ge in self.polystrips.gedges:
                if not ge.is_picked(pt): continue

                self.polystrips.disconnect_gedge(ge)
                self.polystrips.remove_unconnected_gverts()

                self.sel_gvert = None
                self.sel_gedge = None
                return ''

            self.sel_gedge,self.sel_gvert = None,None
            return ''

        if eventd['press'] == 'CTRL+U':
            self.create_undo_snapshot('update')
            for gv in self.polystrips.gverts:
                gv.update_gedges()

        ###################################
        # Selected gedge commands

        if self.sel_gedge:
            if eventd['press'] == 'X':
                self.create_undo_snapshot('delete')
                self.polystrips.disconnect_gedge(self.sel_gedge)
                self.sel_gedge = None
                self.polystrips.remove_unconnected_gverts()
                return ''

            if eventd['press'] == 'K' and not self.sel_gedge.is_zippered() and not self.sel_gedge.has_zippered():
                self.create_undo_snapshot('knife')
                x,y = eventd['mouse']
                pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
                if not pts:
                    return ''
                pt = pts[0]
                t,_    = self.sel_gedge.get_closest_point(pt)
                _,_,gv = self.polystrips.split_gedge_at_t(self.sel_gedge, t)
                self.sel_gedge = None
                self.sel_gvert = gv

            if eventd['press'] == 'U':
                self.create_undo_snapshot('update')
                self.sel_gedge.gvert0.update_gedges()
                self.sel_gedge.gvert3.update_gedges()
                return ''

            if eventd['press']in {'OSKEY+WHEELUPMOUSE', 'CTRL+NUMPAD_PLUS'}:
                self.create_undo_snapshot('count')
                self.sel_gedge.n_quads += 1
                self.sel_gedge.force_count = True
                self.sel_gedge.update()
                return ''

            if eventd['press'] in {'OSKEY+WHEELDOWNMOUSE', 'CTRL+NUMPAD_MINUS'}:

                if self.sel_gedge.n_quads > 3:
                    self.create_undo_snapshot('count')
                    self.sel_gedge.n_quads -= 1
                    self.sel_gedge.force_count = True
                    self.sel_gedge.update()
                return ''

            if eventd['press'] == 'Z':

                if self.sel_gedge.zip_to_gedge:
                    self.create_undo_snapshot('unzip')
                    self.sel_gedge.unzip()
                    return ''

                lge = self.sel_gedge.gvert0.get_gedges_notnone() + self.sel_gedge.gvert3.get_gedges_notnone()
                if any(ge.is_zippered() for ge in lge):
                    # prevent zippering a gedge with gvert that has a zippered gedge already
                    # TODO: allow this??
                    return ''

                x,y = eventd['mouse']
                pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
                if not pts:
                    return ''
                pt = pts[0]
                for ge in self.polystrips.gedges:
                    if ge == self.sel_gedge: continue
                    if not ge.is_picked(pt): continue
                    self.create_undo_snapshot('zip')
                    self.sel_gedge.zip_to(ge)
                    return ''
                return ''

            if eventd['press'] == 'G':
                if not self.sel_gedge.is_zippered():
                    self.create_undo_snapshot('grab')
                    self.ready_tool(eventd, self.grab_tool_gedge)
                    return 'grab tool'
                return ''

            if eventd['press'] == 'A':
                self.sel_gvert = self.sel_gedge.gvert0
                self.sel_gedge = None
                return ''
            if eventd['press'] == 'B':
                self.sel_gvert = self.sel_gedge.gvert3
                self.sel_gedge = None
                return ''

            if eventd['press'] == 'CTRL+R' and not self.sel_gedge.is_zippered():
                self.create_undo_snapshot('rib')
                self.sel_gedge = self.polystrips.rip_gedge(self.sel_gedge)
                self.ready_tool(eventd, self.grab_tool_gedge)
                return 'grab tool'

        ###################################
        # selected gvert commands

        if self.sel_gvert:

            if eventd['press'] == 'K':
                if not self.sel_gvert.is_endpoint():
                    print('Selected GVert must be endpoint (exactly one GEdge)')
                    return ''
                x,y = eventd['mouse']
                pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
                if not pts:
                    return ''
                pt = pts[0]
                for ge in self.polystrips.gedges:
                    if not ge.is_picked(pt): continue
                    self.create_undo_snapshot('split')
                    t,d = ge.get_closest_point(pt)
                    self.polystrips.split_gedge_at_t(ge, t, connect_gvert=self.sel_gvert)
                    return ''
                return ''

            if eventd['press'] == 'X':
                if self.sel_gvert.is_inner():
                    return ''
                self.create_undo_snapshot('delete')
                self.polystrips.disconnect_gvert(self.sel_gvert)
                self.sel_gvert = None
                self.polystrips.remove_unconnected_gverts()
                return ''

            if eventd['press'] == 'CTRL+D':
                self.create_undo_snapshot('dissolve')
                self.polystrips.dissolve_gvert(self.sel_gvert)
                self.sel_gvert = None
                self.polystrips.remove_unconnected_gverts()
                self.polystrips.update_visibility(eventd['r3d'])
                return ''

            if eventd['press'] == 'S':
                self.create_undo_snapshot('scale')
                self.ready_tool(eventd, self.scale_tool_gvert_radius)
                return 'scale tool'

            if eventd['press'] == 'CTRL+G':
                self.create_undo_snapshot('grab')
                self.ready_tool(eventd, self.grab_tool_gvert)
                return 'grab tool'

            if eventd['press'] == 'G':
                self.create_undo_snapshot('grab')
                self.ready_tool(eventd, self.grab_tool_gvert_neighbors)
                return 'grab tool'

            if eventd['press'] == 'CTRL+C':
                self.create_undo_snapshot('toggle')
                self.sel_gvert.toggle_corner()
                self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)
                return ''

            if eventd['press'] == 'CTRL+S':
                self.create_undo_snapshot('scale')
                self.ready_tool(eventd, self.scale_tool_gvert)
                return 'scale tool'

            if eventd['press'] == 'C':
                self.create_undo_snapshot('smooth')
                self.sel_gvert.smooth()
                self.sel_gvert.update_visibility(eventd['r3d'], update_gedges=True)
                return ''

            if eventd['press'] == 'R':
                self.create_undo_snapshot('rotate')
                self.ready_tool(eventd, self.rotate_tool_gvert_neighbors)
                return 'rotate tool'

            if eventd['press'] == 'U':
                self.sel_gvert.update_gedges()
                return ''

            if eventd['press'] == 'CTRL+R':
                # self.polystrips.rip_gvert(self.sel_gvert)
                # self.sel_gvert = None
                # return ''
                x,y = eventd['mouse']
                pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
                if not pts:
                    return ''
                pt = pts[0]
                for ge in self.sel_gvert.get_gedges_notnone():
                    if not ge.is_picked(pt): continue
                    self.create_undo_snapshot('rip')
                    self.sel_gvert = self.polystrips.rip_gedge(ge, at_gvert=self.sel_gvert)
                    self.ready_tool(eventd, self.grab_tool_gvert_neighbors)
                    return 'grab tool'
                return ''
  
            if eventd['press'] == 'M':
                if self.sel_gvert.is_inner(): return ''
                x,y = eventd['mouse']
                pts = common_utilities.ray_cast_path(eventd['context'], self.obj, [(x,y)])
                if not pts:
                    return ''
                pt = pts[0]
                sel_ge = set(self.sel_gvert.get_gedges_notnone())
                for gv in self.polystrips.gverts:
                    if gv.is_inner() or not gv.is_picked(pt) or gv == self.sel_gvert: continue
                    if len(self.sel_gvert.get_gedges_notnone()) + len(gv.get_gedges_notnone()) > 4:
                        dprint('Too many connected GEdges for merge!')
                        continue
                    if any(ge in sel_ge for ge in gv.get_gedges_notnone()):
                        dprint('Cannot merge GVerts that share a GEdge')
                        continue
                    self.create_undo_snapshot('merge')
                    self.polystrips.merge_gverts(self.sel_gvert, gv)
                    self.sel_gvert = gv
                    return ''
                return ''

            if self.sel_gvert.zip_over_gedge:
                gvthis = self.sel_gvert
                gvthat = self.sel_gvert.get_zip_pair()

                if eventd['press'] == 'CTRL+NUMPAD_PLUS':
                    self.create_undo_snapshot('zip count')
                    max_t = 1 if gvthis.zip_t>gvthat.zip_t else gvthat.zip_t-0.05
                    gvthis.zip_t = min(gvthis.zip_t+0.05, max_t)
                    gvthis.zip_over_gedge.update()
                    dprint('+ %f %f' % (min(gvthis.zip_t, gvthat.zip_t),max(gvthis.zip_t, gvthat.zip_t)), l=4)
                    return ''

                if eventd['press'] == 'CTRL+NUMPAD_MINUS':
                    self.create_undo_snapshot('zip count')
                    min_t = 0 if gvthis.zip_t<gvthat.zip_t else gvthat.zip_t+0.05
                    gvthis.zip_t = max(gvthis.zip_t-0.05, min_t)
                    gvthis.zip_over_gedge.update()
                    dprint('- %f %f' % (min(gvthis.zip_t, gvthat.zip_t),max(gvthis.zip_t, gvthat.zip_t)), l=4)
                    return ''

        return ''

    def modal_sketching(self, eventd):
        #my_str = eventd['type'] + ' ' + str(round(eventd['pressure'],2)) + ' ' + str(round(self.stroke_radius_pressure,2))
        #print(my_str)
        if eventd['type'] == 'MOUSEMOVE':
            x,y = eventd['mouse']
            p = eventd['pressure']
            r = eventd['mradius']

            stroke_point = self.sketch[-1]

            (lx, ly) = stroke_point[0]
            lr = stroke_point[1]
            self.sketch_curpos = (x,y)
            self.sketch_pressure = p

            ss0,ss1 = self.stroke_smoothing,1-self.stroke_smoothing
            # Smooth radii
            self.stroke_radius_pressure = lr*ss0 + r*ss1

            self.sketch += [((lx*ss0+x*ss1, ly*ss0+y*ss1), self.stroke_radius_pressure)]

            return ''

        if eventd['release'] in {'LEFTMOUSE','SHIFT+LEFTMOUSE'}:
            # correct for 0 pressure on release
            if self.sketch[-1][1] == 0:
                self.sketch[-1] = self.sketch[-2]

            p3d = common_utilities.ray_cast_stroke(eventd['context'], self.obj, self.sketch) if len(self.sketch) > 1 else []
            if len(p3d) <= 1: return 'main'

            # tessellate stroke (if needed) so we have good stroke sampling
            # TODO, tesselate pressure/radius values?
            # length_tess = self.length_scale / 700
            # p3d = [(p0+(p1-p0).normalized()*x) for p0,p1 in zip(p3d[:-1],p3d[1:]) for x in frange(0,(p0-p1).length,length_tess)] + [p3d[-1]]
            # stroke = [(p,self.stroke_radius) for i,p in enumerate(p3d)]

            stroke = p3d
            self.sketch = []
            dprint('')
            dprint('')
            dprint('inserting stroke')
            self.polystrips.insert_gedge_from_stroke(stroke, False)
            self.polystrips.remove_unconnected_gverts()
            self.polystrips.update_visibility(eventd['r3d'])
            return 'main'

        return ''

    ##############################
    # modal tool functions

    def modal_scale_tool(self, eventd):
        cx,cy = self.action_center
        mx,my = eventd['mouse']
        ar = self.action_radius
        pr = self.mode_radius
        cr = math.sqrt((mx-cx)**2 + (my-cy)**2)

        if eventd['press'] in {'RET','NUMPAD_ENTER','LEFTMOUSE'}:
            self.tool_fn('commit', eventd)
            return 'main'

        if eventd['press'] in {'ESC', 'RIGHTMOUSE'}:
            self.tool_fn('undo', eventd)
            return 'main'

        if eventd['type'] == 'MOUSEMOVE':
            self.tool_fn(cr / pr, eventd)
            self.mode_radius = cr
            return ''

        return ''

    def modal_grab_tool(self, eventd):
        cx,cy = self.action_center
        mx,my = eventd['mouse']
        px,py = self.prev_pos #mode_pos
        sx,sy = self.mode_start

        if eventd['press'] in {'RET','NUMPAD_ENTER','LEFTMOUSE','SHIFT+RET','SHIFT+NUMPAD_ENTER','SHIFT+LEFTMOUSE'}:
            self.tool_fn('commit', eventd)
            return 'main'

        if eventd['press'] in {'ESC','RIGHTMOUSE'}:
            self.tool_fn('undo', eventd)
            return 'main'

        if eventd['type'] == 'MOUSEMOVE':
            self.tool_fn((mx-px,my-py), eventd)
            self.prev_pos = (mx,my)
            return ''

        return ''

    def modal_rotate_tool(self, eventd):
        cx,cy = self.action_center
        mx,my = eventd['mouse']
        px,py = self.prev_pos #mode_pos

        if eventd['press'] in {'RET', 'NUMPAD_ENTER', 'LEFTMOUSE'}:
            self.tool_fn('commit', eventd)
            return 'main'

        if eventd['press'] in {'ESC', 'RIGHTMOUSE'}:
            self.tool_fn('undo', eventd)
            return 'main'

        if eventd['type'] == 'MOUSEMOVE':
            vp = Vector((px-cx,py-cy,0))
            vm = Vector((mx-cx,my-cy,0))
            ang = vp.angle(vm) * (-1 if vp.cross(vm).z<0 else 1)
            self.tool_rot += ang
            self.tool_fn(self.tool_rot, eventd)
            self.prev_pos = (mx,my)
            return ''

        return ''

    def modal_scale_brush_pixel_tool(self, eventd):
        '''
        This is the pixel brush radius
        self.tool_fn is expected to be self.
        '''
        mx,my = eventd['mouse']

        if eventd['press'] in {'RET','NUMPAD_ENTER','LEFTMOUSE'}:
            self.tool_fn('commit', eventd)
            return 'main'

        if eventd['press'] in {'ESC', 'RIGHTMOUSE'}:
            self.tool_fn('undo', eventd)

            return 'main'

        if eventd['type'] == 'MOUSEMOVE':
            '''
            '''
            self.tool_fn((mx,my), eventd)

            return ''

        return ''

    ###########################
    # main modal function (FSM)

    def modal(self, context, event):
        context.area.tag_redraw()
        settings = common_utilities.get_settings()

        eventd = self.get_event_details(context, event)

        if self.footer_last != self.footer:
            context.area.header_text_set('PolyStrips: %s' % self.footer)
            self.footer_last = self.footer

        FSM = {}
        FSM['main'] = self.modal_main
        FSM['nav'] = self.modal_nav
        FSM['sketch'] = self.modal_sketching
        FSM['scale tool'] = self.modal_scale_tool
        FSM['grab tool'] = self.modal_grab_tool
        FSM['rotate tool'] = self.modal_rotate_tool
        FSM['brush scale tool'] = self.modal_scale_brush_pixel_tool

        self.cur_pos = eventd['mouse']
        nmode = FSM[self.mode](eventd)
        self.mode_pos = eventd['mouse']

        self.is_navigating = (nmode == 'nav')
        if nmode == 'nav': return {'PASS_THROUGH'}

        if nmode in {'finish','cancel'}:
            self.kill_timer(context)
            polystrips_undo_cache = []
            return {'FINISHED'} if nmode == 'finish' else {'CANCELLED'}

        if nmode: self.mode = nmode

        return {'RUNNING_MODAL'}

    ###########################################################
    # functions to convert beziers and gpencils to polystrips

    def create_polystrips_from_bezier(self, ob_bezier):
        data  = ob_bezier.data
        mx    = ob_bezier.matrix_world

        def create_gvert(self, mx, co, radius):
            p0  = mx * co
            r0  = radius
            n0  = Vector((0,0,1))
            tx0 = Vector((1,0,0))
            ty0 = Vector((0,1,0))
            return GVert(self.obj,p0,r0,n0,tx0,ty0)

        for spline in data.splines:
            pregv = None
            for bp0,bp1 in zip(spline.bezier_points[:-1],spline.bezier_points[1:]):
                gv0 = pregv if pregv else self.create_gvert(mx, bp0.co, 0.2)
                gv1 = self.create_gvert(mx, bp0.handle_right, 0.2)
                gv2 = self.create_gvert(mx, bp1.handle_left, 0.2)
                gv3 = self.create_gvert(mx, bp1.co, 0.2)

                ge0 = GEdge(self.obj, gv0, gv1, gv2, gv3)
                ge0.recalc_igverts_approx()
                ge0.snap_igverts_to_object()

                if pregv:
                    self.polystrips.gverts += [gv1,gv2,gv3]
                else:
                    self.polystrips.gverts += [gv0,gv1,gv2,gv3]
                self.polystrips.gedges += [ge0]
                pregv = gv3

    def create_polystrips_from_greasepencil(self):
        Mx = self.obj.matrix_world
        gp = self.obj.grease_pencil
        gp_layers = gp.layers
        # for gpl in gp_layers: gpl.hide = True
        strokes = [[(p.co,p.pressure) for p in stroke.points] for layer in gp_layers for frame in layer.frames for stroke in frame.strokes]
        self.strokes_original = strokes

        #for stroke in strokes:
        #    self.polystrips.insert_gedge_from_stroke(stroke)


    ##########################
    # General functions

    def kill_timer(self, context):
        if not self._timer: return
        context.window_manager.event_timer_remove(self._timer)
        self._timer = None

    def get_event_details(self, context, event):
        '''
        Construct an event dict that is *slightly* more convenient than
        stringing together a bunch of logical conditions
        '''

        event_ctrl = 'CTRL+'  if event.ctrl  else ''
        event_shift = 'SHIFT+' if event.shift else ''
        event_alt = 'ALT+'   if event.alt   else ''
        event_oskey = 'OSKEY+' if event.oskey else ''
        event_ftype = event_ctrl + event_shift + event_alt + event_oskey + event.type

        event_pressure = 1 if not hasattr(event, 'pressure') else event.pressure

        def pressure_to_radius(r, p, map = 3):
            if   map == 0:  p = max(0.25,p)
            elif map == 1:  p = 0.25 + .75 * p
            elif map == 2:  p = max(0.05,p)
            elif map == 3:  p = .7 * (2.25*p-1)/((2.25*p-1)**2 +1)**.5 + .55
            return r*p

        return {
            'context': context,
            'region': context.region,
            'r3d': context.space_data.region_3d,

            'ctrl': event.ctrl,
            'shift': event.shift,
            'alt': event.alt,
            'value': event.value,
            'type': event.type,
            'ftype': event_ftype,
            'press': event_ftype if event.value=='PRESS'   else None,
            'release': event_ftype if event.value=='RELEASE' else None,

            'mouse': (float(event.mouse_region_x), float(event.mouse_region_y)),
            'pressure': event_pressure,
            'mradius': pressure_to_radius(self.stroke_radius, event_pressure),
            }