# ------------------------------------------------------------------------------
#
# **FixIt 101**
#
# Attempts to turn groups into solids. Primary purpose was to automatically
# finish holes, but looks like its quite successful in attempting to solidify
# and clean-up the model too. Don't use this technique for cleaning models. Use
# TT CleanUp instead. This plugin was specifically designed to intersect holes
# and remove internal faces. See plugin homepage for demonstrations.
#
# Homepage
#   http://sketchucation.com/forums/viewtopic.php?f=323&t=58745#p534651
#
# Features
#   1. Add faces at desired locations.
#   2. Remove duplicate/overlapping faces.
#   3. Remove most internal faces.
#   4. Merge coplanar faces.
#   5. Remove single edges.
#   6. Repair split edges.
#   7. Repair split curves.
#
# Usage
#   - Select entities you want to fix and then select the plugin menu. All
#     sub-groups and sub-components will be fixed too.
#   - Select plugin menu with an empty selection to fix the whole model.
#
# Access
#   - (Menu) Plugins → FixIt 101
#   - (Edit Menu) → FixIt 101
#
# Version
#   1.5.0
#
# Release Date
#   August 31, 2014
#
# To Do:
#   - Orient all faces.
#
# Credits
#   - ThomThom for letting me use his merge faces technique.
#
# Author
#   Anton Synytsia
#
# ------------------------------------------------------------------------------

require 'sketchup.rb'

module AMS; end
module AMS::Plugins; end

module AMS::Plugins::FixIt101

  NAME          = 'FixIt 101'.freeze
  VERSION       = '1.5.0'.freeze
  RELEASE_DATE  = 'August 31, 2014'.freeze

  @data = {
    :added_faces                => 0,
    :removed_duplicate_faces    => 0,
    :removed_internal_faces     => 0,
    :merged_faces               => 0,
    :removed_edges              => 0,
    :repaired_split_edges       => 0,
    :repaired_split_curves      => 0,
    :moved_to_default_layer     => 0
  }

  def self.clear_results
    @data.keys.each { |key| @data[key] = 0 }
  end

  def self.display_results
    count = 0
    @data.each { |k,v| count += v }
    msg = "FixIt 101 Results\n"
    msg << "#{sprintf('%5d', @data[:added_faces])} facets added.\n"
    msg << "#{sprintf('%5d', @data[:removed_duplicate_faces])} duplicate faces removed.\n"
    msg << "#{sprintf('%5d', @data[:removed_internal_faces])} internal faces removed.\n"
    msg << "#{sprintf('%5d', @data[:merged_faces])} faces merged.\n"
    msg << "#{sprintf('%5d', @data[:removed_edges])} edges removed.\n"
    msg << "#{sprintf('%5d', @data[:repaired_split_edges])} split edges repaired.\n"
    msg << "#{sprintf('%5d', @data[:repaired_split_curves])} split curves repaired.\n"
    msg << "#{sprintf('%5d', @data[:moved_to_default_layer])} entities moved to Layer0.\n"
    msg << "Total Changes : #{count}\n"
    puts "\n" + msg
    UI.messagebox(msg)
  end

  # @param [Sketchup::Entities, Sketchup::Selection] ents
  # @param [Boolean] recursive Whether to search within groups inside the group.
  def self.fix_it(ents, recursive = true)
    unless [Sketchup::Entities, Sketchup::Selection].include?(ents.class)
      raise ArgumentError, "Expected Sketchup::Entities or Sketchup::Selection, but got #{ents.class}."
    end
    entities = ents.is_a?(Sketchup::Entities) ? ents : Sketchup.active_model.active_entities
    tra = Geom::Transformation.new
    temp_pt = Geom::Point3d.new(3.14, 1.59, 2.65)
=begin
    # Intersect all edges containing one face.
    to_intersect = []
    ents.grep(Sketchup::Edge).each { |e|
      next unless e.valid?
      to_intersect << e if e.faces.size == 1
    }
    s = entities.size
    entities.intersect_with(false, tra, entities, tra, false, to_intersect)
    ents.add(entities.to_a[s..-1]) if ents.is_a?(Sketchup::Selection)
    to_intersect.clear
