# A plugin to draw parametric regular polyhedra, of a user-specified radius or length of side
# load "jwm_polyhedra/polyhedra.rb" in Ruby console to load edited file

# License: The MIT License (MIT)

# Copyright 2014 John W. McClenahan

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sub-license, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

# v1.0. Draws all Platonic solids. Parts of the code adapted from Parametric 3D Shapes plugin
# v1.1  Added choice of length of side, or radius, for size.
# v1.1.1 Reverted to plain 'solid at origin' after experimenting with option to
#         pick or enter origin point.
# v1.1.2 Added cpoint at centre of each solid, to act as pick point for subsequent Move
# v1.1.3 Corrected Copyright notice and edited parametric.rb to remove reference to 
#         global constant $parametric_loaded, and replace by checking if __FILE__  was 
#         previously loaded
# v1.2  Changed code to insert polyhedron as a component, at user-defined pick point, 
#       instead of a group at the origin. Added additional cpoint at centre of base

# Load other required files
require "sketchup.rb"
require File.join(File.dirname(__FILE__), 'parametric.rb')

module JWM::Polyhedra
PLUGIN = self # Allows self reference later when calling function in module

### Constants used in construction
#============================================================================
## Tetrahedron
#-------------
# Define constant asin(0.333333) 
#  - angle between radius of solid and radius of base
ASIN_0333 = Math.asin(0.333333333333) # (in radians) = 19.471220614233577 degrees

# Define constant cos((asin(0.333333)) - angle between radius of base and edge of base
COS_ASIN_0333 = Math.cos(ASIN_0333)

# Define constant for the ratio of side to radius
SIDE_TO_RAD_TETRA = 1.632993161856
#============================================================================
## Cube
#------
# Define constant sqrt(3)
SQRT3 = Math.sqrt(3.0)

# Define constant for the ratio of radius to side
SIDE_TO_RAD_CUBE = 2.0/SQRT3

#============================================================================
# Octahedron
# Pre-define constant sqrt(2) to save calculation time
SQRT2 = Math.sqrt(2.0)

# Define constant for ratio of side to radius
SIDE_TO_RAD_OCTA = SQRT2

#============================================================================
## Dodecahedron and Icosahedron
#------------------------------
# Define constant PHI as the Golden Ratio (used in constructing Dodecahedron and Icosahedron)
PHI = (1.0 + Math.sqrt(5.0))/2 # = 1.618033988749895 approx

# Define constants as the lengths of sides of a Golden Section rectangle which fits inside an Icosahedron
#  with circumsphere radius = 1.0 unit
SHORT_SIDE = 0.52573125081 # Mathematical definitions to replace this in due course
LONG_SIDE = 0.85065072264

# Define constants as the lengths of sides of a Golden Section rectangle which defines 
#   location of points of Dodadecahedron of unit radius r (derived from accurate scale drawing:
#   mathematical derivation to replace this in due course. See diagram for definition of a, b, and c)
DDH_A = 0.607061998207
DDH_B = 0.982246946377
DDH_C = 0.794654472292

# Define constant for ratio of side to radius
SIDE_TO_RAD_DODECA = 0.713644179547
SIDE_TO_RAD_ICOSA = 1.051462501620


#=============================================================================
# Find which unit and format the model is using and define unit_length
#   accordingly
#   When LengthUnit = 0
#     LengthFormat 0 = Decimal inches
#     LengthFormat 1 = Architectural (feet and inches)
#     LengthFormat 2 = Engineering (feet)
#     LengthFormat 3 = Fractional (inches)
#   When LengthUnit = 1
#     LengthFormat 0 = Decimal feet
#   When LengthUnit = 2
#     LengthFormat 0 = Decimal mm
#   When LengthUnit = 3
#     LengthFormat 0 = Decimal cm
#   When LengthUnit = 4
#     LengthFormat 0 = Decimal metres

def self.unit_length
  # Get model units (imperial or metric) and length format.
  model = Sketchup.active_model
  manager = model.options
  if provider = manager["UnitsOptions"] # Check for nil value
    length_unit = provider["LengthUnit"] # Length unit value
    length_format = provider["LengthFormat"] # Length format value

    case length_unit
    when 0 ## Imperial units
      if length_format == 1 || length_format == 2
      # model is using Architectural (feet and inches)
      # or Engineering units (feet)
      unit_length = 1.feet
      else
      ## model is using (decimal or fractional) inches
      unit_length = 1.inch
      end # if
    when 1
      ## Decimal feet
      unit_length = 1.feet
    when 2
      ## model is using metric units - millimetres
      unit_length = 10.mm
    when 3
      ## model is using metric units - centimetres
      unit_length = 10.cm
    when 4
      ## model is using metric units - metres
      unit_length =  1.m
    end #end case

  else
    UI.messagebox " Can't determine model units - please set in Window/ModelInfo"
  end # if
