=begin
(c) TIG 2009-2013
***in parts***
 
  obj_importer.rb
  
Note: Based heavily on Jim Foltz's 'obj_import.rb'* AND 
with Whaat's UVW texture mapping ideas... Many thanks !

BUT it's now with added materials with opacity/texture, also units 
setting and YZ axes swap dialogs.

On running it a dialog asks you to select units [pick from list] the 
file uses any 'units' in the .obj's header as the default value, 
if not then default=Inches.

The vt info in the .obj file is used for UVW texture mapping.
There's a warning in the console if .obj's .mtl or any texture files 
are missing.
If the .mtl is missing or incomplete the #default material# is used.
If a texture file is missing the material's colour/opacity is used.

The zoom goes to extents at the end.

A closing dialog asks if you want to swap the YZ axes - Yes/No reply -  
some .obj files are created flipped like that... so if the import 
appears flipped over Yes will right it...
The 'swap YZ axes' has a one step undo.
The imported materials and geometry in the OBJ groups is only undoable 
in multi-steps - instead simply erase the OBJ imported groups
and delete any new materials...

*** IT CAN TAKE A _L_O_N_G_ TIME TO LOAD A BIG OBJ FILE 
    WITH PERHAPS MANY THOUSANDS OF FACES ***
    [perhaps >> 1 minute per Mb ?]

Sometimes very complex OBJ shapes don't receive their textures at all.
Some OBJ files have tiny numbers: then faces might not be created: 
if so, retry with larger 'units', you can scale group(s) down afterwards.
Some OBJ files have tiny face planar discrepencies: then some faces 
might get triangulated to compensate: if so, retry with smaller 'units' 
and scale group(s) up afterwards.
Some complex files might need a bit of manual healing: they can also 
have erroneous vertices - so it's recommeneded you use 'Fix Problems' 
accessed from the 'Model Info > Statistics' dialog - 
especially if the result takes a long time to process or 
have divided faces forced to form geometry or if it feels jittery 
when orbiting around the model etc...
Some complex files with excessive smoothing etc might fail to import.

View textured results in 'Monochrome' to see any reversed faces...
  
Use:
1. Menu > Plugins > OBJ Importer > Import OBJ [with Textures]
2.                               > Import OBJ [no Materials]
3.                               > Import OBJ [as Mesh]

Option 1. Imports faces with mapped textures.
Option 2. Imports faces without materials [but materials added to model].
Option 3. Imports a mesh without materials [but materials added to model].
  
Open the Ruby Console before running to see additional reporting...

Version:
  1.0 20090714 First release.
  1.1 20090715 Existing Materials re-used/make variant option added.
               Swap Y & Z axes ? Yes/No, option added.
  1.2 20090723 Name changed to 'obj_importer.rb' / 'Import OBJ'
               Proper UV texture mapping added.
               Added dialog geometry & texture mapping Units.
  1.3 20090728 Submenu added with '..no Materials' and  '...as Mesh'
               options.  Material/usemtl duplicated entries trapped.
               Failure to import geometry - warning message added.
               Scaling errors corrected.  
               Model.start_action()...commit_action removed as it was
               causing errors on complex files.
  1.4 20100406 Different format obj file text now accepted.
  1.5 20100816 Missing group in obj trapped [with "g OBJ"].
  1.6 20100825 Negative face indices allowed.
  1.7 20100825 Fille from mesh changes.
  1.8 20100825 Trapped for spaces in image-names/paths.
  1.9 20110223 Errors with occasional reversed faces resolved.
  2.0 20130321 Blender-quads now import as two tri-facets.
			   The OBJ file must contain header-text "# Blender" or similar.
  2.1 20131118 Import of OBJ files using a -ve counter for f vertices within 
			   sub-groups fixed.  View now refreshes with each group completion.
###
=end
require "sketchup.rb"
module TIG_obj_importer### was JF
	def self.mtl_imp(path)### thanks mainly to Whaat
		model=Sketchup.active_model
        current_mat=nil
		new_mats=[]
		obj_dir=File.dirname(path).tr("\\","/")  #get the directory that this OBJ file is in
		materials=model.materials
        puts "#{obj_dir}/ - File MISSING." unless FileTest.exist?(path)
        lines=[]
		lines=IO.readlines(path) if FileTest.exist?(path)
        lines.each{|line|
			#line_cnt += 1
			next if line[0]==?#
			line.strip!
			values=line.split
			next if values.length==0
			cmd=values.shift  #removes the first element of the array and returns it
			case cmd
			when "newmtl"
				mat_name=values[0]
                matnames=[];materials.each{|mat|matnames << mat.display_name}
                if matnames.include?(mat_name)
                  puts "#{mat_name} - Material already exists..."
                  ### traps for usemtl called x2 in obj or pre-existing materials
                  current_mat=nil
                else
				  current_mat=materials.add(mat_name)
                  current_mat.color=[0.5,0.5,0.5]###defaults to beige !
				  new_mats << current_mat
                end#if
			when "Kd"
				r=values[0].to_f
				g=values[1].to_f
				b=values[2].to_f
				current_mat.color=[r,g,b] if current_mat
                #set the diffuse color of the material
			when "map_Kd"
				tex_path=File.join(obj_dir,values.join(" "))#<<<<<<<<<<<<
                if FileTest.exist?(tex_path.unpack("U*").map{|c|c.chr}.join)### allows accents
		          current_mat.texture=tex_path if current_mat
                   #set the material texture
                else
                   puts "#{tex_path} - Texture File MISSING."
                end#if
            when "d"###
				current_mat.alpha=values[0].to_f  if current_mat
                #set the material opacity
			end#case###
		}#each line
	return new_mats
	end#end mtl_import
