Created
May 14, 2021 21:05
-
-
Save takehiko/9413208dd92b80bb5482d0fa92af8c65 to your computer and use it in GitHub Desktop.
A calculator of polygonal coordinates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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