#!/usr/bin/ruby

# Simple SVG model generator for MW compatible FCs
# (c) 2015 Jonathan Hudson
# Licence MIT or GPL v2 or later (as you wish)

require 'cairo'
require 'stringio'
include Math

TFLAT=true  # Whether to make Tri / V-tails flat or veed
YFLAT=false # Whether to make Y{4,6} flat or veed

# Calculate motor coordinates for non-trivial platforms
class ShapeFarm
  RAD=0.017453292
  def generate xp,yp,radius,ns=6,offset=0
    p=[]
    0.upto(ns-1) do |n|
      ang = n*360.0/ns + offset
      ang %= 360
      x = radius*sin(RAD*ang)
      y = radius*cos(RAD*ang)
      p << [xp+x, yp-y] # y axis inverted for cairo coord set
    end
    p
  end
end

# The SVG generator
class Model
  attr_accessor :lw, :radius

  RAD = 0.017453292
  RADIUS=28

  # Motor direction for arrow indicators
  CW=0
  CCW=1
  NOARROW=-1

  # Position on the motor circle for arrow indicators
  NE=0
  SE=1
  SW=2
  NW=3

  # Colours
  BODY_GREY='#bababa'
  CIRCLE_GREEN = '#54ad1d'
  ARROW_RED = '#fa0700'

  # We use StringIO in order to be able to add a (non-)copyright statement
  def initialize filename
    @lw = RADIUS
    @radius = RADIUS
    @name = filename
    @output = StringIO.new
    @surface = Cairo::SVGSurface.new(@output, 200,200)
    @cr = Cairo::Context.new(@surface)
  end

  # Draw an arbitrary path, x,y pairs terminated by !
  def draw_path path,fill=BODY_GREY,round=false
    @cr.set_line_cap(Cairo::LINE_JOIN_ROUND) if round
    @cr.set_source_color(fill)
    @cr.set_line_width(@lw)
    first = true
    path.each do |p|
      if p == '!'
	@cr.close_path
	first = true
      elsif first
	@cr.move_to(*p)
	first = false
      else
	@cr.line_to(*p)
      end
    end
    @cr.fill
    @cr.stroke
  end

  # Draw body parts, really just a rounded line
  def draw_body x1,y1,x2,y2
    @cr.set_source_color(BODY_GREY)
    @cr.set_line_width(@lw)
    @cr.set_line_join(Cairo::LINE_JOIN_ROUND)
    @cr.move_to(x1,y1)
    @cr.line_to(x2,y2)
  end

  # Draw a servo box (in rcolor), and black text
  def draw_servo x,y,label,rcolor=:black
    @cr.set_source_color(rcolor)
    @cr.rectangle(x, y, 28, 28);
    @cr.set_font_size(16)
    @cr.stroke
    @cr.move_to(x+4,y+20)
    @cr.set_source_color(:black)
    @cr.show_text(label);
    @cr.stroke
  end

  # Draw direction arrow at Y offset
  def draw_dirn y=80
    @cr.set_line_join(Cairo::LINE_JOIN_BEVEL)
    @cr.set_source_color(ARROW_RED)
    @cr.move_to(100,y)
    @cr.set_line_width(12)
    @cr.rel_line_to(0, 40)
    @cr.stroke
    @cr.set_line_width(1)
    @cr.move_to(100,y-5)
    @cr.rel_line_to(-15, 15)
    @cr.rel_line_to(30, 0)
    @cr.rel_line_to(-15, -15)
    @cr.fill
    @cr.stroke
  end

  # Draw a circle, perhaps with directional arrows
  # lyoffset, lxoffset change label position
  def draw_circle x,y,label,dirn=CCW,loc=NE,fill=nil,colour=nil,lyoffset=0,lxoffset=0
    col = (colour||CIRCLE_GREEN)
    @cr.set_font_size(@radius)
    @cr.set_line_join(Cairo::LINE_JOIN_MITER)
    @cr.set_line_width(3)

    if fill
      @cr.set_source_color(fill)
      @cr.circle(x,y, @radius)
      @cr.fill
      @cr.stroke
    end

    @cr.set_source_color(col)
    @cr.circle(x,y, @radius)

    if dirn != NOARROW
      arrow = @radius*0.6
      adelta = arrow*0.12
      x0 = x
      y0 = y
      dx = 0
      dy = 0
      radj = @radius / Math.sqrt(2)
      xadj = yadj = 0

      case loc
      when NE
	x0 += radj
	y0 -= radj
	case dirn
	when CW
	  xadj = yadj = -arrow
	  dy = adelta
	when CCW
	  xadj = yadj = arrow
	  dx = -adelta
	end
      when SE
	x0 += radj
	y0 += radj
	case dirn
	when CW
	  xadj = arrow
	  yadj = -arrow
	  dx = -adelta
	when CCW
	  xadj = -arrow
	  yadj = arrow
	  dy = -adelta
	end
      when SW
	x0 -= radj
	y0 += radj
	case dirn
      when CW
	  xadj = yadj = arrow
	  dy = -adelta
	when CCW
	  xadj = yadj = -arrow
	  dx = adelta
	end
      when NW
	x0 -= radj
	y0 -= radj
	case dirn
	when CW
	  xadj = -arrow
	  yadj = arrow
	  dx = adelta
	when CCW
	  xadj = arrow
	  yadj = -arrow
	  dy = adelta
	end
      end
      @cr.move_to(x0,y0)
      @cr.rel_line_to(dx, yadj)
      @cr.move_to(x0,y0)
      @cr.rel_line_to(xadj, dy)
    end
    @cr.stroke
    @cr.move_to(x-@radius/4+lxoffset,y+@radius/4+lyoffset)
    @cr.set_source_color(:black)
    @cr.show_text(label);
    @cr.stroke
  end

  # Reset line styles
  def end_body
    @cr.set_line_cap Cairo::LINE_CAP_ROUND
    @cr.stroke
    @cr.set_line_cap Cairo::LINE_CAP_BUTT
  end

  # close a model, write out with attribution
  def close
    @cr.show_page
    @surface.finish
    @output.rewind
    cc=false
    File.open(@name, "w") do |f|
      @output.each do |l|#
	f.puts(l)
	unless cc
	  f.puts("<!-- Public domain (CC-BY-SA if you or your laws insist), generated by Jonathan Hudson's svg_model_motors.rb -->")
	  cc = true
	end
      end
    end
  end
