Skip to content

Instantly share code, notes, and snippets.

@takehiko
Created May 14, 2021 21:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save takehiko/9413208dd92b80bb5482d0fa92af8c65 to your computer and use it in GitHub Desktop.
Save takehiko/9413208dd92b80bb5482d0fa92af8c65 to your computer and use it in GitHub Desktop.
A calculator of polygonal coordinates
#!/usr/bin/env ruby
# polygon-plotter.rb : A calculator of polygonal coordinates
# by takehikom
# see also:
# https://takehikom.hateblo.jp/entry/2021/05/14/073039
# https://takehikom.hateblo.jp/entry/2021/05/15/073040
class PolygonPlotter
def initialize(opt = {})
if opt[:polygon]
@polygon = opt[:polygon]
raise if !(Array === @polygon) || @polygon.length < 3
else
setup_regular_polygon(opt[:regular] || 3)
end
@precision = opt[:prec] || 6
end
def setup_regular_polygon(number_vertex = 3)
# 正多角形の座標を@polygonに設定する
@polygon = number_vertex.times.map { |i|
angle = 360.0 / number_vertex * i
[Math.sin(angle / 180.0 * Math::PI), Math.cos(angle / 180.0 * Math::PI)]
}
self
end
def setup_fake_regular_polygon(number_vertex = 3, type = 1)
# setup_fake_regular_polygon1またはsetup_fake_regular_polygon2
# を呼び出す
if type == 2
setup_fake_regular_polygon2(number_vertex)
else
setup_fake_regular_polygon1(number_vertex)
end
end
def setup_fake_regular_polygon1(number_vertex = 3)
# すべての辺の長さが等しいが1箇所だけ他と角度が異なる
# 多角形の座標を@polygonに設定する
# number_vertex == 4のとき多角形にならない
# 一辺の長さは正多角形から求める
setup_regular_polygon(number_vertex)
side = Math.sqrt((@polygon[1][0] - @polygon[0][0]) ** 2 +
(@polygon[1][1] - @polygon[0][1]) ** 2)
# 始点
x0 = x = 0.0
y0 = y = 1.0
angle = 360.0 / number_vertex * 0.5
angle_error = 360.0 / number_vertex ** 4 * 2
@polygon = [[x0, y0]]
# 残りの点
1.upto(number_vertex - 1) do |i|
x += side * Math.cos(angle / 180.0 * Math::PI)
y -= side * Math.sin(angle / 180.0 * Math::PI)
@polygon << [x, y]
angle += 360.0 / number_vertex
angle += 360.0 / number_vertex if i == number_vertex - 2 # 最後の1つ前だけ角度を変える
end
self
end
def setup_fake_regular_polygon2(number_vertex = 3)
# すべての辺の長さが等しいが角度が異なる
# 多角形の座標を@polygonに設定する
# 一辺の長さは正多角形から求める
setup_regular_polygon(number_vertex)
side = Math.sqrt((@polygon[1][0] - @polygon[0][0]) ** 2 +
(@polygon[1][1] - @polygon[0][1]) ** 2)
# 始点
x0 = x = 0.0
y0 = y = 1.0
angle = 360.0 / number_vertex * 0.5
angle_error = 360.0 / number_vertex ** 4 * 2
@polygon = [[x0, y0]]
# 始点・終点以外
1.upto(number_vertex - 2) do |i|
x += side * Math.cos(angle / 180.0 * Math::PI)
y -= side * Math.sin(angle / 180.0 * Math::PI)
@polygon << [x, y]
angle += 360.0 / number_vertex - angle_error * i
end
# 終点
xlast, ylast = find_last_point(x0, y0, x, y, side)
@polygon << [xlast, ylast]
self
end
def find_last_point(x1, y1, x2, y2, r)
# 次の条件を満たす[x, y]の1つを返す
# [x1, y1]と[x, y]との距離がrと等しい
# [x2, y2]と[x, y]との距離がrと等しい
d = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
w = d * 0.5
h = -Math.sqrt(r ** 2 - w ** 2)
theta = Math.atan2(y2 - y1, x2 - x1)
x = w * Math.cos(theta) - h * Math.sin(theta) + x1
y = w * Math.sin(theta) + h * Math.cos(theta) + y1
[x, y]
end
def print_status
# 多角形の座標を出力する
puts "point: #{array_to_string(@polygon)}"
@sides = []
@polygon.length.times do |i|
x1, y1 = @polygon[i]
x2, y2 = @polygon[(i + 1) % @polygon.length]
d = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
@sides << d
end
# 各辺の長さの出力
puts "side: #{array_to_string(@sides)}"
@angles = []
@polygon.length.times do |i|
x1, y1 = @polygon[i]
x2, y2 = @polygon[(i + 1) % @polygon.length]
x3, y3 = @polygon[(i + 2) % @polygon.length]
# http://www5d.biglobe.ne.jp/~noocyte/Programming/Geometry/RotationDirection.html#GetAngle
ax = x1 - x2
ay = y1 - y2
bx = x3 - x2
by = y3 - y2
t = Math.atan2(ax * by - ay * bx, ax * bx + ay * by)
a = t / Math::PI * 180.0
a += 360.0 if a < 0
@angles << a
end
# 角の和が大きい場合はすべて360から引く
if @angles.sum >= (@polygon.length - 2) * 180.0 * 1.1
@angles.map! { |a| 360.0 - a }
end
# 各頂点の角度の出力
puts "angle: #{array_to_string(@angles)}: #{@angles.sum}"
self
end
alias :start :print_status
def array_to_string(a)
# 小数点以下の桁数を@precisionにし,配列を文字列にする
a.map { |b|
if Array === b
array_to_string(b)
elsif Numeric === @precision && @precision > 0
"%.*f" % [@precision, b]
else
b
end
}.inspect.gsub('"', '')
end
def to_svg(io = $stdout, param = {})
# SVGを出力する
image_width = (param[:w] || 320).to_i
image_height = (param[:h] || 320).to_i
margin = param[:margin] || 10
line_width = param[:l] || 3
background_color = param[:bg] || "\#f0fff0"
stroke = param[:fg] || "black"
option_exterior = param[:ext] # 真なら外角の線分を出力する
cx = image_width / 2
cy = image_height / 2
mag = [image_width, image_height].min * 0.5 - margin
io.puts '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
io.puts '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d">' % [image_width, image_height]
io.puts '<rect x="0" y="0" width="%d" height="%d" fill="%s" stroke="none" />' % [image_width, image_height, background_color]
if option_exterior
@polygon.length.times do |i|
p0 = @polygon[i]
p1 = @polygon[(i + 1) % @polygon.length]
p2 = [p1[0] + (p1[0] - p0[0]), p1[1] + (p1[1] - p0[1])]
p1 = [cx + p1[0] * mag, cy - p1[1] * mag]
p2 = [cx + p2[0] * mag, cy - p2[1] * mag]
io.puts '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="%s" stroke-width="%f" />' % (p1 + p2 + [stroke, line_width])
end
end
io.print '<polygon points="' + @polygon.map { |a| "%f %f" % [cx + a[0] * mag, cy - a[1] * mag]}.join(" ")
io.puts '" fill="none" stroke="%s" stroke-width="%f" />' % [stroke, line_width]
io.puts '</svg>'
self
end
def self.create_files(param = {})
prec = param[:prec] || 3
min = param[:min] || 3
max = param[:max] || 10
min.upto(max) do |n|
puts "==== Regular, n=#{n}, prec=#{prec} ===="
PolygonPlotter.new(prec: prec, regular: n).start.to_svg(open("polygon#{n}.svg", "w"))
puts "==== Regular, n=#{n}, prec=#{prec}, with exterior edges ===="
PolygonPlotter.new(prec: prec, regular: n).start.to_svg(open("polygon#{n}ext.svg", "w"), ext: true)
puts "==== Fake 1, n=#{n}, prec=#{prec} ===="
PolygonPlotter.new(prec: prec).setup_fake_regular_polygon(n).start.to_svg(open("polygon#{n}f1.svg", "w"))
puts "==== Fake 1, n=#{n}, prec=#{prec}, with exterior edges ===="
PolygonPlotter.new(prec: prec).setup_fake_regular_polygon(n).start.to_svg(open("polygon#{n}f1ext.svg", "w"), ext: true)
puts "==== Fake 2, n=#{n}, prec=#{prec} ===="
PolygonPlotter.new(prec: prec).setup_fake_regular_polygon2(n).start.to_svg(open("polygon#{n}f2.svg", "w"))
puts "==== Fake 2, n=#{n}, prec=#{prec}, with exterior edges ===="
PolygonPlotter.new(prec: prec).setup_fake_regular_polygon2(n).start.to_svg(open("polygon#{n}f2ext.svg", "w"), ext: true)
end
end
end
if __FILE__ == $0
PolygonPlotter.create_files(min: 3, max: 10, prec: 3)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment