547 lines
13 KiB
Ruby
Executable File
547 lines
13 KiB
Ruby
Executable File
#!/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
|
|
|
|
# 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=:silver,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(:silver)
|
|
@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(: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||Cairo::Color::ROYAL_BLUE)
|
|
@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::CW,Model::SE
|
|
m.draw_circle 160,40,"2",Model::CCW,Model::NE
|
|
m.draw_circle 60,160,"3",Model::CCW,Model::SW
|
|
m.draw_circle 40,40,"4",Model::CW,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
|