###
###-------------------------------------------------------------------
###
  def self.run(use_mesh=false,no_materials=false)###
    model=Sketchup.active_model
	view=model.active_view
	begin
		model.start_operation("Import OBJ",true)
	rescue
		model.start_operation("Import OBJ")
	end
      ###
      @current_mat=nil
      @groups={}
      @vertices=[]
	  @uvs=[]
      @nos=[]
      @faces=[]
	  @mats=[]
      errors = Hash.new(0)
      face_verts = Hash.new(0)

      if use_mesh
	      mesh = Geom::PolygonMesh.new(1000000.0, 1000000.0)
      end

      obj = UI.openpanel("Select .obj file", "", "*.obj")###
      return unless obj
      puts "Processing '#{obj}'..."
      
      obj=obj.tr("\\","/")###
      
	  obj_dir=File.dirname(obj.unpack("U*").map{|c|c.chr}.join)
      mainGroupName = File.basename(obj.unpack("U*").map{|c|c.chr}.join)

      line_cnt = g_cnt = 0
		
      lines = IO.readlines(obj.unpack("U*").map{|c|c.chr}.join)
      
     ### dialog for units...
   units="Inches"
   ### change if header says 'units'...
   lines.each{|line|
    if line.downcase.include?("units")
      ustr=line.downcase.split("=").last.strip
      case ustr
        when "inches";units="Inches"
        when "feet";units="Feet"
        when "yards";units="Yards"
        when "miles";units="Miles"
        when "millimeters";units="Millimeters"
        when "centimeters";units="Centimeters"
        when "meters";units="Meters"
        when "kilometers";units="Kilometers"
        when "millimetres";units="Millimeters"
        when "centimetres";units="Centimeters"
        when "metres";units="Meters"
        when "kilometres";units="Kilometers"
      end#case
     end#if
   }#end each lines
   ###
   enums=["Inches|Feet|Yards|Miles|Millimeters|Centimeters|Meters|Kilometers"]
   prompts=["OBJ Units: "]
   values=[units]
   results=inputbox(prompts,values,enums,"OBJ Importer Units")
   return nil unless results
   units=results[0]
   scale=1.0
   case units
    when "Inches";scale=1.0
    when "Feet";scale=12.0
    when "Yards";scale=36.0
    when "Miles";scale=36.0*1760.0
    when "Millimeters";scale=1.0/25.4
    when "Centimeters";scale=1.0/2.54
    when "Meters";scale=1.0/0.0254
    when "Kilometers";scale=1000/0.0254
   end#case
   ### trap in case of a missing 'g'
   lines=["g OBJ\n"]+lines
   ###
   ### count indices
   blender=false
   vs=0; vts=0; vns=0;
   lines.each{|line|
	 line_cnt += 1
	 Sketchup.set_status_text("Processing line #{line_cnt} of #{lines.length}")
	 next if line[0]==?#
	 line.strip!
	 values = line.split
	 next if values.length==0
	 cmd=values.shift
	case cmd
	 when "mtllib"
		mtl_path=File.join(obj_dir, values.join(" ").unpack("U*").map{|c|c.chr}.join)
		@mats=self.mtl_imp(mtl_path)#import all the materials in the MTL file
	 when "usemtl"
		mat_name=values[0]
		@current_mat=@mats.find{|mat|mat.name==mat_name}
        @current_mat=nil if no_materials ###
	 when "v"
	    v=values.map{|e|Float(e)*scale }
	    @vertices << v
	    mesh.add_point(v)if use_mesh
		vs+=1
	when "vt" 
		uvw=values.map{|e|e.to_f }
		@uvs << uvw
		vts+=1
	when "vn" 
		nos=values.map{|e|e.to_f }
		@nos << nos
		vns+=1
	when "g"
	    g_cnt += 1
	    gname=values.join "_"
	    gname=mainGroupName if gname.nil?
	    if @groups[gname].nil?
	       @groups[gname]= Sketchup.active_model.entities.add_group
	       @groups[gname].name=gname
		   vs=0; vts=0; vns=0; ### restart for each subgroup
		   begin ### show progress
			view.refresh
		   rescue
			view.invalidate
		   end
	    end#if
	    @entities=@groups[gname].entities
	    if use_mesh
	       @entities.add_faces_from_mesh(mesh)
	       mesh=Geom::PolygonMesh.new
	    end#if
	 when "f"
	    face=[]; face_uvs=[]; face_nos=[]
  	    values.each{|v|
	       w = v.split("/")
           w0=nil; w1=nil; w2=nil ### "f 1/2/3 etc"
           w0= w[0].to_i if w[0]
           w1= w[1].to_i if w[1]
           w2= w[2].to_i if w[2]
           ### now trap for -ve values "f -1/-2/-3 etc"
           w0= vs+w0+1 if w0 && w0 < 0
           w1= vts+w1+1 if w1 && w1 < 0
           w2= vns+w2+1 if w2 && w2 < 0
           ###
	       face << w0 if w0
		   face_uvs << w1 if w1
		   face_nos << w2 if w2
	    }#each values
        verts = face.map{|v|@vertices[v-1]}
        face_uvs=face_uvs.collect{|index| @uvs[index-1]} if face_uvs && @uvs
        face_nos=face_nos.collect{|index| @nos[index-1]} if face_nos && @nos
        face_verts[verts.length] += 1
        @faces << face
	    verts_length=verts.length
		nfaces=[]
	    begin
	        if use_mesh
				mesh.add_polygon(verts)	
	        else
				new_face=@entities.add_face(verts)
				nfaces << new_face if new_face && blender
                if new_face
                    flipped=0
                    face_nos.each{|nos|flipped+=1 if new_face.normal != nos}
                    new_face.reverse! if flipped>1
                end#if
				if new_face && @current_mat
					if @current_mat.texture
						pt_array=[]
						pt_array=[verts[0],face_uvs[0],verts[1],face_uvs[1],verts[2],face_uvs[2]]if verts_length==3 && face_uvs
						pt_array=[verts[0],face_uvs[0],verts[1],face_uvs[1],verts[2],face_uvs[2],verts[3],face_uvs[3]]if verts_length>3 && face_uvs
						new_face.position_material(@current_mat,pt_array,true)
					else
						new_face.material=@current_mat
						###new_face.back_material=@current_mat ###
					end#if
				end#if
	       end#if
	    rescue
            puts 'Error: 666'
	        errors[$!.to_s] += 1
	        bb = Geom::BoundingBox.new
	        verts.map{|v|bb.add(v) if v} ###################
            verts.compact!###
	        c = bb.center
	        -1.upto(verts.length-2){|i|
		    begin
			  puts 'Error: 999'
		      new_face=@entities.add_face(verts[i], verts[i+1], c)
              new_face.material=@current_mat if new_face
			  nfaces << new_face if new_face && blender
              ###new_face.back_material=@current_mat if new_face ###
		    rescue
		      errors[$!.to_s] += 1
		    end#begin/rescue
	       }
	    end#begin/rescue
		if nfaces[1]
			eds=[]
			nfaces.each{|f| eds << f.edges }
			eds.flatten!
			eds.uniq!
			eds.each{|e|
				next unless e.valid?
				next unless e.faces.length==2
				e.erase! if e.faces[0].normal.dot(e.faces[1].normal)>0.99999999 && e.faces[0].material==e.faces[1].material
			}
		end
	 end#case
   }#each lines
   ###
    @entities.fill_from_mesh(mesh)if use_mesh ###############
      ### check something's made...
      enty=false;@groups.each{|g0,g1|enty=true if g1 && g1.valid?}
      unless enty ### error report
        UI.beep
        puts "Errors:\n"
        errors.each {|k, v| print "#{k}: #{v}\n"}
        face_verts.keys.sort.each {|k| print "ngons with #{k} verts: #{face_verts[k]}\n"}
        ###
        UI.messagebox("No Geometry was made !\nTry again using another Option or Units...")
        return nil
      end#if
      model.selection.clear
      Sketchup.send_action("viewZoomExtents:")
      UI.beep
    model.commit_operation
      if UI.messagebox("Flip OBJ YZ axes ?",MB_YESNO,"")==6 ### 6=YES 7=NO
	   begin
			model.start_operation("Swap OBJ YZ Axes",true)
	   rescue
			model.start_operation("Swap OBJ YZ Axes")
	   end
        tr=Geom::Transformation.rotation(ORIGIN,X_AXIS,90.degrees)
        @groups.each{|g0,g1|g1.transform!(tr)if g1 && g1.valid?}
        Sketchup.send_action("viewZoomExtents:")
       model.commit_operation
      end#if  
      Sketchup.send_action("selectSelectionTool:")
      return true      
   end#def

end#module------------------------

### Menu
unless file_loaded?(File.basename(__FILE__))
  menu = UI.menu("Plugins").add_submenu("OBJ Importer")
  menu.add_item("Import OBJ [with Textures]"){TIG_obj_importer::run(false,false)}
  menu.add_item("Import OBJ [no Materials]"){TIG_obj_importer::run(false,true)} 
  menu.add_item("Import OBJ [as Mesh]"){TIG_obj_importer::run(true,false)} 
end
file_loaded(File.basename(__FILE__))
###
