# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#   Recurve plugin for Google SketchUp
# Created by Diggory Blake on 22 April 2011
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

require 'sketchup.rb'

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Change this to true to move Select Only into Recurve:
RECURVE_ALL_UNDER_RECURVE_MENU = false
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

module Recurve
	def Recurve.can_select_curve?(ents)
		return false unless ents.length >= 1
		return false unless ents[0].is_a?(Sketchup::Edge)
		
		if ents[0].curve then
			ents.each do |ent|
				return false unless ent.is_a?(Sketchup::Edge)
				return false unless ent.curve.equal?(ents[0].curve)
			end
			
			return true
		else
			ent = ents[0]
			
			return (ent.is_a?(Sketchup::Edge) and (ents.length == 1))
		end
	end
	
	def Recurve.can_select_curve_selection?()
		Sketchup.active_model and can_select_curve?(Sketchup.active_model.selection)
	end
	
	def Recurve.select_curve(ents, planar)
		return unless can_select_curve?(ents)
		
		edges = Set.new
		
		points = []
		plane = nil
		
		findadjacent = lambda do |vertex, vec|
			loop do
				foundedge = false
				vertex.edges.each do |edge|
					next if edges.contains?(edge) or edge.soft?
					
					othervertex = edge.other_vertex(vertex)
					
					if planar and plane then
						next unless othervertex.position.on_plane? plane
					end
					
					dirvec = othervertex.position - vertex.position
					next unless vec.angle_between(dirvec) <= 1.0
					
					edges.insert edge
					
					if planar and !plane then
						points << othervertex.position
						if points.length == 3 then
							vec0 = points[1]-points[0]
							vec1 = points[2]-points[1]
							if vec0.parallel? vec1 then
								points.pop
							else
								plane = Geom.fit_plane_to_points(points)
							end
						end
					end
					
					vertex = othervertex
					vec = dirvec
					foundedge = true

					break
				end
				
				break unless foundedge
			end
		end
		
		vertex0 = ents[0].vertices[0]
		vertex1 = ents[0].vertices[1]
		points << vertex0.position
		points << vertex1.position
		edges.insert ents[0]
		findadjacent.call(vertex0, vertex0.position - vertex1.position)
		findadjacent.call(vertex1, vertex1.position - vertex0.position)
		
		Sketchup.active_model.selection.clear
		Sketchup.active_model.selection.add edges.to_a()
		
		return edges
    end
	
	def Recurve.select_curve_selection(planar)
		select_curve Sketchup.active_model.selection, planar
	end

	def Recurve.can_recurve?(ents, quick = false)
		return false unless ents.length > 1
		return true if quick and ents.length > 50
		
		ents.each do |ent|
			return false unless ent.is_a? Sketchup::Edge and ent.valid?
		end
		
		return true
	end
	
	def Recurve.recurve(ents, reversedirection = 0)
		return unless can_recurve?(ents)
		
		ents.each { |ent| ent.explode_curve if ent.curve }
		edges = []
		
		curvepoints = [[]]
		
		curveparts = []
		findadjacent = lambda do |vertex, direction, pointarray|
			loop do
				if direction == 0 then
					pointarray.push(vertex.position)
				else
					pointarray.unshift(vertex.position)
				end
				
				hardedges = 0
				vertex.edges.each do |edge|
					next if edge.soft?
					hardedges += 1
				end
				
				foundedge = false
				vertex.edges.each do |edge|
					next if edges.index(edge) or edge.soft?
					
					if ents.is_a? Array then
						next unless ents.index(edge)
					else
						next unless ents.contains?(edge)
					end
					
					othervertex = edge.other_vertex(vertex)
					
					if hardedges <= 2 then
						edges << edge
						
						vertex = othervertex
						foundedge = true
					else
						curveparts << [vertex, othervertex, edge]
					end
					
					break
				end
				
				break unless foundedge
			end
		end
		
		vertex0 = ents[0].vertices[0]
		vertex1 = ents[0].vertices[1]
		edges << ents[0]
		findadjacent.call(vertex0, reversedirection, curvepoints[0])
		findadjacent.call(vertex1, 1-reversedirection, curvepoints[0])

		curveparts.each do |curvepart|
			vertex = curvepart[0]
			othervertex = curvepart[1]
			edge = curvepart[2]
			
			next if edges.index(edge)
			edges << edge
			
			newpointarray = [vertex.position]
			findadjacent.call(othervertex, 0, newpointarray)
			curvepoints << newpointarray if newpointarray.length >= 3
		end
		
		curvecount = curvepoints.length
		
		faces = []
		curves = []
		removableedges = []
		edges.each do |edge|
			edge.vertices.each do |vert|
				vert.faces.each do |face|
					next if faces.index(face)
					
					faces << face
					face.edges.each do |edge2|
						next unless edge2.curve and !edge2.soft? and !curves.index(edge2.curve)
						
						curves << edge2.curve
						points = []
						edge2.curve.vertices.each do |vertex|
							points << vertex.position
						end
						curvepoints << points
						
						edge2.curve.edges.each do |edge3|
							next if edges.index(edge3)
							
							edges << edge3
						end
					end
				end
			end
			removableedges << edge if edge.faces.length == 0
		end
		
		group = Sketchup.active_model.active_entities.add_group(faces + edges)
		
		#Sketchup.active_model.active_entities.erase_entities(removableedges)
		
		curveedges = []
		curvepoints.each do |points|
			curveedges << Sketchup.active_model.active_entities.add_curve(points)
		end
		curves = []
		(0...curvecount).each { |index| curves << curveedges[index][0].curve }
		
		if group then
			group.explode
		end
		
		Sketchup.active_model.selection.clear
		
		result = []
		curves.each { |curve| result += curve.edges if curve and curve.valid? }
		
		Sketchup.active_model.selection.add result
		
		return result
	end
	
	def Recurve.can_recurve_selection?()
		Sketchup.active_model and can_recurve?(Sketchup.active_model.selection, true)
	end
	
	def Recurve.recurve_selection()
		Sketchup.active_model.start_operation "Recurve Edges"
		
		recurve Sketchup.active_model.selection
		
		Sketchup.active_model.commit_operation
	end
	
	def Recurve.select_only(enttype, &filter)
		keeps = []
		Sketchup.active_model.selection.each do |ent|
			if ent.is_a? enttype then
				keeps << ent if !filter or filter.call(ent)
			end
		end
		
		Sketchup.active_model.selection.clear
		Sketchup.active_model.selection.add(keeps)
	end
	
	def Recurve.reverse_curve(ents)
		return unless can_reverse_curve?(ents)
		
		curves = Set.new
		
		ents.each do |ent|
			curves.insert(ent.curve) if ent.is_a? Sketchup::Edge and ent.curve and !curves.contains?(ent.curve)
		end
		
		result = Sketchup.active_model.selection.to_a
		curves.each do |curve|
			next unless curve.valid?
		
			edges = curve.edges
			result -= edges
			
			direction = edges[1].vertices.index(edges[0].vertices[0]) ? 1 : 0
			
			edges = recurve(edges, direction)
			
			if edges and edges.is_a? Array then
				edges.each do |edge|
					result << edge if edge and edge.valid?
				end
			end
		end
		
		Sketchup.active_model.selection.clear
		
		result.each do |edge|
			Sketchup.active_model.selection.add edge if edge and edge.valid?
		end
		
		return result
	end
	
	def Recurve.reverse_curve_selection()
		Sketchup.active_model.start_operation "Reverse Curve"
		
		reverse_curve Sketchup.active_model.selection
		
		Sketchup.active_model.commit_operation
	end
	
	def Recurve.can_reverse_curve?(ents, quick = false)
		return true if quick and ents.length > 50
		ents.each do |ent|
			return true if ent.is_a? Sketchup::Edge and ent.curve
		end
		
		return false
	end

	def Recurve.can_reverse_curve_selection?()
		Sketchup.active_model and can_reverse_curve?(Sketchup.active_model.selection, true)
	end