end


def render_bi
  m = Model.new "bicopter.svg"
  m.draw_body 40,100,160,100
  m.end_body
  m.draw_circle 40,100,"1",Model::CW,Model::NW
  m.draw_circle 160,100,"2",Model::CCW,Model::NE
  m.draw_servo  64, 120, "S1"
  m.draw_servo  108, 120, "S2"
  m.draw_dirn 70
  m.close
end

def render_tri
  m = Model.new "tri.svg"
  if TFLAT
    m.draw_body 40,40,160,40
    m.draw_body 100,40,100,160
  else
    m.draw_body 100,50,40,40
    m.draw_body 100,50,160,40
    m.draw_body 100,50,100,160
  end
  m.end_body
  m.draw_circle 100,160,"1",Model::CCW,Model::NW
  m.draw_circle 160,40,"2",Model::CCW,Model::NW
  m.draw_circle 40,40,"3", Model::CCW, Model::NE
  m.draw_servo  140, 140, "S1"
  m.draw_dirn 70
  m.close
end

def render_y4
  m = Model.new "y4.svg"
  m.draw_circle 100,170,"3",Model::CCW,Model::SE,false,:dark_green,14
  if YFLAT == true
    m.draw_body 40,40,160,40
    m.draw_body 100,40,100,140
  else
    m.draw_body 100,50,40,40
    m.draw_body 100,50,160,40
    m.draw_body 100,50,100,140
  end
  m.end_body
  m.draw_circle 160,40,"2",Model::CCW,Model::NE
  m.draw_circle 40,40,"4", Model::CW, Model::NW
  m.draw_circle 100,140,"1",Model::CW,Model::NE,"#fff8",nil,-10

  m.draw_dirn 60
  m.close
end

def render_y6
  m = Model.new "y6.svg"
  m.draw_circle 100,170,"4",Model::CW,Model::SW,false,:dark_green,14
  m.draw_circle 30,30,"6",Model::CCW,Model::NE,false,:dark_green,-10
  m.draw_circle 170,30,"5",Model::CCW,Model::NW,false,:dark_green,-10
  if YFLAT == true
    m.draw_body 40,50,160,50
    m.draw_body 100,50,100,140
  else
    m.draw_body 100,60,40,50
    m.draw_body 100,60,160,50
    m.draw_body 100,60,100,140
  end
  m.end_body
  m.draw_circle 145,55,"2",Model::CW,Model::NW,"#fff8",nil,12
  m.draw_circle 55,55,"3", Model::CW, Model::NE,"#fff8",nil,12
  m.draw_circle 100,140,"1",Model::CCW,Model::NW,"#fff8",nil,-10
  m.draw_dirn 60
  m.close
end

