=begin
###
Copyright 2011-2015 TIG (c)
Permission to use, copy, modify, and distribute this software for any 
purpose, and currently without fee, is hereby granted, provided that  
this text and the above copyright (c) notice appear in all copies.
This software is provided "as is" and without any express or implied 
warranties, including, without limitation, the implied warranties of 
merchantability and fitness for a particular purpose.
###
=end
###
module TIG
  module SolidSolver
	###
	unless file_loaded?(__FILE__)
		cmd=UI::Command.new(NAME){self.solidsolver()}
		UI.menu("Tools").add_item(cmd)
		UI.add_context_menu_handler{|menu| menu.add_item(cmd) if self.possible?() }	
		file_loaded(__FILE__)
	end
	###
	def self.possible?()
		@model=Sketchup.active_model
		@view=@model.active_view
		@ss=@model.selection
		if @ss.empty? || @ss[1] || ( ! @ss[0].is_a?(Sketchup::Group) && ! @ss[0].is_a?(Sketchup::ComponentInstance) )
			return nil
		else
			return true
		end
	end
	###
	def self.intersecting?()
		if @container.is_a?(Sketchup::Group)
			ents=@container.entities
		else ### component instance
			ents=@container.definition.entities
		end
		entsIN=ents.to_a
		tr = Geom::Transformation.new()
		@ns = []
		@ns = ents.intersect_with(true, tr, ents, tr, true, ents.grep(Sketchup::Face))
		if @ns[0]
			if ents.to_a.length == entsIN.length
				@ns=[]
				return false
			else ### there are new entities
				return true
			end
		else ### no new entities
			@ns=[]
			return false
		end
	end
	###
	def self.solidsolver()
		###
		unless self.possible?()
			UI.messagebox("Select ONE GROUP/INSTANCE that you want to become Solid,\nOR which appears Solid, but might intersect itself !")
		end
		@container=@ss[0]
		@model.start_operation(NAME)
		###
		@xray=@model.rendering_options["ModelTransparency"]
		@ss.clear
		@view.refresh
		@msgX="#{NAME}: ."
		Sketchup::set_status_text(@msgX)
		if @container.is_a?(Sketchup::Group)
			@entities=@container.entities
			name=@container.name
			uniq="Group is "
			if @entities.parent.instances[1]
				@container.make_unique
				@entities=@container.entities
				if name.empty?
					uniq="Group was made_unique,\nit is "
				else
					uniq="Group \""+name+"\" was made_unique as \""+@container.name+"\",\nit is "
				end
			end
		else ### component instance
			@entities=@container.definition.entities
			name=@container.definition.name
			uniq="Component-Instance is "
			if @entities.parent.instances[1]
				@container.make_unique
				@entities=@container.definition.entities
				uniq="Component-instance \""+name+"\" was made_unique as \""+@container.definition.name+"\"\nit is "
			end
		end
		@msg="#{NAME}:\n\nThe Selected "
		@msg<<uniq
		@cops=0
		@hole=0
		@small=0
		@parts=0
		@splits=0
		@revs=0
		###
		self.nested?()
		if @gps==0
			exploded=false
		elsif @gps==1
			UI.messagebox("#{NAME}:\n\nCannot continue with nested entities !")
			@model.abort_operation
			@ss.add(@container)
			return nil
		else ### @gps is a string
			exploded=true
		end
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		###
		if @container.manifold?
			@msgX<<"."
			Sketchup::set_status_text(@msgX)
			if self.intersecting?()
				UI.messagebox("This seemingly 'Solid' Selected GROUP/INSTANCE 'intersects' itself !")
			else
				if exploded
					UI.messagebox("The Selected GROUP/INSTANCE is now a 'Solid' !")
					@model.commit_operation
					@ss.add(@container)
					return nil
				else ### nothing was exploded
					UI.messagebox("The Selected GROUP/INSTANCE is already a 'Solid' !")
					@model.abort_operation
					@ss.add(@container)
					return nil
				end
				return nil
			end
		else ### carry on - it's not solid !
			###
		end
		###
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		###
		self.intersecting?() unless @ns && @ns[0]
		###
		if @ns && @ns[0]
			arr = @ns.grep(Sketchup::Edge).select{|e| e.valid? }.compact
			@ss.add( arr )
			@model.rendering_options["ModelTransparency"] = true
			UI.messagebox("#{NAME}:\n\nThe form has been 'intersected'.")
			@ss.clear
			@model.rendering_options["ModelTransparency"] = @xray
			@ns=[]
		end
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		self.heal_holes()
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		self.heal_small()
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		self.remove_partitions()
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		self.heal_splits()
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		@view.refresh
		@msgX="#{NAME}:"
		Sketchup::set_status_text(@msgX)
		self.get_coplanar()
		@msgX="#{NAME}:"
		Sketchup::set_status_text(@msgX)
		if @coplanar[0]
			are="are"
			are="is" unless @coplanar[1]
			edges="Edges"
			edges="Edge" unless @coplanar[1]
			them="them"
			them="it" unless @coplanar[1]
			@ss.add(@coplanar)
			UI.beep
			if UI.messagebox("#{NAME}:\n\nThere #{are} #{@coplanar.length} Coplanar #{edges}.\n\nRemove #{them} ?",MB_YESNO,"")==6 ### 6=YES 7=NO
				self.erase_coplanar()
				@cops+=@coplanar.length
			else
				@ss.remove(@coplanar)
			end
		end
		@msgX<<"."
		Sketchup::set_status_text(@msgX)
		###
		if @cops==0 && @hole==0 && @small==0 && @parts==0
			now=""
			still=""
		else
			now="now "
			still="still "
		end
		if @container.manifold?
			@msg<<now+"'Solid'.\n\n"
			isnot=false
		else
			@msg<<still+"NOT 'Solid' !\n\n"
			isnot=true
		end
		###
		ces="s"
		hes="s"
		ses="s"
		pes="s"
		res="s"
		ces="" if @cops==1
		hes="" if @hole==1
		ses="" if @small==1
		pes="" if @parts==1
		res="" if @revs==1
		@splits=0 if @splits==1
		@msg<<@gps
		@msg<<@cops.to_s+"\t Unfaced/Coplanar Edge"+ces+" Removed\n"
		@msg<<@hole.to_s+"\t Hole"+hes+" Healed\n"
		@msg<<@small.to_s+"\t Small Face"+ses+" Healed\n"
		@msg<<@parts.to_s+"\t Partition/Flap"+pes+" Removed\n"
		@msg<<@splits.to_s+"\t Split-Edges Merged\n"
		@msg<<@revs.to_s+"\t Face"+res+" Reoriented\n"
		UI.messagebox(@msg)
		if isnot
			edges1=[]
			edges2=[]
			@entities.grep(Sketchup::Edge).each{|e|
				edges1 << e if e.faces.length == 1
				edges2 << e if e.faces.length > 2
			}
			if edges2[0]
				@model.commit_operation
				UI.beep
				if UI.messagebox("#{NAME}:\n\nThe lack of 'Solidity' might be because\nsome internal-partitions remain or perhaps\nmore than two Faces share some Edges,\n[e.g. two otherwise 'solid' cubes might share an edge].\n\nDo you want to try and fix it?\n\n[It might cause unexpected results, but it will be undoable...]",MB_YESNO,"")==6 ### 6=YES 7=NO
					@ss.add(@container)
					self.solidsolver()
					return nil
				end
			elsif ! @entities[0]
				UI.messagebox("#{NAME}:\n\nThis Group/Instance can never be solved into a 'Solid'!\nTry manually adding/removing Edges/Faces...\n[No changes have been made]")
				@model.abort_operation
			elsif edges1[0]
				UI.messagebox("#{NAME}:\n\nThis Group/Instance can never be solved into a 'Solid'!\nTry manually adding/removing Edges/Faces...\n[Undo if appropriate]")
				@model.commit_operation
			else
				ents=[]
				@entities.each{|e|ents << e.class}
				if ents.include?(Sketchup::Group) || ents.include?(Sketchup::ComponentInstance)
					UI.messagebox("#{NAME}:\n\nThis Group/Instance still contains Groups/Instances!\nSo it can never be considered 'Solid'...")
				end
				@model.commit_operation
			end
		end
		@ss.add(@container)
	end
    ###
	def self.heal_splits()
		aedges=[]
		@entities.grep(Sketchup::Edge).each{|e| aedges << e if e.valid? }
		verts=[]
		aedges.each{|edge| verts << edge.vertices }
		verts.flatten!
		verts.uniq!
		vends=[]
		verts.each{|vx|
			edges=vx.edges
			next unless edges.length == 2
			e0=edges[0]
			e2=edges[1]
			ptx=vx.position
			v0=e0.other_vertex(vx)
			v2=e2.other_vertex(vx)
			pt0=v0.position
			pt2=v2.position
			next unless ptx.on_line?([pt0,pt2])
			e0=vx.edges[0]
			vec=pt0.vector_to(pt2)
			axes=nil
			axes=vec.axes if vec.valid?
			vends << [vx.to_s, ptx.to_a, axes]
		}
		vends.to_a.each{|ar|
			vx, pt, axes = ar
			next unless axes
			pte=Geom::Point3d.new(pt.x, pt.y, pt.z).offset(axes[0], 0.01)
			edg=@entities.add_line(pt,pte)
			edg.erase!
		}
		@splits=vends.length+1
	end
	###
    def self.nested?()
		@gps=0
		gps=[]
		@entities.each{|e| gps << e if e.is_a?(Sketchup::Group) || e.is_a?(Sketchup::ComponentInstance) }
		if gps.length == 1
			UI.beep
			if UI.messagebox("#{NAME}:\n\nThis Group/Instance contains 1 nested Group/Instance.\n\nDo you want to Explode it ?\n\n[This Group/Instance with never be 'Solid' otherwise...]",MB_YESNO,"")==6 ### 6=YES 7=NO
				num=0
				gone=false
				until gone
					@entities.to_a.each{|e|
						if e.is_a?(Sketchup::Group) || e.is_a?(Sketchup::ComponentInstance)
							e.explode
							num+=1
						end
					}
					ents=[]
					@entities.each{|e|ents << e.class }
					gone = true unless ents.include?(Sketchup::Group) || ents.include?(Sketchup::ComponentInstance)
			    end
				if num == 1
					@gps = num.to_s+"\t Nested Group/Instance Exploded\n"
				else
					@gps = num.to_s+"\t Nested Groups/Instances Exploded\n"
				end
			else
				@gps=1
			end
		elsif gps.length > 1
			UI.beep
			if UI.messagebox("#{NAME}:\n\nThis Group/Instance contains #{gps.length} nested Groups/Instances.\n\nDo you want to Explode them ?\n\n[This Group/Instance with never be 'Solid' otherwise...]", MB_YESNO,"")==6 ### 6=YES 7=NO
				num=0
				gone=false
				until gone
					@entities.to_a.each{|e|
						if e.class==Sketchup::Group || e.class==Sketchup::ComponentInstance
							e.explode
							num+=1
						end
					}
					ents=[]
					@entities.each{|e|ents << e.class}
					gone = true unless ents.include?(Sketchup::Group) || ents.include?(Sketchup::ComponentInstance)
				end
				@gps = num.to_s+"\t Nested Groups/Instances Exploded\n"
			else
				@gps=1
			end
		else
			@gps=0
		end
    end
	###
	def self.get_coplanar()
		@faceless=[]
		@flapped=[]
		@coplanar=[]
		@entities.grep(Sketchup::Edge).each{|e|
			next unless e.valid?
			if ! e.faces[0]
				@faceless << e
			elsif e.faces.length == 1
				@flapped << e.faces[0]
			elsif e.faces.length == 2 && e.faces[0].normal.dot(e.faces[1].normal) > 0.99999999 && e.faces[0].material==e.faces[1].material && e.faces[0].back_material==e.faces[1].back_material
				@coplanar << e
			end
		}
		if @faceless[0]
			@entities.erase_entities(@faceless)
			@cops+=@faceless.length
		end
		if @flapped[0]
			@entities.erase_entities(@flapped)
			@parts+=@flapped.length
			self.get_coplanar()
		end
	end
	###
	def self.erase_coplanar()
		@entities.erase_entities(@coplanar)
	end
	###
	def self.heal_holes(first=true)
		return if @container.manifold?
		edges = @entities.grep(Sketchup::Edge).select{|e| e.faces.length == 1 }
		(edges.length).times{|i|
		  edges = @entities.grep(Sketchup::Edge).select{|e| e.faces.length == 1 }
		  break unless edges[0]
		  filled=0
		  edges.each{|e|
			next unless e.valid?
			if e.faces.length == 1
				###
				edgs = []
				e.vertices.each{|v| edgs << v.edges }
				edgs.flatten!
				edgs.uniq!
				edgs.clone.each{|ee| edgs.delete(ee) if ee.faces[1] }
				###
				if ex = edgs[1]
					facesIN = @entities.grep(Sketchup::Face)
					ex.find_faces
					ctr=(@entities.grep(Sketchup::Face)-facesIN).length
					filled+=ctr
					if ctr == 0
						e.erase!
						@parts+=1
					end
				else ### a lone edge
					e.erase!
					@parts+=1
				end
			end
		  }
		  @hole+=filled if first
		  break if @container.manifold?
		}
	end
	###
	def self.heal_small()
	    @entities.grep(Sketchup::Edge).each{|e|
			next unless e.valid?
			if e.length <=0.25.mm
				vs=e.start
				ve=e.end
				ps=vs.position
				pe=ve.position
				vec=pe.vector_to(ps)
				@entities.transform_by_vectors([ve], [vec])
				pp=vs.position.transform(@container.transformation)
				tgp=@entities.add_group()
				li=tgp.entities.add_line(pp, pp.offset(Z_AXIS))
				tgp.entities.transform_by_vectors([li.end], [Z_AXIS.reverse])
				tgp.explode
				@small+=1
			end
        }
	end
	###
    def self.remove_partitions()
		###
		return if @container.manifold?
		tc = @container.transformation
		###
		shell = Shell.new(@container, @entities, tc)
		shell.process()
		###
		togos = shell.faces_int
		@entities.erase_entities(togos) if togos[0]
		@parts += togos.length
		###
		shell.faces_rev.each{|f| (f.reverse!; @revs+=1) if f.valid? }
		###
		self.get_coplanar()
		###
    end#def
    ###
	 # Based on Shellify by Anders Lyhagen && some later TT's recoding
	 # methods added/adjusted by TIG
  class Shell
    ###
    PI2 = Math::PI * 2
	###
    attr_reader :faces_int, :faces_rev
    ###
    def initialize(cont, ents, tc) ### contner, entities, transformation
	  @model = Sketchup.active_model
	  @cont = cont
      @ents = ents
	  @tc = tc
      @faces_shell = []
      @faces_int = []
      @faces_rev = []
    end
    ###
    def process()
      @faces_int = []
      @faces_rev = []
      @faces_shell = []
	  arry = find_con(@ents)
	  arry.each{|arr|
        face = find_seed_face(@cont, arr, @tc)
        next unless face
        @faces_shell << find_shell(face)
      }
	  @faces_shell.flatten!
	  @faces_shell.uniq!
      @faces_int = @ents.grep(Sketchup::Face) - @faces_shell
      return
    end
	###
    private
	###
    def find_con(es)
	  esa = es
      esa = es.to_a unless esa.is_a?(Array)
      arr = []
      until esa.empty?
        e = esa.pop
        next unless e.respond_to?(:all_connected)
        ccc = e.all_connected
        esa = esa - ccc
		arr << ccc
      end
      return arr
    end
    ###
    def face_normal(f)
      norm = f.normal
      norm.reverse! if @faces_rev.include?(f)
      return norm
    end
	###
    def edge_reversed_in?(e, f)
	  unless e.is_a?(Sketchup::Edge) && f.is_a?(Sketchup::Face)
		return false
	  end
      reversed = e.reversed_in?(f)
      reversed = ! reversed if @faces_rev.include?(f)
      return reversed
    end
	###
    def reverse_face(f)
      if @faces_rev.include?(f)
        @faces_rev.delete(f)
      else
        @faces_rev << f
      end
      return f
    end
	###
    # Find a start face in the connected shell: use ray test to find an external face
    ###
    def find_seed_face(cont, ents, tc)
	  face = nil
      faces = ents.grep(Sketchup::Face)
	  faces.each{|f|
		ray = @model.raytest([f.bounds.center.transform(tc), f.normal.transform(tc)])
		if ! ray
			face = f
			break
		elsif ! ray[1].include?(@cont)
			face = f
			break
		elsif ! ents.include?(ray[1][-1])
			face = f
			break
		end
	  }
	  faces.each{|f|
		ray = @model.raytest([f.bounds.center.transform(tc), f.normal.reverse.transform(tc)])
		if ! ray
			face = f
			f.reverse!
			break
		elsif ! ray[1].include?(@cont)
			face = f
			f.reverse!
			break
		elsif ! ents.include?(ray[1][-1])
			face = f
			break
		end
	  } unless face
	  face = faces[0] unless face
      return face
    end
	###
    # Construct a vector along the edge in the face's loop direction.
    def edge_vector(e, f)
      if edge_reversed_in?(e, f)
        e.end.position.vector_to(e.start)
      else
        e.start.position.vector_to(e.end)
      end
    end
    # The edges known to have two faces, return the face that is not the argument.
	# Reverse the other face (of) if appropriate.
    def get_other_face(e, f)
      of = e.faces.find{|ef| ef != f }
      reverse_face(of) if edge_reversed_in?(e, f) == edge_reversed_in?(e, of)
      return of
    end
    # Given a face known to be in the shell and one of its edges, find the other
    # shell face connected to the edge.
    def find_other_shell_face(e, f)
      return nil if e.faces.size == 1
      return get_other_face(e, f) if e.faces.size == 2
      ev = edge_vector(e, f)
      norm = face_normal(f)
      prod = norm.cross(ev)
      revd = edge_reversed_in?(e, f)
      ang_min = PI2
      sf = nil
      e.faces.each{|of|
        next if of == f
        ofno = face_normal(of)
        if edge_reversed_in?(e, of) == revd
          ofno.reverse! if edge_reversed_in?(e, of) == revd
        end
        other = ev.cross(ofno)
        angle = prod.angle_between(other)
        if other.dot(norm) < 0
          angle = PI2 - angle
        end
        if angle < ang_min
          ang_min = angle
          sf = of
        end
      }
      if edge_reversed_in?(e, sf) == revd
        reverse_face(sf)
      end
      return sf
    end
    # Traverses the connected mesh for the given start face and processs
    # faces representing the outer shell of the mesh.
    def find_shell(f)
	  return unless f.is_a?(Sketchup::Face)
      stack = [] # Unproccessed shell faces.
      procc = [] # Proccessed shell faces.
      shell = [] # Shell faces
	  ###
      stack << f # Set up initial stack.
      procc << f
	  ###
      until stack.empty? do
        ff = stack.pop
		next unless ff.is_a?(Sketchup::Face)
        shell << ff
        # Look for neighboring shell faces.
        ff.loops.each{|loop|
          loop.edges.each{|e|
            next if procc.include?(e) || e.faces.length < 2
            procc << e
            osf = find_other_shell_face(e, ff)
            next if procc.include?(osf)
            stack << osf
            procc << osf
          }
        }
      end
	  ###
      return shell
    end
	
  end # class Shell
  
  end#module
end#module