end

# Add menu items
if( not file_loaded? "recurve.rb" )

	# Add a choice to the context menu
	UI.add_context_menu_handler do |menu|
		menu.add_separator
		
		if RECURVE_ALL_UNDER_RECURVE_MENU then
			recurveMenu = menu.add_submenu("Recurve")
			selectOnlyMenu = recurveMenu.add_submenu("Select Only")
		else
			selectOnlyMenu = menu.add_submenu("Select Only")
		end
		
		selectOnlyMenu.add_item("Edges") { Recurve::select_only(Sketchup::Edge) }
		selectOnlyMenu.add_item("Hard Edges") { Recurve::select_only(Sketchup::Edge) { |edge| !edge.soft? } }
		selectOnlyMenu.add_item("Soft Edges") { Recurve::select_only(Sketchup::Edge) { |edge| edge.soft? } }
		selectOnlyMenu.add_item("Straight Edges") { Recurve::select_only(Sketchup::Edge) { |edge| !edge.curve } }
		selectOnlyMenu.add_item("Curved Edges") { Recurve::select_only(Sketchup::Edge) { |edge| edge.curve } }
		selectOnlyMenu.add_separator
		selectOnlyMenu.add_item("Faces") { Recurve::select_only(Sketchup::Face) }
		selectOnlyMenu.add_item("Convex Faces") { Recurve::select_only(Sketchup::Face) { |face| face.outer_loop.convex? } }
		selectOnlyMenu.add_item("Concave Faces") { Recurve::select_only(Sketchup::Face) { |face| !face.outer_loop.convex? } }
		selectOnlyMenu.add_item("Simple Faces") { Recurve::select_only(Sketchup::Face) { |face| face.loops.length == 1 } }
		selectOnlyMenu.add_item("Complex Faces") { Recurve::select_only(Sketchup::Face) { |face| face.loops.length != 1 } }
		selectOnlyMenu.add_separator
		selectOnlyMenu.add_item("Groups") { Recurve::select_only(Sketchup::Group) }
		
		valid1 = Recurve::can_recurve_selection?
		valid2 = Recurve::can_select_curve_selection?
		valid3 = Recurve::can_reverse_curve_selection?
		
		if (valid1 or valid2 or valid3) then
			if !RECURVE_ALL_UNDER_RECURVE_MENU then
				recurveMenu = menu.add_submenu("Recurve")
			end
			
			if valid1
				recurveMenu.add_item("Recurve Edges") { Recurve::recurve_selection }
			end
			
			if valid2
				recurveMenu.add_item("Select Curve") { Recurve::select_curve_selection false }
				recurveMenu.add_item("Select Curve (Planar)") { Recurve::select_curve_selection true }
				recurveMenu.add_item("Select Curve and Recurve") {
					Recurve::select_curve_selection false
					Recurve::recurve_selection
				}
				recurveMenu.add_item("Select Curve and Recurve (Planar)") {
					Recurve::select_curve_selection true
					Recurve::recurve_selection
				}
			end
			
			if valid3
				recurveMenu.add_item("Reverse Curve") { Recurve::reverse_curve_selection }
			end
		end
	end

	file_loaded("recurve.rb")
end

