require 'sketchup.rb'
###########################################################
#
#    Copyright (C) 2012 Uli Tessel (utessel@gmx.de)
#
#    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 3 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, see <http://www.gnu.org/licenses/>.
#
###########################################################

module UTessel_GcodeExporter

  ### CONSTANTS ### ------------------------------------------------------------
  
  # Plugin information
  ID          = 'Path2GCode'.freeze
  VERSION     = '1.0.1'.freeze
  PLUGIN_NAME = 'Path to GCode Export'.freeze

  FILE_EXTENSION = ".ngc".freeze

  # Resource paths
  PATH_ROOT   = File.dirname( __FILE__ ).freeze
  
  ### MENU & TOOLBARS ### ------------------------------------------------------
 
  unless file_loaded?( File.basename(__FILE__) )

    cmd = UI::Command.new( PLUGIN_NAME ) { self.gcodeExport }    
    cmd.menu_text = "Export Path to G-Code..."
    cmd.tooltip = "Generate a G-Code file from paths in the current selection"
    # todo: Icon file?

    UI.menu("Plugins").add_item cmd
  end

  ### MAIN SCRIPT ### ----------------------------------------------------------
  
  def self.gcodeExport()

    proposal = File.basename( Sketchup.active_model.path )
    if proposal != ""
      proposal = proposal.split(".")[0]
      proposal += FILE_EXTENSION
    else
      proposal = "Untitled" + FILE_EXTENSION
    end

    filename = UI.savepanel( "G-Code", nil, proposal )

    return unless filename

    model=Sketchup.active_model
    what=model.selection

    # nothing selected? Use whole model
    if (what.count==0)
      what = model.entities
    end

    begin
      exporter = Path2GCodeExporter.new()
      exporter.dumpToFile( filename, what )
    rescue => bang
      print "Error: " + bang
    end
  end
  #--------------------------------------------------------------------------

class Path2GCodeExporter

  def dumpOneVertex( first, vert, trans )

  if (trans)
    pos = trans * vert.position
  else
    pos = vert.position
  end

  x = " X%2.3f" % pos.x.to_mm.to_f
  y = " Y%2.3f" % pos.y.to_mm.to_f
  z = " Z%2.3f" % pos.z.to_mm.to_f
  
  if (first)
    @file.write "G00" + x + y + "\n"
    @file.write "G00" + z + "\n"
  else
    @file.write "G01" 
    
    if x != @lastx
      @file.write x
    end
    if y != @lasty
      @file.write y
    end
    if z != @lastz
      @file.write z
    end
        
    @file.write "\n"
  end

  @lastx = x
  @lasty = y
  @lastz = z
end
#--------------------------------------------------------------------------

def dumpAttributes( entity )

   # not part of this file, but if you have "gcode" attributes in your model
   # then these will all be written to the file when we come to that object

   # todo: Might help if the key-name is also written (as comment?!)

   dictionary = entity.attribute_dictionary("gcode", false)    
   return if dictionary==nil
   dictionary.values.each{ |attrib| @file.write attrib+"\n" }
end
#--------------------------------------------------------------------------

def findNext( edge, edges )

  # typical case 1: end of path
  return nil if edges.length == 1

  # typical case 2: exactly two edges: don't use attributes
  if edges.length == 2 then
    nextedge = edges[0]
    if (nextedge == edge)
      nextedge = edges[1]
    end	         
    return nextedge
  end

  # other cases:
  current = edge.get_attribute("gcodehelp", "exit" )

  candidates = []

  for candidate in edges
    next if candidate == edge

    check = candidate.get_attribute("gcodehelp", "entry" )
    if check == current then
      candidates << candidate
    end

  end

  # just one candidate found? good, use that one 
  if candidates.length == 1 then
    return candidates[0]
  end

  @file.write "(problem: more than one ("+candidates.length.to_s+") way to walk through path)\n"      
  UI.messagebox( "problem: more than one way to walk through path" )      
  return nil
end

#--------------------------------------------------------------------------

def dumpVertex( vert, trans )

  @file.write "( path )\n"
  dumpOneVertex( true, vert, trans )
  
  firstZ = @lastz

  edge = vert.edges[0]
  while edge do
    
    dumpAttributes( edge )
  
    # look to other side:
    vert = edge.other_vertex vert

    # visit that point
    dumpOneVertex( false, vert, trans )
    
    # and continue there
    edge = findNext(edge, vert.edges)
  end
  
  @file.write "G00" + firstZ + "\n"
  
  # don't dump same path again from other side: 
  # final vert typically also has only one edge, so it is also in the list
  @vertices.delete vert   
end
#--------------------------------------------------------------------------

def combineTransformation( t1, t2 )

   if (t1)
    if (t2)
      return t1 * t2
    else
      return t1
    end
  else
    return t2
  end

end
#--------------------------------------------------------------------------

def collectVertex( v )
  # only collect vertex with one edge: End-Points
  if (v)
    if (v.edges.length == 1)
         @vertices << v
    end
  end
end

#--------------------------------------------------------------------------
def collectEdge( edge )
 
  collectVertex( edge.start )
  collectVertex( edge.end )

end

#--------------------------------------------------------------------------
def collectEdges( entity, trans )
  return if not entity.visible?
  collectEdge( entity ) if entity.is_a? Sketchup::Edge
end

#--------------------------------------------------------------------------
def dumpGroup( entity, etrans, trans, name )
  subtrans = combineTransformation( trans, etrans )

  @file.write "("+ name + ")\n"
  
  dumpAttributes( entity )
  
  @vertices = []
  entity.entities.each { |sub| collectEdges( sub, subtrans ) }

  # start with top most vertices
  @vertices.sort! { |a,b| b.position.z <=> a.position.z }  

  @vertices.each { |vert| dumpVertex( vert, subtrans ) }
  
  entity.entities.each { |sub| dumpEntity( sub, subtrans ) }
end

#--------------------------------------------------------------------------

def dumpEntity( entity, trans )

  return if not entity.visible?

  #----------- GROUP -----------------
  if entity.is_a? Sketchup::Group

    dumpGroup( entity, entity.transformation, trans, "Group %s" % entity.name.to_s );

  #----------- COMPONENT -----------------
  elsif entity.is_a? Sketchup::ComponentInstance
    dumpGroup( entity.definition, entity.transformation, trans, "Component %s_%s" % [entity.definition.name.to_s, entity.entityID.to_s] );

  end
end
#--------------------------------------------------------------------------

def entitySort( a, b )
  if a.is_a? Sketchup::Group
    if b.is_a? Sketchup::Group
      return a.name.to_s <=> b.name.to_s
    end  
  end
  
  return 0  
end

#--------------------------------------------------------------------------
def dumpToFile( filename, what )

  @file = File.new( filename, "w" )
  if not @file
          UI.messagebox "Problem opening @file "+filename+" for writing", MB_OK, "Error"
          return
  end

  begin
    @file.write "(created by " + PLUGIN_NAME + ",Version " + VERSION + ")\n"
    
    list = what.sort{ |a,b| entitySort( a,b ) }
 
    list.each{ |entity| dumpEntity( entity, nil ) }
    
    @file.write "M02 (end of program)\n"
    
  rescue StandardError => bang
    @file.write "\n( Closed due to error: " + bang + ")"
    raise
  ensure
    @file.close
  end
end

end #class

file_loaded( File.basename(__FILE__) )

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