# ##### BEGIN GPL LICENSE BLOCK #####
#
#  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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

__bpydoc__ = """\
This addon enables you to straighten out multiple connected verts between their endpoints.


Documentation

First go to Preferences->Add-ons and enable the StraightenPlus addon in the Mesh category.
Go to EditMode, select one or more connected-edges-snakes (you can straighten several at the same time).
The vertices will be straightened out between the endpoints.  Choose amount of straightening with slider
or tweak interactively with LEFTMOUSE.
Restrict axis toggle restricts straightening to the view plane.
ENTER  to end operation and keep values, RIGHTMOUSE or ESC to cancel.

If you wish to hotkey StraightenPlus:
In the Keymap section of the Preferences, at the bottom of the 3D View > Mesh Mode > Mesh(Global) section click 'Add New' button.
In the Operator Identifier box put 'mesh.straightenplus'.
Assign a hotkey.
Save Preferences (Optional).
"""


bl_info = {
	"name": "StraightenPlus",
	"author": "Gert De Roost",
	"version": (0, 4, 0),
	"blender": (2, 80, 0),
	"location": "View3D > Tools",
	"description": "Straighten connected verts",
	"warning": "",
	"wiki_url": "",
	"tracker_url": "",
	"category": "Mesh"}



import bpy
import bmesh
from mathutils import Vector
import math


started = False


class MyProps(bpy.types.PropertyGroup):
	CancelAxis : bpy.props.BoolProperty(
		name = "Restrict axis", 
		description = "Dont straighten along the view axis",
		default = False)

	Percentage : bpy.props.FloatProperty(
		name = "Amount", 
		description = "Amount of straightening",
        default = 1,
        min = 0,
        max = 1)

	
class MESH_OT_StraightenPlus(bpy.types.Operator):
	bl_idname = "mesh.straightenplus"
	bl_label = "StraightenPlus"
	bl_description = "Straighten connected verts"
	bl_options = {'REGISTER', 'UNDO'}
	
	@classmethod
	def poll(cls, context):
		obj = context.active_object
		return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')

	def invoke(self, context, event):
	
		global started, mainop
		
		started = True
		
		mainop = self
		
		area = context.area
		area.header_text_set(text="Straighten :  Leftmouse tweaks - Enter confirms - Rightmouse/ESC cancels")

		self.scn = context.scene
		self.rv3d = context.space_data.region_3d
		self.oldperc = False
		self.tweaking = False
		self.prepare_lists(context)
		result = self.do_straighten()
		if result == False:
			context.area.header_text_set(None)
			self.bm.free()
			bpy.ops.object.editmode_toggle()
			bpy.ops.object.editmode_toggle()
			started = False
			return {'FINISHED'}			
		
		context.window_manager.modal_handler_add(self)
		
		return {'RUNNING_MODAL'}


	def modal(self, context, event):
		
		global started
		
		if event.type in {'RIGHTMOUSE', 'ESC'}:
			# cancel operation, reset mesh
			context.area.header_text_set(None)
			for vlist in self.vertlist:
				for (v, co) in vlist:
					v.co = co
			self.bm.free()
			bpy.ops.object.editmode_toggle()
			bpy.ops.object.editmode_toggle()
			started = False
			return {'CANCELLED'}
			
		elif event.type in {'MIDDLEMOUSE', 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'}:
			return {'PASS_THROUGH'}
			
		elif event.type in {'MOUSEMOVE', 'LEFTMOUSE'}:
			mxa = event.mouse_x
			mya = event.mouse_y
			self.region = None
			for a in bpy.context.screen.areas:
				if not(a.type == 'VIEW_3D'):
					continue
				for r in a.regions:
					if not(r.type == 'UI'):
						continue
					if mxa > r.x and mya > r.y and mxa < r.x + r.width and mya < r.y + r.height:
						self.region = r
						break
			if self.region:
				return {'PASS_THROUGH'}
				
		if event.type in {'RET', 'NUMPAD_ENTER'}:
			if not(self.region):
				return {'PASS_THROUGH'}
			context.area.header_text_set(None)
			# Consolidate changes if ENTER pressed.
			# Free the bmesh.
			self.bm.free()
			bpy.ops.object.editmode_toggle()
			bpy.ops.object.editmode_toggle()
			started = 0
			return {'FINISHED'}
			
		return {'RUNNING_MODAL'}
		

	def prepare_lists(self, context):
	
		bpy.ops.object.editmode_toggle()
		bpy.ops.object.editmode_toggle()
		selobj = context.active_object
		self.mesh = selobj.data
		self.bm = bmesh.from_edit_mesh(self.mesh)
	
		self.selset = set([])
		for edge in self.bm.edges:
			if edge.select:
				self.selset.add(edge)
	
	
		def addstart(vert, posn):
			
			# recursive: adds to initial edgelist at start
			for e in vert.link_edges:
				if e in self.selset:
					self.selset.discard(e)
					v = e.other_vert(vert)
					self.vertlist[posn].insert(0, (v, Vector(v.co[:])))
					addstart(v, posn)
					break
		
		def addend(vert, posn):
			
			# recursive: adds to initial edgelist at end
			for e in vert.link_edges:
				if e in self.selset:
					self.selset.discard(e)
					v = e.other_vert(vert)
					self.vertlist[posn].append((v, Vector(v.co[:])))
					addend(v, posn)
					break
		
		posn = 0
		self.vertlist = []
		while len(self.selset) > 0:
			# initialize next edgesnake		
			self.vertlist.append([])
			elem = self.selset.pop()
			vert = elem.verts[0]
			self.selset.add(elem)
			self.vertlist[posn].append((vert, Vector(vert.co[:])))
			# add to start and end of arbitrary start vert
			addstart(vert, posn)
			addend(vert, posn)
			posn += 1
		
	
	
	def do_straighten(self):
	
		for vlist in self.vertlist:
			vstart = vlist[0][0]
			vend = vlist[len(vlist) - 1][0]
			for (vert, vco) in vlist:
				savco = vco[:]
				# P' = A + {(AB dot AP) / || AB ||^2} AB
				ab = vend.co - vstart.co
				if ab.length == 0:
					return False
				ap = vco - vstart.co
				perpco = vstart.co + ((ab.dot(ap) / (ab.length ** 2)) * ab)
				vert.co = vco + ((perpco - vco) * (self.scn.my_props.Percentage))
				
				if self.scn.my_props.CancelAxis:
					# cancel movement in direction perp view
					delta = (vert.co - vco)
					if delta.length != 0:
						eyevec = Vector(self.rv3d.view_matrix[2][:3])
						ang = delta.angle(eyevec)
						deltanor = math.cos(ang) * delta.length
						nor = eyevec
						nor.length = abs(deltanor)
						if deltanor >= 0:
							nor = -1*nor
						vert.co = vert.co + nor		
	
		self.mesh.update()
				

class PANEL_PT_StraightenPlusPanel(bpy.types.Panel):
	bl_label = "StraightenPlus"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'UI'
	bl_category = 'StraightenPlus'

	def draw(self, context):

		self.layout.use_property_split = True
		scn = context.scene
		self.layout.operator("mesh.straightenplus", text="Straighten")
		if started:
			self.layout.prop(scn.my_props, "Percentage")
			self.layout.prop(scn.my_props, "CancelAxis")
			if scn.my_props.Percentage != mainop.oldperc:
				mainop.do_straighten()
				mainop.oldperc = scn.my_props.Percentage


classes = (
	MyProps,
	MESH_OT_StraightenPlus,
	PANEL_PT_StraightenPlusPanel,
)

def register():
	from bpy.utils import register_class
	for cls in classes:
		register_class(cls)
	bpy.types.Scene.my_props = bpy.props.PointerProperty(type=MyProps)


def unregister():
	from bpy.utils import unregister_class
	for cls in classes:
		unregister_class(cls)


if __name__ == "__main__":
	register()





				