end

def self.size_by
  # Prompt user to say whether to make the radius or side the measure of size
  @@size_by ||= "Side" # Set default value for size setting
  prompts = ["Size by radius or length of side? "]
  defaults = ["Side"]
  list = ["Side|Radius"]

  # Get size
  results = UI.inputbox(prompts, defaults, list, "How do you want to specify the size of the polyhedron?")
  if results # Return selection
    size_by = results[0]
  else
    size_by = "Side"
  end
end

def onCancel(flag, view)
  self.reset(view)
end
#=============================================================================

class Tetrahedron < Parametric
# Note: from Wikipedia, radius of circumsphere is sqrt(3/8)*edge_length
# Wikipedia gives coordinates of corners (±1, 0, -1/sqrt(2)), (±1, 0, -1/sqrt(2)) for tetrahedron centred on the origin with edge_length = 2. This is # WRONG. The centre is not midway up between base and apex.

# I've redrawn tetrahedron in a circumsphere centred at ORIGIN with unit radius (see diagrams in DOCUMENTATION.md)
# Height from centroid of base triangle to apex is 1.333recurring * radius
# From ORIGIN to apex is just the radius
# From centroid of base to ORIGIN is 0.333recurring * radius
# Side of triangular face is 2 * (sqrt(1-(1/3)**2) * cos(30 degrees) = 1.632993161856

 def self.create
    tetrahedron = Tetrahedron.new
    defn = tetrahedron.entity
    Sketchup.active_model.place_component(defn, false)
  end

  def create_entity(model)
    @entity = model.definitions.add("Tetrahedron")
  end

  def create_entities(data, container)
    draw_tetrahedron(data, container)

  end

  def draw_tetrahedron(data, container)
  # Set sizes to draw
  size = data[@@size_by].to_l  # Radius or side
  if @@size_by == "Side"
    radius = size/SIDE_TO_RAD_TETRA
  else
    radius = size
  end
  # Remember values for next use
  @@dimension1 = size

  # Add point at centre to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,0])

  # Draw base and define apex point
  base_down_by = -radius/3.0
  triangle = container.add_ngon [0,0, base_down_by], Z_AXIS, radius * COS_ASIN_0333, 3
  # Reverse to get front face outside
  triangle.reverse!
  base = container.add_face triangle
  base_edges = base.edges

  # Create the sides
  apex = [0,0,radius]

  for i in 0..2
    edge = base_edges[i] 

    tetrahedron = container.add_face edge.start.position, edge.end.position, apex
    
    #Reverse faces round apex
    container.each do |entity|
      if entity.is_a? Sketchup::Face
        entity.reverse!
      end #if
    end #do
  end #for
  # Add cpoint at centre of base to act as further pick point for placement
  container.add_cpoint([0,0,base_down_by])

end

def default_parameters
  # Ask whether to set size of radius or side (call size_by method)
  @@size_by = PLUGIN.size_by

  # Set starting defaults to one unit_length
  @@unit_length = PLUGIN.unit_length

  # Set other starting defaults if not set
  if !defined? @@dimension1  # then no previous values input
    defaults = { @@size_by => @@unit_length }
  else
  # Reuse last inputs as defaults
    defaults = { @@size_by => @@dimension1 }
  end # if

  # Return values
  defaults
end

def translate_key(key)
  prompt = key

  # Return value 
  # No translation needed here - key is already capitalised as Radius or Side 
  prompt
end

def validate_parameters(data)
  ok = true

  # Return value
  ok
end

end # Class Tetrahedron

#=============================================================================

class Cube < Parametric
# A cube of side s has radius of circumsphere r = s*sqrt(3)/2

# So a cube of radius r has a side of 2*r/sqrt(3), and half the diagonal of the cube's 
#   base (the radius) is (r/sqrt(3))*sqrt(2)

  def self.create
    cube = Cube.new
    defn = cube.entity
    Sketchup.active_model.place_component(defn, false)
  end

  def create_entity(model)
    @entity = model.definitions.add("Cube")
  end

  def create_entities(data, container)
    draw_cube(data, container)
  end

  def draw_cube(data, container)
  # Set sizes to draw
  size = data[@@size_by].to_l  # Radius or side
  if @@size_by == "Side"
    radius = size/SIDE_TO_RAD_CUBE
  else
    radius = size
  end
  
  # Remember values for next use
  @@dimension1 = size

  # Add point at centre to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,0])

 
  # Draw base, reverse it to have it facing outwards, and pushpull it to height
  base_down_by = -radius/SQRT3
    square = container.add_ngon [0,0, base_down_by], Z_AXIS, (radius/SQRT3)*SQRT2, 4
  # Reverse to get front face outside
  square.reverse!

  
  # Add base to container and pushpull it up to form the cube
  base = container.add_face square
  base.pushpull 2.0*radius/SQRT3  

  # Add point at centre of base to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,base_down_by])
   
end

def default_parameters
  # Ask whether to set size of radius or side
  @@size_by = PLUGIN.size_by
  
  # Set starting defaults to one unit_length
  @@unit_length = PLUGIN.unit_length

  # Set other starting defaults if not set
  if !defined? @@dimension1  # then no previous values input
    defaults = { @@size_by => @@unit_length }
  else
  # Reuse last inputs as defaults
    defaults = { @@size_by => @@dimension1 }
  end # if

  # Return values
  defaults
end

def translate_key(key)
  prompt = key

  # Return value
  # No translation needed here - key is already capitalised as Radius or Side 
  prompt
end

def validate_parameters(data)
  ok = true

  # Return value
  ok
end

end # Class Cube

#=============================================================================

class Octahedron < Parametric

def self.create
  octahedron = Octahedron.new
  defn = octahedron.entity
  Sketchup.active_model.place_component(defn, false)
end

def create_entity(model)
  @entity = model.definitions.add("Octahedron")
end

def create_entities(data, container)
  draw_octahedron(data, container)
end

def draw_octahedron(data, container)

  # Octahedron has square centre of the same half-diagonal as the radius of its circumsphere

  # Set sizes to draw
  size = data[@@size_by].to_l  # Radius or side
  if @@size_by == "Side"
    radius = size/SIDE_TO_RAD_OCTA
  else
    radius = size
  end
  
  # Remember values for next use
  @@dimension1 = size

  # Add point at centre to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,0])
  
  # Draw base and define apex points
  square = container.add_ngon [0,0,0], Z_AXIS, radius , 4
  top_apex = [0,0,radius]
  bottom_apex = [0,0,-radius]
  
  # Create the faces
  edge1 = nil
  edge2 = nil

  for i in 0..3
    edge = square[i] 
    container.add_face edge.start.position, edge.end.position, top_apex # top half  
    container.add_face edge.start.position, edge.end.position, bottom_apex # bottom_half
    container.each do |entity|
      if entity.is_a? Sketchup::Face
        entity.reverse!
      end #if
    end #do
  end #for
end

def default_parameters
  # Ask whether to set size of radius or side
  @@size_by = PLUGIN.size_by
  
  # Set starting defaults to one unit_length
  @@unit_length = PLUGIN.unit_length

  # Set other starting defaults if not set
  if !defined? @@dimension1  # then no previous values input
    defaults = { @@size_by => @@unit_length }
  else
  # Reuse last inputs as defaults
    defaults = { @@size_by => @@dimension1 }
  end # if

  # Return values
  defaults
end

def translate_key(key)
  prompt = key

  # Return value
  prompt
end

def validate_parameters(data)
  ok = true

  # Return value
  ok
end

end # Class Octahedron
#======================================================
class Dodecahedron < Parametric

def self.create
  dodecahedron = Dodecahedron.new
  defn = dodecahedron.entity
  Sketchup.active_model.place_component(defn, false)
end

def create_entity(model)
  @entity = model.definitions.add("Dodecahedron")
end

def create_entities(data, container)
  draw_dodecahedron(data, container)
end

def draw_dodecahedron(data, container)  
  # Set sizes to draw
  size = data[@@size_by].to_l  # Radius or side
  if @@size_by == "Side"
    radius = size/SIDE_TO_RAD_DODECA
  else
    radius = size
  end
  
  # Remember values for next use
  @@dimension1 = size

  # Golden rectangle locating points on Dodecahedron with unit radius has 
  #   sides of length DDH_A, DDH_B, DDH_C defined in module initialization above
  
  # Scale size by radius
  short_side = radius*DDH_A # of golden rectangle
  long_side = radius*DDH_B # of golden rectangle
  half_height = radius*DDH_C # of dodecahedron = (DDH_A + DDH_B)/2
  delta = half_height - short_side
  
  # Create an empty mesh
  numpoly = 12 # faces
  numpts = 20 # vertices
  mesh = Geom::PolygonMesh.new(numpts, numpoly)
  
  # Define rotations of 36 and 72 degrees about Z_AXIS
  rotate_minus36 = Geom::Transformation.rotation ORIGIN, Z_AXIS, -36.degrees
  rotate36 = Geom::Transformation.rotation ORIGIN, Z_AXIS, 36.degrees
  rotate72 = Geom::Transformation.rotation ORIGIN, Z_AXIS, 72.degrees
  
  # Define four arrays for base, lower shoulder, upper shoulder and top points
  # We'll later make the sixth point the same as the first to allow iterations to 'wrap around'
  points_base = [6]
  points_lower = [6]
  points_upper = [6]
  points_top = [6]

  # Define points as starting vertices of Dodecahedron at each level
  points_base[0] = Geom::Point3d.new(short_side, 0, -half_height) # first base level point{
  points_lower[0] = Geom::Point3d.new(long_side,0, -delta) # first lower 'shoulder' level point
  points_upper[0] = Geom::Point3d.new(long_side,0,delta).transform rotate_minus36 # first upper 'shoulder' level point
  points_top[0] = Geom::Point3d.new(short_side, 0, half_height).transform rotate_minus36 # first top point
  
  # First point in each 'row' is already drawn (i=0): 
  #  just fill in the gaps and add an extra one on top of the first point
  for i in 1..5 
    # base points
    points_base[i] = Geom::Point3d.new(points_base[i-1]).transform rotate72
    # lower 'shoulder' points
    points_lower[i] = Geom::Point3d.new(points_lower[i-1]).transform rotate72
    # upper 'shoulder' points
    points_upper[i] = Geom::Point3d.new(points_upper[i-1]).transform rotate72
    # top points
    points_top[i] = Geom::Point3d.new(points_top[i-1]).transform rotate72
  end

=begin
  # Add base points to container (for initial testing)
  points_base.each do |point|
    if point
      container.add_cpoint point
    end
 # p "Point added " + point.inspect
  end
  points_lower.each do |point|
    if point
      container.add_cpoint point
    end
 # p "Point added " + point.inspect
  end
  points_upper.each do |point|
    if point
      container.add_cpoint point
    end
 # p "Point added " + point.inspect
  end
  points_top.each do |point|
    if point
      container.add_cpoint point
    end
 # p "Point added " + point.inspect
  end
=end

# Add point at centre to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,0])

# Create successive faces
  # Base - draw clockwise to face outside down
  mesh.add_polygon points_base[0], points_base[4], points_base[3], points_base[2], points_base[1]

  # First row of surrounding faces
  for i in 0..4
    mesh.add_polygon points_base[i], points_base[i+1], points_lower[i+1], points_upper[i+1], points_lower[i]
  end
    
  # Upper row of surrounding faces
  for i in 0..4
    mesh.add_polygon points_lower[i], points_upper[i+1], points_top[i+1], points_top[i], points_upper[i]
  end

  # Top - draw counter-clockwise to face outside up
  mesh.add_polygon points_top[0], points_top[1], points_top[2], points_top[3], points_top[4]

  # Create faces from the mesh
  container.add_faces_from_mesh mesh, 0 # smooth constant = 0 for no smoothing

  # Add centrepoint on bottom face as pick point for potential Move/Copy operations
  container.add_cpoint ([0,0,-half_height])
end

def default_parameters
  # Ask whether to set size of radius or side
  @@size_by = PLUGIN.size_by
  
  # Set starting defaults to one unit_length
  @@unit_length = PLUGIN.unit_length

  # Set starting defaults if none set
  if !defined? @@dimension1  # then no previous values input
    defaults = { @@size_by => @@unit_length }
  else
  # Reuse last inputs as defaults
    defaults = { @@size_by => @@dimension1 }
  end # if

  # Return values
  defaults
end

def translate_key(key)
  prompt = key

  # Return value
  prompt
end

def validate_parameters(data)
  ok = true

  # Return value
  ok
end

end # Class Dodecahedron

#======================================================
class Icosahedron < Parametric

def self.create
  icosahedron = Icosahedron.new
  defn = icosahedron.entity
  Sketchup.active_model.place_component(defn, false)
end

def create_entity(model)
  @entity = model.definitions.add("Icosahedron")
end

def create_entities(data, container)
  draw_icosahedron(data, container)
end

def draw_icosahedron(data, container)  

  # Set sizes to draw
  size = data[@@size_by].to_l  # Radius or side
  if @@size_by == "Side"
    radius = size/SIDE_TO_RAD_ICOSA
  else
    radius = size
  end
  
  # Remember values for next use
  @@dimension1 = size
  # Golden rectangle fitting inside Icosahedron with unit radius has 
  #   sides of length LONG_SIDE, SHORT_SIDE, defined in module initialization above
  
  # Scale size by radius
  long_side = radius*LONG_SIDE 
  short_side = radius*SHORT_SIDE

  # Create an empty mesh
  numpoly = 20 # faces
  numpts = 12 # vertices
  mesh = Geom::PolygonMesh.new(numpts, numpoly)
  
  # Define points as vertices of Icosahedron
  # (I'm sure there's a way to iterate through this, but I haven't worked it out yet)
  points = []
  points[0] = Geom::Point3d.new([long_side,-short_side,0])
  points[1] = Geom::Point3d.new([short_side,0,long_side])
  points[2] = Geom::Point3d.new([long_side,short_side,0]) 
  points[3] = Geom::Point3d.new([0,long_side,short_side])
  points[4] = Geom::Point3d.new([-long_side,short_side,0])
  points[5] = Geom::Point3d.new([-short_side,0,long_side])
  points[6] = Geom::Point3d.new([-long_side,-short_side,0])
  points[7] = Geom::Point3d.new([0,-long_side,short_side])
  points[8] = Geom::Point3d.new([short_side,0,-long_side])
  points[9] = Geom::Point3d.new([0,long_side,-short_side])
  points[10] = Geom::Point3d.new([-short_side,0,-long_side])
  points[11] = Geom::Point3d.new([0,-long_side,-short_side])

  # Add faces from points
  mesh.add_polygon points[0], points[2], points[1]
  mesh.add_polygon points[2], points[3], points[1]
  mesh.add_polygon points[3], points[5], points[1]
  mesh.add_polygon points[3], points[4], points[5]
  mesh.add_polygon points[4], points[6], points[5]
  mesh.add_polygon points[6], points[7], points[5]
  mesh.add_polygon points[7], points[1], points[5]
  mesh.add_polygon points[7], points[0], points[1]
  mesh.add_polygon points[0], points[8], points[2]
  mesh.add_polygon points[2], points[8], points[9]
  mesh.add_polygon points[2], points[9], points[3]
  mesh.add_polygon points[3], points[9], points[4]
  mesh.add_polygon points[9], points[10], points[4]
  mesh.add_polygon points[4], points[10], points[6]
  mesh.add_polygon points[9], points[8], points[10]
  mesh.add_polygon points[6], points[10], points[11]
  mesh.add_polygon points[6], points[11], points[7]
  mesh.add_polygon points[7], points[11], points[0]
  mesh.add_polygon points[11], points[10], points[8]
  mesh.add_polygon points[11], points[8], points[0]
  mesh.add_polygon points[0], points[8], points[2]  

  # Add point at centre to act as pick point for any subsequent Move operation
  container.add_cpoint([0,0,0])
  
  # Create faces from the mesh
  container.add_faces_from_mesh(mesh, 0) # smooth constant = 0 for no smoothing

end

def default_parameters
  # Ask whether to set size of radius or side
  @@size_by = PLUGIN.size_by
  
  # Set starting defaults to one unit_length
  @@unit_length = PLUGIN.unit_length

  # Set starting defaults if none set
  if !defined? @@dimension1  # then no previous values input
    defaults = { @@size_by => @@unit_length }
  else
  # Reuse last inputs as defaults
    defaults = { @@size_by => @@dimension1 }
  end # if

  # Return values
  defaults
end

def translate_key(key)
  prompt = key

  # Return value
  prompt
end

def validate_parameters(data)
  ok = true

  # Return value
  ok
end

end # Class Icosahedron

#=============================================================================

# Add a menu for creating polyhedra
# Checks if this script file has been loaded before in this SU session
unless file_loaded?(__FILE__) # If not, create menu entries
  shapes_menu = UI.menu("Draw").add_submenu("Polyhedra")
  shapes_menu.add_item("Tetrahedron") { Tetrahedron.create }
  shapes_menu.add_item("Cube") { Cube.create }
  shapes_menu.add_item("Octahedron") { Octahedron.create }
  shapes_menu.add_item("Dodecahedron") { Dodecahedron.create }
  shapes_menu.add_item("Icosahedron") { Icosahedron.create }
  file_loaded(__FILE__)
end

end # module JWM::Polyhedra