def render_vtail
  m = Model.new  "vtail_quad.svg"
  if TFLAT == true
    m.draw_body 40,40,160,40
    m.draw_body 100,40,100,180
  else
    m.draw_body 100,50,40,40
    m.draw_body 100,50,160,40
    m.draw_body 100,50,100,180
  end
  m.draw_body 100,180,140,160
  m.draw_body 100,180,60,160
  m.end_body
  m.draw_circle 140,160,"1",Model::CCW,Model::SE
  m.draw_circle 160,40,"2",Model::CW,Model::NE
  m.draw_circle 60,160,"3",Model::CW,Model::SW
  m.draw_circle 40,40,"4",Model::CCW,Model::NW
  m.draw_dirn
  m.close
end

def render_atail
  m = Model.new  "atail_quad.svg"
  if TFLAT == true
    m.draw_body 40,40,160,40
    m.draw_body 100,40,100,140
  else
    m.draw_body 100,50,40,40
    m.draw_body 100,50,160,40
    m.draw_body 100,50,100,140
  end
  m.draw_body 100,140,140,160
  m.draw_body 100,140,60,160
  m.end_body
  m.draw_circle 60,160,"1",Model::CCW,Model::SW
  m.draw_circle 160,40,"2",Model::CCW,Model::NE
  m.draw_circle 140,160,"3",Model::CW,Model::SE
  m.draw_circle 40,40,"4",Model::CW,Model::NW
  m.draw_dirn
  m.close
end

def render_octox8 # just x8 surely?
  m = Model.new  "octo_x8.svg"

  m.draw_circle 170,170,"5",Model::CCW,Model::NE,false,:dark_green,14,8
  m.draw_circle 170,30,"6",Model::CW,Model::SE,false,:dark_green,-10,8
  m.draw_circle 30,170,"7",Model::CW,Model::NW,false,:dark_green,14,-10
  m.draw_circle 30,30,"8",Model::CCW,Model::SW,false,:dark_green,-10,-10

  m.draw_body 50,50,150,150
  m.draw_body 50,150,150,50
  m.end_body
  m.draw_circle 150,150,"1",Model::CW,Model::SW,"#fff8",nil,-10
  m.draw_circle 150,50,"2",Model::CCW,Model::NW,"#fff8",nil,12
  m.draw_circle 50,150,"3",Model::CCW,Model::SE,"#fff8",nil,-10
  m.draw_circle 50,50,"4",Model::CW,Model::NE,"#fff8",nil,12
  m.draw_dirn
  m.close
end

def render_quadx
  m = Model.new  "quad_x.svg"
  m.draw_body 40,40,160,160
  m.draw_body 40,160,160,40
  m.end_body
  m.draw_circle 160,160,"1",Model::CW,Model::SE
  m.draw_circle 160,40,"2",Model::CCW,Model::NE
  m.draw_circle 40,160,"3",Model::CCW,Model::SW
  m.draw_circle 40,40,"4",Model::CW,Model::NW
  m.draw_dirn
  m.close
end

def render_quadp
  m = Model.new "quad_p.svg"
  m.draw_body 40,100,160,100
  m.draw_body 100,40,100,160
  m.end_body
  m.draw_circle 100,160,"1",Model::CW,Model::SW
  m.draw_circle 160,100,"2",Model::CCW,Model::NE
  m.draw_circle 100,40,"4",Model::CW,Model::NE
  m.draw_circle 40,100,"3",Model::CCW,Model::SW
  m.draw_dirn
  m.close
end

def render_hexp
  s = ShapeFarm.new
  p = s.generate 100, 100, 60, 6

  m = Model.new  "hex_p.svg"
  m.draw_body  *p[0],*p[3]
  m.draw_body  *p[1],*p[4]
  m.draw_body  *p[2],*p[5]
  m.end_body
  m.radius = 24
  m.draw_circle *p[0],"5",Model::CCW,Model::NW
  m.draw_circle *p[1],"2",Model::CW,Model::NE
  m.draw_circle *p[2],"1",Model::CCW,Model::SE
  m.draw_circle *p[3],"6",Model::CW,Model::SW
  m.draw_circle *p[4],"3",Model::CCW,Model::SW
  m.draw_circle *p[5],"4",Model::CW,Model::NW
  m.draw_dirn
  m.close
end

def render_hexx
  s = ShapeFarm.new
  p = s.generate 100, 100, 60, 6, 30

  m = Model.new "hex_x.svg"
  m.draw_body  *p[0],*p[3]
  m.draw_body  *p[1],*p[4]
  m.draw_body  *p[2],*p[5]
  m.end_body
  m.radius = 24
  m.draw_circle *p[0],"2",Model::CCW,Model::NE
  m.draw_circle *p[1],"5",Model::CW,Model::SE
  m.draw_circle *p[2],"1",Model::CCW,Model::SE
  m.draw_circle *p[3],"3",Model::CW,Model::SW
  m.draw_circle *p[4],"6",Model::CCW,Model::SW
  m.draw_circle *p[5],"4",Model::CW,Model::NW
  m.draw_dirn
  m.close
end

def render_octx
  s = ShapeFarm.new
  p = s.generate 100, 100, 70, 8, 22.5

  m = Model.new  "octo_flat_x.svg"
  m.lw = 20
  m.radius = 20
  m.draw_body  *p[0],*p[4]
  m.draw_body  *p[1],*p[5]
  m.draw_body  *p[2],*p[6]
  m.draw_body  *p[3],*p[7]
  m.end_body
  m.draw_circle *p[0],"2",Model::CCW,Model::NE
  m.draw_circle *p[1],"6",Model::CW,Model::NE
  m.draw_circle *p[2],"3",Model::CCW,Model::SE
  m.draw_circle *p[3],"7",Model::CW,Model::SE
  m.draw_circle *p[4],"4",Model::CCW,Model::SW
  m.draw_circle *p[5],"8",Model::CW,Model::SW
  m.draw_circle *p[6],"1",Model::CCW,Model::NW
  m.draw_circle *p[7],"5",Model::CW,Model::NW
  m.draw_dirn
  m.close
end

def render_octp
  s = ShapeFarm.new
  p = s.generate 100, 100, 70, 8

  m = Model.new "octo_flat_p.svg"
  m.lw = 20
  m.radius = 20

  m.draw_body  *p[0],*p[4]
  m.draw_body  *p[1],*p[5]
  m.draw_body  *p[2],*p[6]
  m.draw_body  *p[3],*p[7]
  m.end_body

  m.draw_circle *p[0],"2",Model::CW,Model::NE
  m.draw_circle *p[1],"6",Model::CCW,Model::NE
  m.draw_circle *p[2],"3",Model::CW,Model::SE
  m.draw_circle *p[3],"7",Model::CCW,Model::SE
  m.draw_circle *p[4],"4",Model::CW,Model::SW
  m.draw_circle *p[5],"8",Model::CCW,Model::SW
  m.draw_circle *p[6],"1",Model::CW,Model::NW
  m.draw_circle *p[7],"5",Model::CCW,Model::NW
  m.draw_dirn
  m.close
end

def render_aero
  m = Model.new "airplane.svg"
  m.lw = 1

# For easy? of understanding, split into parts
# Nose
# m.draw_path([[85,20], [80,40], [120,40], [115,20],'!'],:silver, true)
# Wing
#  m.draw_path([[80,40], [20,60], [20,100], [70,80], [130,80], [180,100],
#                [180,60], [120,40],'!'], :silver, true)
# Aft
#  m.draw_path([[80,80],[90,150],[110,150],[120,80],'!'], :silver, true)
# Tail
#  m.draw_path([[90,150], [50,155], [50,175], [150,175], [150,155], [110,150],'!'], :silver, true)

  m.draw_path([[85,20], [80,40], [20,60], [20,100], [70,80],
		[80,80], [90,150], [50,155], [50,175],
		[150,175], [150,155], [110,150],[120,80],[130,80],
		[180,100], [180,60], [120,40], [115,20],'!'],:silver, true)
  m.draw_path([[20,80],[20,100],[70,80],[70,60], '!'], :red)
  m.draw_path([[180,80],[180,100],[130,80],[130,60], '!'], :green)
  m.draw_path([[50,165], [50,175], [150,175], [150,165],'!'],:orange)
  m.draw_path([[100,140], [95,150], [100,175], [105,150],'!'],:black)
  m.end_body
  m.radius = 14
  m.draw_circle 100,15,"1/2",Model::NOARROW,Model::SE, false, nil, 0, -9
  m.draw_servo 30, 100, " 3", :red
  m.draw_servo 142, 100, " 4", :green

  m.draw_servo 64, 134, " 5", :black
  m.draw_servo 154, 168, " 6", :orange
  m.draw_dirn 50
  m.close
end

def render_wing
  m = Model.new "flying_wing.svg"
  m.lw = 1
  m.draw_path([[80,20],[20,80],[20,120],[70,80],[130,80], [180,120],[180,80],
		[120,20],'!'], :silver)
  m.draw_path([[20,100],[20,120],[70,80],[70,60], '!'], :red)
  m.draw_path([[180,100],[180,120],[130,80],[130,60], '!'], :green)

  m.draw_servo 30, 120, " 3", :red
  m.draw_servo 142, 120, " 4", :green
  m.draw_circle 100,110,"1/2",Model::NOARROW,Model::SE, false, nil, 0, -16
  m.draw_dirn 30
  m.close
end

render_bi
render_tri
render_quadx
render_quadp
render_hexp
render_hexx
render_octx
render_octp
render_vtail
render_atail
render_y4
render_y6
render_octox8
render_aero
render_wing
