symmetry_axis 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/symmetry_axis/finder.rb +85 -0
- data/lib/symmetry_axis/line.rb +105 -103
- data/lib/symmetry_axis.rb +7 -81
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec1f5b69d4fc36f4c0c526330060b996ed5c5718
|
4
|
+
data.tar.gz: 91850dc12093fc4b9fb4adb0174807fda74e7658
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 838a5ebaca4fbe9137b3dd4f91bae665ca0385a088a35b2e1199b224570f9d86e430be2940d45981909cc169dfa9982b6fb876370c6c31da2e3a5888aa7f10e8
|
7
|
+
data.tar.gz: b0255602df7641adb7eeb17e4ffe2e8edf2f9fd812efce39e868dcb07cdc8677c603cd7cd83cfcbca6174879e3946a5e99e1e813eadad084d451efd8a12f1803
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module SymmetryAxis
|
2
|
+
# The algorithm, based on principle, every point have a pair.
|
3
|
+
# It uses the first got point and iterates over +(N - 1)+
|
4
|
+
# other points to find a line,
|
5
|
+
# that another points lie by twos on lines parallel to.
|
6
|
+
# The perpendicular line to this line is the symmetry axis.
|
7
|
+
class Finder
|
8
|
+
attr_reader :line
|
9
|
+
|
10
|
+
def initialize(point_array)
|
11
|
+
@input = point_array
|
12
|
+
return if @input.size.odd?
|
13
|
+
@impossible_angles = []
|
14
|
+
@two_points_cursor = 1
|
15
|
+
loop do
|
16
|
+
return unless take_two_points # step 1
|
17
|
+
break if points_are_on_parallel_lines # step 2
|
18
|
+
end
|
19
|
+
find_axis # step 3
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def take_two_points
|
25
|
+
@two_points = [@input.first]
|
26
|
+
(@two_points_cursor...(@input.size)).each do |i|
|
27
|
+
rad = Line.for_points(@two_points[0], @input[i]).angle_rad
|
28
|
+
next if impossible?(rad)
|
29
|
+
choose_point i
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def choose_point(i)
|
36
|
+
@two_points_cursor = i + 1
|
37
|
+
@two_points[1] = @input[i]
|
38
|
+
@two_points_line = Line.for_points(@two_points[0], @input[i])
|
39
|
+
end
|
40
|
+
|
41
|
+
def impossible?(rad)
|
42
|
+
return false if @impossible_angles.empty?
|
43
|
+
impossible_rad = @impossible_angles.min_by { |e| (e - rad).abs }
|
44
|
+
(rad - impossible_rad).abs < (2 * Line.EPS)
|
45
|
+
end
|
46
|
+
|
47
|
+
def points_are_on_parallel_lines
|
48
|
+
other_points = @input.select { |p| !@two_points.include?(p) }
|
49
|
+
@axis_perpendicular_points = @two_points.dup
|
50
|
+
check_perpendiculars(other_points)
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_perpendiculars(points)
|
54
|
+
loop do
|
55
|
+
break if points.empty?
|
56
|
+
line = @two_points_line.parallel(*points.first)
|
57
|
+
size0 = points.size
|
58
|
+
on_line = points.select { |p2| line.on_me?(*p2) }
|
59
|
+
points -= on_line
|
60
|
+
return false if size0 - points.size < 2
|
61
|
+
save_perpendicular_points(on_line, line)
|
62
|
+
end
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def save_perpendicular_points(points, line)
|
67
|
+
return if line != @two_points_line
|
68
|
+
points += @two_points
|
69
|
+
@axis_perpendicular_points = pppair(points, line)
|
70
|
+
end
|
71
|
+
|
72
|
+
def pppair(points, line)
|
73
|
+
if line.vertical?
|
74
|
+
[points.min_by { |p| p[1] }, points.max_by { |p| p[1] }]
|
75
|
+
else
|
76
|
+
[points.min_by { |p| p[0] }, points.max_by { |p| p[0] }]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_axis
|
81
|
+
p1, p2 = *@axis_perpendicular_points
|
82
|
+
@line = Line.symmetryof(p1[0], p1[1], p2[0], p2[1])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/symmetry_axis/line.rb
CHANGED
@@ -1,126 +1,128 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
1
|
+
module SymmetryAxis
|
2
|
+
# The lines here are defined by the coefficients of the equation:
|
3
|
+
# y = a + bx
|
4
|
+
# But this equation can not describe vertical lines (infinite +b+).
|
5
|
+
# So the vertical lines are defined by +x+.
|
6
|
+
class Line
|
7
|
+
attr_reader :a, :b, :x
|
8
|
+
EPS, TOO_BIG_B = 0.000001, 100_000_000
|
9
|
+
|
10
|
+
def initialize(x1, y1, x2, y2)
|
11
|
+
@vertical = nil
|
12
|
+
if x1 < x2
|
13
|
+
resolve_coefs(x1, y1, x2, y2)
|
14
|
+
else
|
15
|
+
resolve_coefs(x2, y2, x1, y1)
|
16
|
+
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
# Shorthand for constructor.
|
20
|
+
# @return [Line]
|
21
|
+
def self.for_points(p1, p2)
|
22
|
+
Line.new(p1[0], p1[1], p2[0], p2[1])
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def ==(other)
|
26
|
+
self.class.aeq(self, other) && self.class.beq(self, other)
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
# @return [String]
|
30
|
+
def to_s
|
31
|
+
if !vertical?
|
32
|
+
"(a=#{@a}, b=#{@b})"
|
33
|
+
else
|
34
|
+
"(x=#{@x})"
|
35
|
+
end
|
34
36
|
end
|
35
|
-
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
38
|
+
# Is the line strictly vertical. This line could be created from
|
39
|
+
# almost vertical pair of points (+b+ coefficient was just Too Big).
|
40
|
+
#
|
41
|
+
# If +true+, you cannot use +a+ and +b+ fields
|
42
|
+
# (you can read +x+ field instead).
|
43
|
+
def vertical?
|
44
|
+
@vertical = (nil == @b) || (TOO_BIG_B <= @b.abs) if nil == @vertical
|
45
|
+
@vertical
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
# Zero means three o'clock, +pi/2+ means noon.
|
49
|
+
# @return [Float]
|
50
|
+
def angle_rad
|
51
|
+
return (Math::PI / 2) if vertical?
|
52
|
+
Math.atan(@b)
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
55
|
+
# Zero means three o'clock, 90 means noon.
|
56
|
+
# @return [Float]
|
57
|
+
def angle_degrees
|
58
|
+
angle_rad / Math::PI * 180
|
59
|
+
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
# Does the point lie on the line.
|
62
|
+
def on_me?(x, y)
|
63
|
+
if vertical?
|
64
|
+
(@x - x).abs < EPS
|
65
|
+
else
|
66
|
+
f = @a + @b * x
|
67
|
+
(f - y).abs < on_me_eps(x)
|
68
|
+
end
|
67
69
|
end
|
68
|
-
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
def on_me_eps(x)
|
72
|
+
e = EPS
|
73
|
+
e += (EPS / @b).abs if @b != 0
|
74
|
+
e += (EPS / x).abs if x != 0
|
75
|
+
e
|
76
|
+
end
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
78
|
+
# Symmetry line of two points.
|
79
|
+
# The perpendicular line drawn through the center
|
80
|
+
# of the line segment between the points.
|
81
|
+
# @return [Line]
|
82
|
+
def self.symmetryof(x1, y1, x2, y2)
|
83
|
+
line = Line.new(x1, y1, x2, y2)
|
84
|
+
x, y = (x1 + x2) / 2, (y1 + y2) / 2
|
85
|
+
if line.vertical?
|
86
|
+
Line.new(0, y, 1, y)
|
87
|
+
else
|
88
|
+
r = line.angle_rad + (Math::PI / 2)
|
89
|
+
return Line.new(x, 0, x, 1) if Math::PI / 2 == r.abs
|
90
|
+
return Line.new(x, y, x + 1, y + Math.tan(r))
|
91
|
+
end
|
90
92
|
end
|
91
|
-
end
|
92
93
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
# Draw a parallel line through the point.
|
95
|
+
# @return [Line]
|
96
|
+
def parallel(x, y)
|
97
|
+
return Line.new(x, y, x, y + 1) if vertical?
|
98
|
+
Line.new(x, y, x + 1, y + @b)
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
+
private :on_me_eps
|
101
102
|
|
102
|
-
|
103
|
+
private
|
103
104
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
105
|
+
def resolve_coefs(xmin, yxmin, xmax, yxmax)
|
106
|
+
if 0 == (xmax - xmin).abs
|
107
|
+
@b, @a = nil, yxmin
|
108
|
+
else
|
109
|
+
@b = (yxmax - yxmin) / Float(xmax - xmin)
|
110
|
+
@a = yxmin - @b * xmin
|
111
|
+
end
|
112
|
+
@x, @a = xmin, 0 if vertical?
|
110
113
|
end
|
111
|
-
@x, @a = xmin, 0 if vertical?
|
112
|
-
end
|
113
114
|
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
def self.aeq(o1, o2)
|
116
|
+
(o1.a - o2.a).abs < (2 * EPS)
|
117
|
+
end
|
117
118
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
119
|
+
def self.beq(o1, o2)
|
120
|
+
if o1.vertical? || o2.vertical?
|
121
|
+
return false unless o1.vertical? && o2.vertical?
|
122
|
+
(o1.x - o2.x).abs < (2 * EPS)
|
123
|
+
else
|
124
|
+
(o1.b - o2.b).abs < (2 * EPS)
|
125
|
+
end
|
124
126
|
end
|
125
127
|
end
|
126
128
|
end
|
data/lib/symmetry_axis.rb
CHANGED
@@ -1,85 +1,11 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
attr_reader :line
|
8
|
-
|
9
|
-
def initialize(point_array)
|
10
|
-
@input = point_array
|
11
|
-
return if @input.size.odd?
|
12
|
-
@impossible_angles = []
|
13
|
-
@two_points_cursor = 1
|
14
|
-
loop do
|
15
|
-
return unless take_two_points # step 1
|
16
|
-
break if points_are_on_parallel_lines # step 2
|
17
|
-
end
|
18
|
-
find_axis # step 3
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def take_two_points
|
24
|
-
@two_points = [@input.first]
|
25
|
-
(@two_points_cursor...(@input.size)).each do |i|
|
26
|
-
rad = Line.for_points(@two_points[0], @input[i]).angle_rad
|
27
|
-
next if impossible?(rad)
|
28
|
-
choose_point i
|
29
|
-
return true
|
30
|
-
end
|
31
|
-
false
|
32
|
-
end
|
33
|
-
|
34
|
-
def choose_point(i)
|
35
|
-
@two_points_cursor = i + 1
|
36
|
-
@two_points[1] = @input[i]
|
37
|
-
@two_points_line = Line.for_points(@two_points[0], @input[i])
|
38
|
-
end
|
39
|
-
|
40
|
-
def impossible?(rad)
|
41
|
-
return false if @impossible_angles.empty?
|
42
|
-
impossible_rad = @impossible_angles.min_by { |e| (e - rad).abs }
|
43
|
-
(rad - impossible_rad).abs < (2 * Line.EPS)
|
44
|
-
end
|
45
|
-
|
46
|
-
def points_are_on_parallel_lines
|
47
|
-
other_points = @input.select { |p| !@two_points.include?(p) }
|
48
|
-
@axis_perpendicular_points = @two_points.dup
|
49
|
-
check_perpendiculars(other_points)
|
50
|
-
end
|
51
|
-
|
52
|
-
def check_perpendiculars(points)
|
53
|
-
loop do
|
54
|
-
break if points.empty?
|
55
|
-
line = @two_points_line.parallel(*points.first)
|
56
|
-
size0 = points.size
|
57
|
-
on_line = points.select { |p2| line.on_me?(*p2) }
|
58
|
-
points -= on_line
|
59
|
-
return false if size0 - points.size < 2
|
60
|
-
save_perpendicular_points(on_line, line)
|
61
|
-
end
|
62
|
-
true
|
63
|
-
end
|
64
|
-
|
65
|
-
def save_perpendicular_points(points, line)
|
66
|
-
return if line != @two_points_line
|
67
|
-
points += @two_points
|
68
|
-
@axis_perpendicular_points = pppair(points, line)
|
69
|
-
end
|
70
|
-
|
71
|
-
def pppair(points, line)
|
72
|
-
if line.vertical?
|
73
|
-
[points.min_by { |p| p[1] }, points.max_by { |p| p[1] }]
|
74
|
-
else
|
75
|
-
[points.min_by { |p| p[0] }, points.max_by { |p| p[0] }]
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def find_axis
|
80
|
-
p1, p2 = *@axis_perpendicular_points
|
81
|
-
@line = Line.symmetryof(p1[0], p1[1], p2[0], p2[1])
|
1
|
+
module SymmetryAxis
|
2
|
+
# Shorthand method for
|
3
|
+
# Finder.new(points).line
|
4
|
+
# @return [Line]
|
5
|
+
def self.of(points)
|
6
|
+
Finder.new(points).line
|
82
7
|
end
|
83
8
|
end
|
84
9
|
|
85
10
|
require 'symmetry_axis/line'
|
11
|
+
require 'symmetry_axis/finder'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: symmetry_axis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "Георгий Устинов"
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -62,6 +62,7 @@ extensions: []
|
|
62
62
|
extra_rdoc_files: []
|
63
63
|
files:
|
64
64
|
- lib/symmetry_axis.rb
|
65
|
+
- lib/symmetry_axis/finder.rb
|
65
66
|
- lib/symmetry_axis/line.rb
|
66
67
|
homepage: https://github.com/georgy7/symmetry_axis
|
67
68
|
licenses:
|