=end
    # Remove overlapping/duplicate faces.
    to_remove = []
    ents.grep(Sketchup::Edge).each { |e|
      next unless e.valid?
      e.faces.each { |f1|
        next unless f1.valid?
        e.faces.each { |f2|
          next unless f2.valid?
          next if f1 == f2
          next unless f1.normal.parallel?(f2.normal)
          next if to_remove.include?(f1) or to_remove.include?(f2)
          v1 = f1.outer_loop.vertices
          v2 = f2.outer_loop.vertices
          if (v1 - v2).empty? && (v2 - v1).empty?
            to_remove << f2
          end
        }
      }
    }
    to_remove.each { |e|
      next unless e.valid?
      e.erase!
      @data[:removed_duplicate_faces] += 1
    }
    to_remove.clear
    # Add faces at desired locations.
    size = entities.to_a.size
    ents.grep(Sketchup::Edge).each { |e|
      e.find_faces if e.valid? && e.faces.size == 1
    }
    # Sometimes added faces appear inside solids; remove them.
    entities.to_a[size..-1].each { |face|
      next unless face.valid?
      face.edges.each { |edge|
        if edge.valid? && edge.faces.size > 2
          to_remove << face
          @data[:removed_internal_faces] += 1
          break
        end
      }
    }
    to_remove.each { |e| e.erase! if e.valid? }
    to_remove.clear
    # Sometimes added faces intersect entities in between; remove them.
    # Not implemented yet.
    # Remove internal faces.
    # This removes faces with all face edges having three faces.
    # In other words, this removes most faces inside solids.
    ents.grep(Sketchup::Face).each { |e|
      next unless e.valid?
      remove = true
      e.edges.each { |edge|
        next if edge.faces.size > 2
        remove = false
        break
      }
      to_remove << e if remove
    }
    to_remove.each { |e|
      next unless e.valid?
      edges = e.edges
      e.erase!
      ents.add(edges) if ents.is_a?(Sketchup::Selection)
      @data[:removed_internal_faces] += 1
    }
    to_remove.clear
    # Remove unused edges.
    # Removes all coplanar and single edges.
    2.times {
      ents.grep(Sketchup::Edge).each { |e|
        next unless e.valid?
        if e.faces.empty?
          e.erase!
          @data[:removed_edges] += 1
          next
        end
        next if e.faces.size != 2
        f1, f2 = e.faces
        # Check if faces are coplanar.
        if f1.normal.parallel?(f2.normal) && f1.material == f2.material && f1.back_material == f2.back_material
          # Verify that faces are safe to merge. Faces are safe to merge if they are
          # coplanar. Checking face normal is not always enough. Use technique by
          # ThomThom, which checks if all points lie on plane.
          vertices = f1.vertices + f2.vertices
          plane = Geom.fit_plane_to_points( vertices )
          safe = vertices.all? { |v| v.position.on_plane?(plane) }
          if safe
            e.erase!
            @data[:removed_edges] += 1
            @data[:merged_faces] += 1
          end
        end
      }
    }
    # Repair split edges.
    repaired = []
    ents.grep(Sketchup::Edge).each { |e|
      next unless e.valid?
      e.vertices.each { |v|
        next unless v.valid?
        if v.edges.size == 2
          v1 = v.edges[0].line[1]
          v2 = v.edges[1].line[1]
          if v1.parallel?(v2)
            repaired << e
            to_remove << entities.add_line(v.position, temp_pt)
          end
        end
      }
    }
    to_remove.each { |e|
      next unless e.valid?
      e.erase!
    }
    to_remove.clear
    repaired.uniq!
    repaired.each { |e|
      next unless e.valid?
      @data[:repaired_split_edges] += 1
    }
    repaired.clear
    # Repair curves.
    repaired = {}
    ents.grep(Sketchup::Edge).each { |e|
      next unless e.valid?
      next unless e.curve
      e.vertices.each { |v|
        next unless v.valid?
        # Determine if current vertex edges contains a broken curve.
        found = false
        v.edges.each { |edge|
          next unless edge.valid?
          next unless edge.curve
          next if edge == e || edge.curve.edges.include?(e)
          next if edge.faces.size != e.faces.size
          found = true
          break
        }
        next unless found
        # Soften surrounding edges if the curve exists.
        v.edges.each { |edge|
          next unless edge.valid?
          if edge.curve
            repaired[edge.curve] = edge.curve.edges.size
            next
          end
          edge.soft = true
          edge.smooth = true
        }
        # Attempt to combine curve by adding a temporary edge and deleting.
        entities.add_line(v.position, temp_pt).erase!
      }
      # We don't need a curve if it consists of one edge.
      e.explode_curve if e.curve.count_edges == 1
    }
    repaired.keys.uniq.each { |curve|
      next unless curve.valid?
      next if curve.edges.size == repaired[curve]
      @data[:repaired_split_curves] += 1
    }
    repaired.clear
    # Orient faces.
    # Not yet implemented.
    # Geometry to layer zero.
    control_types = [Sketchup::Edge, Sketchup::Face, Sketchup::ConstructionLine, Sketchup::ConstructionPoint]
    ents.each { |e|
      next unless e.valid?
      if control_types.include?(e.class) && e.layer.name != 'Layer0'
        e.layer = 'Layer0'
        @data[:moved_to_default_layer] += 1
      end
    }
    # Recursive.
    return unless recursive
    ents.grep(Sketchup::Group).each { |e| fix_it(e.entities, true) }
    processed_definitions = []
    ents.grep(Sketchup::ComponentInstance).each { |e|
      next if processed_definitions.include?(e.definition)
      processed_definitions << e.definition
      fix_it(e.definition.entities, true)
    }
    processed_definitions.clear
  end

  # @param [Sketchup::Entities, Sketchup::Selection] ents
  # @param [Boolean] recursive Whether to search within groups inside the group.
  def self.fix_it2(ents, recursive = true)
    # Remove unused faces level 2.
    # Removes all faces connected to a solid.
    ents.grep(Sketchup::Face).each { |e|
      next unless e.valid?
      remove = false
      e.edges.each { |edge|
        s = edge.faces.size
        if s == 3
          remove = true
        elsif s == 2
          remove = false
          break
        end
      }
      if remove
        e.erase!
        count += 1
      end
    }
    # Remove unused faces level 3.
    # Removes all faces inside solid.
    # This requires a much more advanced technique.
    # Return number of changes made.
    # Other way
    ents.grep(Sketchup::Edge).each { |e|
      next
      next unless e.valid?
      next if e.faces.size != 1
      f = e.faces[0]
      e.find_faces
      next if e.faces.size != 2
      # Verify that added face doesn't intersect with other geometry in between.
      # Erase if it does.
      e.faces.each { |face|
        next unless face.valid?
        next if face == f
        grp = entities.add_group
        plane = face.plane
        pts = []
        face.vertices.each { |v|
          pt = v.position
          ext unless pt.on_plane?(plane)
          pts << pt
        }
        grp.entities.add_face(pts)
        size = grp.entities.size
        entities.intersect_with(false, tra, grp.entities, tra, false, ents.to_a)
        face.erase! if grp.entities.size > size
        puts 'added'
        grp.erase!
      }
      ents.add(e.faces) if ents.is_a?(Sketchup::Selection)
      p e.faces
      @data[:added_faces] += e.faces.size - 1
    }
    count
  end

  def self.fix_selection
    model = Sketchup.active_model
    if Sketchup.version.to_i < 7
      model.start_operation('FixIt 101')
    else
      model.start_operation('FixIt 101', true)
    end
    fix_it(model.selection, true)
    model.commit_operation
    display_results
    clear_results
  end

  def self.fix_model
    model = Sketchup.active_model
    if Sketchup.version.to_i < 7
      model.start_operation('FixIt 101')
    else
      model.start_operation('FixIt 101', true)
    end
    fix_it(model.entities, true)
    display_results
    clear_results
    res = UI.messagebox("Would you like to purge unused definitions, materials, layers, and styles too?", MB_YESNO)
    if res == IDYES
      s1 = model.definitions.count
      model.definitions.purge_unused
      s1 -= model.definitions.count
      s2 = model.materials.count
      model.materials.purge_unused
      s2 -= model.materials.count
      s3 = model.layers.count
      model.layers.purge_unused
      s3 -= model.layers.count
      s4 = model.styles.count
      model.styles.purge_unused
      s4 -= model.styles.count
      msg = "Purge Unused Entities Results\n"
      msg << "#{sprintf('%5d', s1)} definitions removed.\n"
      msg << "#{sprintf('%5d', s2)} materials removed.\n"
      msg << "#{sprintf('%5d', s3)} layers removed.\n"
      msg << "#{sprintf('%5d', s4)} styles removed.\n"
      msg << "Total : #{s1 + s2 + s3 + s4}\n"
      puts "\n" + msg
      UI.messagebox(msg)
    end
    model.commit_operation
  end

  def self.fix_auto
    Sketchup.active_model.selection.empty? ? fix_model : fix_selection
  end

end # module AMS::Plugins::FixIt101


unless file_loaded?(__FILE__)
  submenu = UI.menu('Plugins')

  cmd = UI::Command.new('FixIt 101'){
    AMS::Plugins::FixIt101.fix_auto
  }
  cmd.menu_text = cmd.tooltip = 'FixIt 101'
  cmd.status_bar_text = 'Solidify and clean-up the model.'
  submenu.add_item(cmd)

  UI.add_context_menu_handler { |menu|
    menu.add_item(cmd) unless Sketchup.active_model.selection.empty?
  }

  file_loaded(__FILE__)
end
