module Ene_3dRotate

menu = UI.menu("Tools")
menu.add_item("3D Rotate") { Sketchup.active_model.select_tool(Tool.new) }

tb = UI::Toolbar.new "3d Rotate"
cmd = UI::Command.new("3d Rotate") { Sketchup.active_model.select_tool(Tool.new) }
cmd.large_icon = "tool.png"
cmd.small_icon = "tool_small.png"
cmd.tooltip = "3d Rotate"
cmd.status_bar_text = "Rotate entities freely around point."
tb.add_item cmd

#Load workaround for missing API call to split edges and faces
require File.join(PLUGIN_ROOT, "ene_3dRotate/edge_breaking_after_transform.rb")

class Tool

	def initialize
		#Declare vars
		@pAnchor	#Origin for rotation
		@pStart1	#Grab selection here...
		@pTarget1	#...and drag towards this point (yaw, pitch)
		@pStart2	#Optionally grab once again here...
		@pTarget2	#...and drag towards this point (roll)
    
		@cursor = UI.create_cursor File.join(PLUGIN_ROOT, "ene_3dRotate/cursor.png"), 16, 14

	end#def
	
	def activate
		#Tool is selected
		
		#Initialize input points
		@pAnchor = Sketchup::InputPoint.new
		@pStart1 = Sketchup::InputPoint.new
		@pTarget1 = Sketchup::InputPoint.new
		@pStart2 = Sketchup::InputPoint.new
		@pTarget2 = Sketchup::InputPoint.new
		@ip = Sketchup::InputPoint.new
		
		@ph = Sketchup.active_model.active_view.pick_helper
		
		#If nothing is selected from the start, what's hovered on the first click becomes the selection
		@hasSelection = (Sketchup.active_model.selection.length != 0)
		
		#Needs edge breaking, changed to true when first rotation is performed. Not reset when tool is since edge breaking is needed until tool is exit
		@needs_e_b = false
		
		#Reset state counter
		self.reset(nil)
    
	end#def
	
	def onSetCursor
		UI.set_cursor @cursor
	end#def
	
	def deactivate(view)
		if(@needs_e_b)
			#Break edges on endpoints, and on other edges if Sketchup.break_edges? == true
			#Same as for several native tools
			
			#No API call for this :'(
			#Use own function (external file)
			
			Sketchup.active_model.start_operation("3D Rotate")
			
			ent = Sketchup.active_model.active_entities
			ent_array = []
			ent.each { | i| ent_array << i }
			ent_rotated_array = []
			Sketchup.active_model.selection.each { | i| ent_rotated_array << i }
			
			#ent_array-= ent_rotated_array
			
			Ene_3dRotate::break_edge_on_vertices ent_rotated_array, ent_array, Sketchup.break_edges?
			
			Sketchup.active_model.commit_operation
		end#if
		
		#Update screen on deactivation if rotation was performed
		view.invalidate if(@rotated)
	end#def
	
	def reset(view)
		#Clear input points
		@pAnchor.clear
		@pStart1.clear
		@pTarget1.clear
		@pStart2.clear
		@pTarget2.clear
		
		#Point currently being selected
		@state = 0
		
		#Status text for first input point
		#Status text is set as a class variable so it can be used in resume()
		@status_text = @hasSelection ? "Pick rotation origin." : "Pick entity and rotation origin."
		Sketchup::set_status_text(@status_text, SB_PROMPT)
		
		#Update view if necessary
		view.invalidate if(@rotated and view)
		
		#Set rotated flag to false
		@rotated = false
	end#def
	
	def onCancel(flag, view)
		#Reset tool when pressing ESC or reselecting it
		self.reset(view)
	end
	
	def resume(view)
		#Reset status text after tool has been temporarily deactivated
		Sketchup::set_status_text(@status_text, SB_PROMPT)
	end#def
	
	def draw(view)
		#Updates the view when necessary. Outlines input point and adds temporary guidelines
		#Achorpoint
		@pAnchor.draw(view) if(@pAnchor.valid? and@pAnchor.display?)
		#Start1 and line to anchorpoint
		if(@pStart1.valid?)
			#Draw point
			@pStart1.draw(view) if(@pStart1.display?)
			#Draw temporary line (anchor - start)
			view.line_stipple = "-"
			view.set_color_from_line @pAnchor.position, @pStart1.position
			view.draw_line(@pAnchor.position, @pStart1.position)
		end
		#target1 and line to anchorpoint and start1
		if(@pTarget1.valid?)
			#Draw point
			@pTarget1.draw(view) if(@pTarget1.display?)
			#Draw temporary line (anchor - target)
			view.drawing_color = Sketchup::Color.new(128, 128, 128)
			view.line_stipple = "_"
			view.draw_line(@pAnchor.position, @pTarget1.position)
			#Draw temporary line (start - target)
			view.line_stipple = "."
			view.draw_line(@pStart1.position, @pTarget1.position)
		end
		#Define plane roll is rotated in
		if(@pAnchor.valid? and @pTarget1.valid?)
			rollPlane = [@pAnchor.position, (@pTarget1.position.- @pAnchor.position)]
		end
		#Start2 and lines to anchorpoint
		if(@pStart2.valid?)
			#Draw point
			@pStart2.draw(view) if(@pStart2.display?)
			#Draw temporary line (anchor - start put to plane)
			view.line_stipple = "-"
			view.set_color_from_line @pAnchor.position, @pStart1.position.project_to_plane(rollPlane)
			view.draw_line(@pAnchor.position, @pStart2.position.project_to_plane(rollPlane))
			#Draw temporary line (start put to plane - start)
			view.line_stipple = "."
			view.drawing_color = Sketchup::Color.new(128, 128, 128)
			view.draw_line(@pStart2.position.project_to_plane(rollPlane), @pStart2.position)
		end
		#Target2 and lines to anchorpoint & start2
		if(@pTarget2.valid?)
			#Draw point
			@pTarget2.draw(view) if(@pTarget2.display?)
			#Draw temporary line (anchor - target put to plane)
			view.line_stipple = "_"
			view.drawing_color = Sketchup::Color.new(128, 128, 128)
			view.draw_line(@pAnchor.position, @pTarget2.position.project_to_plane(rollPlane))
			#Draw temporary line (target put to plane - target)
			view.line_stipple = "."
			view.draw_line(@pTarget2.position.project_to_plane(rollPlane), @pTarget2.position)
			#Draw temporary line (start put to plane - target put to plane)
			view.line_stipple = "."
			view.draw_line(@pStart2.position.project_to_plane(rollPlane), @pTarget2.position.project_to_plane(rollPlane))
		end
	end#def
	
	def onMouseMove(flags, x, y, view)
		#Temporarily select what's hovered if northing's selected
		#Temporary selection stays when tool first clicks
		unless(@hasSelection)
			#Clear selection
			Sketchup.active_model.selection.clear
			
			#Select what's hovered
			@ph.do_pick(x, y)
			picked = @ph.best_picked
			if(@ip.degrees_of_freedom == 0 and @ip.vertex and picked.class != Sketchup::Group and picked.class != Sketchup::ComponentInstance)
				#when hovering endpoints in this drawing context, select all edges it's binding just like native rotate tool
				picked = @ip.vertex.edges
			end#if
			Sketchup.active_model.selection.add picked if(picked)
			
		end#if
	
		#Find points when hovering
		if(@state == 0)
			#Anchorpoint
			
			#NOTE: possible awesome feature:
			#when pressing shift while selecting anchor point:
			#	rotate Yawpicth using center of gravity as start1 and downwards as target1
			#	set state to 3 (skipping manually selecting start1 and target1)
			#Statusbar saying "Shift = Hang from point."(?)
			
			@ip.pick view, x, y
			if( @ip != @pAnchor )
				#Input point has moved
				#Update view
				view.invalidate
				#Set anchorpoint to current hovered point
				@pAnchor.copy! @ip
			end#if
			
		elsif(@state == 1)
			#start1 point
			
			@ip.pick view, x, y, @pAnchor
			if( @ip != @pStart1 )
				#Input point has moved
				#Update view
				view.invalidate
				 #Set start1 point to current hovered point
				@pStart1.copy! @ip
			end#if
		
		elsif(@state == 2)
			#target1 point
			
			@ip.pick view, x, y, @pStart1
			if( @ip != @pTarget1 )
				#Input point has moved
				
				#Snap to radius and edge/c-line
				#(please please please, add this to the native Sketchup rotate tool)
				#Flag that changes tooltip
				smartEdgeSnap = false
				if(@ip.degrees_of_freedom == 1)
					#Point is on edge or c-line
					
					#Find the points on line at the same distance from anchor as start1 is
					#intersect line with a sphere with anchorPoint as center and length from anchor to start1 as radius
					
					if(@ip.edge)
						#Line from edge
						line = @ip.edge.line
						line.each{|i| i.transform!(@ip.transformation)}
					else
						#Line from c-line (I wish there was a c-line property for input points)
						@ph.do_pick(x, y)
						#NOTE: Returns entities in groups/components but not parent drawing context (and not inside groups in parent)
						picked = @ph.leaf_at(0)
						if(picked.class == Sketchup::ConstructionLine)
							line = [picked.position, picked.direction]
							line.each{|i| i.transform!(@ph.transformation_at(0))}
						end#if
					end#if
					if(line)
						center = @pAnchor.position
						radius = @pStart1.position.distance(@pAnchor.position)
						intersections = self.intersectLineSphere(line, center, radius)
						if(intersections)
							#Intersections found
							intersections.each do |i|
								iOnScreen = view.screen_coords i
								if(iOnScreen.x > x-10 and iOnScreen.x < x+10 and iOnScreen.y > y-10 and iOnScreen.y < y+10)
									#Mouse is within 10px from point, snap to it
									#Set smart edge snap flag to change tooltip
									smartEdgeSnap = true
									#Move input point to intersection
									@ip = Sketchup::InputPoint.new(i)
									@ip.pick view, x, y, @ip
								end#if
							end#each
						end#if				
					end#if				
				end
				
				#Update view
				view.invalidate
				#Set target1 point to current hovered point
				@pTarget1.copy! @ip
			end#if
		
		elsif(@state == 3)
			#start2 point (optional)
			
			@ip.pick view, x, y, @pAnchor
			if( @ip != @pStart2 )
				#Input point has moved
				#Update view
				view.invalidate
				#Set start2 point to current hovered point
				@pStart2.copy! @ip
			end#if
		
		elsif(@state == 4)
			#target2 point
			
			@ip.pick view, x, y, @pStart2
			if( @ip != @pTarget2 )
				#Input point has moved
				
				#Snap to radius and edge
				#(please please please, add this to the native Sketchup rotate tool)
				#Flag that changes tooltip
				smartEdgeSnap = false
				if(@ip.degrees_of_freedom == 1 and @ip.position.on_plane?([@pAnchor.position, @pTarget1.position.-(@pAnchor.position)]))
					#Point is on edge or c-line and in the plane of the rotation
					
					#Find the points on line at the same distance from anchor as start1 is
					#intersect line with a sphere with anchorPoint as center and length from anchor to start1 as radius
					
					if(@ip.edge)
						#Line from edge
						line = @ip.edge.line
						line.each{|i| i.transform!(@ip.transformation)}
					else
						#Line from c-line (I wish there was a c-line property for input points)
						@ph.do_pick(x, y)
						#NOTE: Returns entities in groups/components but not parent drawing context
						picked = @ph.leaf_at(0)
						if(picked.class == Sketchup::ConstructionLine)
							line = [picked.position, picked.direction]
							line.each{|i| i.transform!(@ph.transformation_at(0))}
						end#if
					end#if
					if(line)
						center = @pAnchor.position
						radius = @pStart2.position.distance(@pAnchor.position)
						
						intersections = self.intersectLineSphere(line, center, radius)
						
						if(intersections)
							intersections.each do |i|
								iOnScreen = view.screen_coords i
								if(iOnScreen.x > x-10 and iOnScreen.x < x+10 and iOnScreen.y > y-10 and iOnScreen.y < y+10)
									#Mouse is within 10px from point, snap to it
									#Set smart edge snap flag to change tooltip
									smartEdgeSnap = true
									#Move imput point to intersection
									@ip = Sketchup::InputPoint.new(i)
									@ip.pick view, x, y, @ip
								end#if
							end#each
						end#if
					end#if
				end
				
				#Update view
				view.invalidate
				#Set target1 point to current hovered point
				@pTarget2.copy! @ip
			end#if
		
		end#if elsif
		
		#Set point tooltip
		view.tooltip = smartEdgeSnap ? "From Radius" : @ip.tooltip
	end#def
	
	def onLButtonDown(flags, x, y, view)
		#Do nothing with this click if nothing is selected
		return nil if(Sketchup.active_model.selection.length == 0)
		
		#Keep what's currently selected.
		#Might be temporarily selected because its hovered if there was no selection when tool was selected
		@hasSelection = true
		
		#Select point when and draw when clicking
		#first 3 clicks select points and set jaw & pitch. 4th and 5th click are optional and sets roll
		if(@state == 0)
			#Select anchorpoint
			#@pAnchor.pick view, x, y
			if(@pAnchor.valid?)
				@state=1
				@status_text = "Grab entity."#first status text defined in reset()
				Sketchup::set_status_text(@status_text, SB_PROMPT)
			end#if
			
		elsif(@state == 1)
			#Select startpoint 1
			#@pStart1.pick view, x, y, @pAnchor
			if(@pStart1.valid?)
				@state=2
				@status_text = "Pick target to drag entity towards."
				Sketchup::set_status_text(@status_text, SB_PROMPT)
			end#if
			
		elsif(@state == 2)
			#Select targetpoint 1 and set yaw and pitch for selection
			#@pTarget1.pick view, x, y, @pAnchor
			if(@pTarget1.valid?)
				@state=3
				@status_text = "(Optional) Grab entity again."
				Sketchup::set_status_text(@status_text, SB_PROMPT)
				
				self.rotateYawPitch(@pAnchor.position, @pStart1.position, @pTarget1.position)
				
				#Set rotated flag so script knows whether to update view or not when leaving tool
				@rotated = true
			end#if
		elsif(@state == 3)
			#Select startpoint 2
			#@pStart2.pick view, x, y, @pAnchor
			if(@pStart1.valid?)
				@state=4
				@status_text = "Pick target to drag entity towards."
				Sketchup::set_status_text(@status_text, SB_PROMPT)
			end#if
		elsif(@state == 4)
			#Select targetpoint 2 and set roll for selection
			#@pTarget2.pick view, x, y, @pAnchor
			if(@pTarget2.valid?)
				
				self.rotateRoll(@pAnchor.position, @pStart2.position, @pTarget2.position,(@pTarget1.position).-(@pAnchor.position))
				
				self.reset(view)
			end#if
		end#if elsif
	end#def

	def flattenVector(plane, vector)
		#Flatten vector to plane
		#plane's point does not affect output, only its normal does
		
		pointStart = plane[0]
		normal = plane[1]
		
		#Point at end of vector
		pointVectorEnd = pointStart.offset vector
		#point projected to plane
		pointIntesection = pointVectorEnd.project_to_plane plane
		#vector from startpoint to intersection
		outputVector = pointIntesection.- pointStart
		return outputVector
	end#def

	def angle_between_in_Plane(plane, v1, v2)
		#Get angle between vectors in a given plane
		#Can return negative angles unlike built in angleBetween method
		
		#Flatten to plane
		v1 = self.flattenVector(plane,v1)
		v2 = self.flattenVector(plane,v2)
		
		#Get angle between
		a = v1.angle_between v2
		
		#Determine if angle is positive or negative (rotate v2 90 degrees cc and cw seen from above and see which is closest to v2)
		negAnglePointForward = Geom::Point3d.new.offset v2
		negAnglePointPos = negAnglePointForward.transform(Geom::Transformation.rotation(Geom::Point3d.new, plane[1], Math::PI/2))
		negAnglePointNeg = negAnglePointForward.transform(Geom::Transformation.rotation(Geom::Point3d.new, plane[1], -Math::PI/2))
		negAnglePointCheck = Geom::Point3d.new.offset v1
		a*= -1 if (negAnglePointCheck.distance negAnglePointNeg) < (negAnglePointCheck.distance negAnglePointPos)
		
		return a
	end#def

	def intersectLineSphere(line, center, radius)
		#Get intersection points between line and sphere
		
		lineStart = line[0]
		lineVector = line[1].normalize
		
		#Calculate distance from line's start along line to intersections
		
		firstTerm = -(lineVector.dot(lineStart.-(center)))
		secondTermSquared = (lineVector.dot(lineStart.-(center)))**2 - (lineStart.-(center)).dot(lineStart.-(center)) + radius**2
		
		#If root is less than 0 the line does not intersect the sphere and there's no point on the line to snap to
		return nil if(secondTermSquared < 0)
			
		secondTerm = Math.sqrt(secondTermSquared)
		
		sum1 = firstTerm+secondTerm
		sum2 = firstTerm-secondTerm
		
		#Find points
		lineVector.length = sum1
		point1 = lineStart.offset lineVector
		
		lineVector.length = sum2
		point2 = lineStart.offset lineVector
		
		#return points
		return [point1, point2]
	end#def
	
	def rotateYawPitch(pAnchor, pStart, pTarget)
		#Rotate geometry yaw & pitch
		
		#When tool is deselected edges needs to be broken
		@needs_e_b = true
		
		#Vector geometry is rotated around. Don't affect result if rotateRoll is called too
		vUp = Geom::Vector3d.new(0,0,1)
		
		#Create vectors from points
		vStart = pStart.- pAnchor
		vTarget = pTarget.- pAnchor
		
		#Create transformations
		
		#Yaw (only when not start nor target is directly above/under anchor)
		unless(vUp.parallel? vTarget or vUp.parallel? vStart)
			#z, upwards
			axis = vUp
			#angle between target and start in horizontal plane from anchorpoint
			angle = angle_between_in_Plane([Geom::Point3d.new,vUp], vTarget, vStart)
			yaw = Geom::Transformation.rotation pAnchor, axis, angle
		end
		
		#Pitch
		if(not vUp.parallel? vTarget)
			#Axis is perpendicular to vTarget and in horizontal plane unless target is directly above/below anchor
			axis = flattenVector([Geom::Point3d.new,vUp],vTarget).transform(Geom::Transformation.rotation(Geom::Point3d.new, vUp, Math::PI/2))
		elsif(not vUp.parallel? vStart)
			#Otherwise axis is perpendicular to vStart and in horizontal plane unless start too is directly above/below anchor
			axis = flattenVector([Geom::Point3d.new,vUp],vStart).transform(Geom::Transformation.rotation(Geom::Point3d.new, vUp, Math::PI/2))
		elsif(not vStart.samedirection? vTarget)
			#Otherwise, if both target and start is directly above/below anchor but on different sides,
			#just use a random axis in horizontal plane.
			#An additional roll will get rid of the randomness
			axis = Geom::Vector3d.new(1,0,0)
		else
			#If both points are on the same side of anchor and directly above/below it nothing needs to be done
			return nil
		end
		#pitch difference between vTarget and vStart
		angle = vUp.angle_between(vTarget) - vUp.angle_between(vStart)
		pitch = Geom::Transformation.rotation pAnchor, axis, angle
		
		#Combine translations
		trans = yaw ? (pitch.* yaw) : pitch
		
		Sketchup.active_model.start_operation("3D Rotate", true, true, false)
		#Rotate
		entsSelected = []
		Sketchup.active_model.selection.each{|i| entsSelected << i}
		Sketchup.active_model.entities.transform_entities trans, entsSelected		
		Sketchup.active_model.commit_operation
		
	end#def
	
	def rotateRoll(pAnchor, pStart, pTarget, axis)
	
		#Create vectors from points
		vStart = pStart.- pAnchor
		vTarget = pTarget.- pAnchor
		
		plane = [Geom::Point3d.new,axis]
		angle = angle_between_in_Plane(plane, flattenVector(plane, vTarget), flattenVector(plane, vStart))
		trans = Geom::Transformation.rotation pAnchor, axis, angle
		
		Sketchup.active_model.start_operation("3D Rotate", true, true, false)
		#Rotate
		entsSelected = []
		Sketchup.active_model.selection.each{|i| entsSelected << i}
		Sketchup.active_model.entities.transform_entities trans, entsSelected
		Sketchup.active_model.commit_operation
	
	end#def
	
end#class
end#module