bezier_curve 0.8.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7bdfef0208f447bf444392f207505bcf076d56db
4
+ data.tar.gz: fffe1353eca9734c7b1e5a8076a0da179bfd0435
5
+ SHA512:
6
+ metadata.gz: 7b57e98d8ffcd9ac90b1d969e3728890bc654493d56006d8ec5a5d39f4386e6b6eb5252a6272bfc8c10511130b5bf7f0cbaedcfb8c62ae2779dedf9092119818
7
+ data.tar.gz: e8f69f9b96cb70bed85e8f70167247e33ecd26dd9536061c822b0773f4391b8f274303fc95958d1afc881b549d6c42c6f39d52a787fc15e615b8842bf16b6e8a
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # bezier_curve
2
+ A bézier curve library for Ruby, supporting n-dimensional, nth-degree curves
3
+
4
+ A rather simple and small library that should do pretty much anything you need to do with a bezier curve. It supports arbitrary numbers of dimensions, and arbitrary numbers of control points. Even still, it is pretty easy to use. Here's a simple quadratic (1st degree) bezier curve, in 2-dimensional space:
5
+
6
+ # create the curve
7
+ curve = BezierCurve.new([0,0],[0,1],[1,1])
8
+ # determine its points for drawing purposes
9
+ curve.points
10
+ [[0, 0], [0.00390625, 0.12109375], ... [0.87890625, 0.99609375], 1, 1]
11
+
12
+ When outputting points, it automatically chooses a suitable angle tolerance which would make the resulting poly-line appear relatively smooth (no angle changes more than about 3 degrees). But you can choose your own tolerance:
13
+
14
+ # Extreme precision, less than 1 degree tolerance
15
+ curve.points(tolerance: Math::PI/180)
16
+ # just give me 5 points, quick:
17
+ curve.points(count: 5)
18
+
19
+ You can easily split a curve into two, at a given value of `t`
20
+
21
+ # split the curve at its midpoint
22
+ curve.split_at(0.5)
23
+ #=> [BezierCurve([0, 0], [0.0, 0.5], [0.25, 0.75]), BezierCurve([0.25, 0.75], [0.5, 1.0], [1, 1])]
24
+
25
+ You can also specify n-dimensional curves:
26
+
27
+ # a 4-dimensional curve of 2nd order
28
+ curve = BezierCurve.new([0,0,0,0], [1,1,2,2], [4,5,9,3])
29
+ # get its points
30
+ curve.points(count:3)
31
+ #=> [[0.0, 0.0, 0.0, 0.0], [1.5, 1.75, 3.25, 1.75], [4.0, 5.0, 9.0, 3.0]]
32
+
33
+ You can also specify any degree/order of curve you like:
34
+
35
+ # 2d curve, 6th degree
36
+ curve = BezierCurve.new([0,0],[1,1],[2,-1],[3,1],[4,-1],[5,1],[6,-1])
37
+ curve.degree
38
+ #=> 6
39
+ curve.points.size
40
+ #=> 35
41
+
42
+ Note that the complexity goes up exponentially for higher degree curves, so performance may suffer if used extensively; but the capability is there if you need it. In fact, if you want, you can really slow things down and do a 18-dimensional curve of the 300th order. The sky is the limit. Well, your processor and memory are the limit, but you get the idea.
43
+
44
+ I am interested in making this the most useful ruby library for working with bezier curves. If you need help, or have a suggestion for improvement, feel free to file an issue on Github (https://github.com/marcuserronius/bezier_curve/), or contact me directly.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
@@ -0,0 +1,46 @@
1
+ # bezier_curve/n_point.rb
2
+
3
+ # Extends an array to treat it as an n-dimensional point. Should not
4
+ # occlude any original Array methods
5
+ module NPoint
6
+ # calculates how far this point is from `other`
7
+ def dist_from(other)
8
+ raise "Dimension counts don't match! (%i and %i)" %[size,other.size] unless size == other.size
9
+ zip(other).map{|a,b|(a-b)**2}.inject{|a,b|a+b}**0.5
10
+ end
11
+ # calculates the angle formed by this point and two others
12
+ # in radians
13
+ def angle_to(a,b)
14
+ a,b = *[a,b].map(&:to_np)
15
+ d0,d1 = dist_from(a), a.dist_from(b)
16
+ z = self.to(a,1+d1/d0)
17
+ dz = z.dist_from(b)
18
+ Math.asin(dz/2/d1)*2
19
+ end
20
+
21
+ # calculates the new point on the vector of self->other, scaled by `t`.
22
+ # t=0 returns `self`, t=.5 is the midpoint, t=1 is `other`.
23
+ def to(other, t)
24
+ self.zip(other).map{|a,b|t*(b-a)+a}.to_np
25
+ end
26
+
27
+ # convert to NPoint; returns self
28
+ def to_np
29
+ self
30
+ end
31
+
32
+ # get the x value
33
+ def x() self[0]; end
34
+ # get the y value
35
+ def y() self[1]; end
36
+ # get the z value
37
+ def z() self[2]; end
38
+ end
39
+
40
+
41
+ class Array
42
+ # add n-point functions to a copy of this array
43
+ def to_np
44
+ dup.extend NPoint
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ # bezier_curve/version.rb
2
+ # Just the version information for this library
3
+
4
+ class BezierCurve
5
+ VERSION = "0.8.0"
6
+ RELEASE_DATE = "2015-06-18"
7
+ end
@@ -0,0 +1,151 @@
1
+ # bezier_curve.rb
2
+ # A bézier curve library for Ruby, supporting n-dimensional,
3
+ # nth-degree curves.
4
+
5
+ require 'bezier_curve/version'
6
+ require 'bezier_curve/n_point'
7
+
8
+ # A bezier curve. Usage:
9
+ # c = BezierCurve.new([0,0], [0,1], [1,1])
10
+ # c.first #=> [0,0]
11
+ # c.last #=> [1,1]
12
+ # c[0.375] #=> [t=.375]
13
+ # c.points #=> an array of points that make a fairly smooth curve
14
+ # c.points(tolerance:Math::PI/20)
15
+ # #=> returns points with given tolerance for smoothness
16
+ # c.points(count: 10) #=> returns 10 points, for evenly spaced values of `t`
17
+ class BezierCurve
18
+ # create a new curve, from a list of points.
19
+ def initialize(*controls)
20
+ # check for argument errors
21
+ ZeroDimensionError.check! controls
22
+ DifferingDimensionError.check! controls
23
+ InsufficientPointsError.check! controls
24
+
25
+ @controls = controls.map(&:to_np)
26
+ end
27
+
28
+ attr_reader :controls
29
+
30
+ # the first control point
31
+ def first() controls.first; end
32
+ alias_method :start, :first
33
+ # the last control point
34
+ def last() controls.last; end
35
+ alias_method :end, :last
36
+
37
+ # the degree of the curve
38
+ def degree
39
+ controls.size - 1
40
+ end
41
+ alias_method :order, :degree
42
+ # the number of dimensions given
43
+ def dimensions
44
+ controls[0].size
45
+ end
46
+
47
+ # find the point for a given value of `t`.
48
+ def index(t)
49
+ pts = controls
50
+ while pts.size > 1
51
+ pts = (0..pts.size-2).map do |i|
52
+ pts[i].zip(pts[i+1]).map{|a,b| t*(b-a)+a}
53
+ end
54
+ end
55
+ pts[0].to_np
56
+ end
57
+ alias_method :[], :index
58
+
59
+ # divide this bezier curve into two curves, at the given `t`
60
+ def split_at(t)
61
+ pts = controls
62
+ a,b = [pts.first],[pts.last]
63
+ while pts.size > 1
64
+ pts = (0..pts.size-2).map do |i|
65
+ pts[i].zip(pts[i+1]).map{|a,b| t*(b-a)+a}
66
+ end
67
+ a<<pts.first
68
+ b<<pts.last
69
+ end
70
+ [BezierCurve.new(*a), BezierCurve.new(*b.reverse)]
71
+ end
72
+
73
+
74
+ # Returns a list of points on this curve. If you specify `count`,
75
+ # returns that many points, evenly spread over values of `t`.
76
+ # If you specify `tolerance`, no adjoining line segments will
77
+ # deviate from 180 by an angle of more than the value given (in
78
+ # radians). If unspecified, defaults to `tolerance: 1/64pi` (~3 deg)
79
+ def points(count:nil, tolerance:Math::PI/64)
80
+ if count
81
+ (0...count).map{|i| index i/(count-1.0)}
82
+ else
83
+ lines = subdivide(tolerance)
84
+ lines.map{|seg|seg.first} + [lines.last.last]
85
+ end
86
+ end
87
+
88
+ # recursively subdivides the curve until each is straight within the
89
+ # given tolerance value, in radians
90
+ def subdivide(tolerance)
91
+ if is_straight?(tolerance)
92
+ [self]
93
+ else
94
+ a,b = split_at(0.5)
95
+ a.subdivide(tolerance) + b.subdivide(tolerance)
96
+ end
97
+ end
98
+
99
+
100
+ # test this curve to see of it can be considered straight, optionally
101
+ # within the given angular tolerance, in radians
102
+ def is_straight?(tolerance)
103
+ # sanity check for tolerance in radians
104
+ if first.angle_to(index(0.5), last) <= tolerance
105
+ # maximum wavyness is `degree` - 1; split at `degree` points
106
+ pts = points(count:degree)
107
+ # size-3, because we ignore the last 2 points as starting points;
108
+ # check all angles against `tolerance`
109
+ (0..pts.size-3).all? do |i|
110
+ pts[i].angle_to(pts[i+1], pts[i+2]) < tolerance
111
+ end
112
+ end
113
+ end
114
+
115
+ # Indicates an error where the control points are in zero dimensions.
116
+ # Sounds silly, but you never know when software is generating the
117
+ # points.
118
+ class ZeroDimensionError < ArgumentError
119
+ def initialize
120
+ super "Points given must have at least one dimension"
121
+ end
122
+ def self.check! pointset
123
+ raise new.tap{|e|e.backtrace.shift} if
124
+ pointset[0].size == 0
125
+ end
126
+ end
127
+
128
+ # Indicates that the points do not all have the same number of
129
+ # dimensions, which makes them impossible to use.
130
+ class DifferingDimensionError < ArgumentError
131
+ def initialize
132
+ super "All points must have the same number of dimensions"
133
+ end
134
+ def self.check! pointset
135
+ raise new.tap{|e|e.backtrace.shift} if
136
+ pointset[1..-1].any?{|pt| pointset[0].size != pt.size}
137
+ end
138
+ end
139
+
140
+ # Indicates that there aren't enough control points; minimum of two
141
+ # for a first-degree bezier.
142
+ class InsufficientPointsError < ArgumentError
143
+ def initialize
144
+ super "All points must have the same number of dimensions"
145
+ end
146
+ def self.check! pointset
147
+ raise new.tap{|e|e.backtrace.shift} if
148
+ pointset.size <= 1
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,104 @@
1
+ # test functionality of BezierCurve class
2
+ require 'test/unit'
3
+ require 'bezier_curve'
4
+
5
+ class TestBezierCurve < Test::Unit::TestCase
6
+ # shorthand to create a bezier curve
7
+ def bc(*args)
8
+ BezierCurve.new(*args)
9
+ end
10
+
11
+ def test_degree
12
+ assert_equal 2, bc([0,0],[0,1],[1,1]).degree
13
+ assert_equal 3, bc([0,0],[0,1],[1,0],[1,1]).degree
14
+ end
15
+
16
+ def test_dimensions
17
+ assert_equal 2, bc([0,0],[0,1],[1,1]).dimensions
18
+ assert_equal 3, bc([0,0,0],[0,1,1],[1,1,2]).dimensions
19
+ end
20
+
21
+ def test_index
22
+ # simple curves that cross the origin
23
+ # degree 1
24
+ assert_equal [0,0], bc([1,1],[-1,-1])[0.5]
25
+ assert_equal [0,0,0], bc([1,1,1],[-1,-1,-1])[0.5]
26
+ assert_equal [0,0,0,0], bc([1,1,1,1],[-1,-1,-1,-1])[0.5]
27
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[-1,-1,-1,-1,-1])[0.5]
28
+ # degree 2 (quadratic)
29
+ assert_equal [0,0], bc([1,1],[0,-1],[-1,1])[0.5]
30
+ assert_equal [0,0,0], bc([1,1,1],[0,0,-1],[-1,-1,1])[0.5]
31
+ assert_equal [0,0,0,0], bc([1,1,1,1],[0,0,0,-1],[-1,-1,-1,1])[0.5]
32
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[0,0,0,0,-1],[-1,-1,-1,-1,1])[0.5]
33
+ # degree 3 (cubic)
34
+ assert_equal [0,0], bc([1,1],[-1,1],[1,-1],[-1,-1])[0.5]
35
+ assert_equal [0,0,0], bc([1,1,1],[-1,1,1],[1,-1,-1],[-1,-1,-1])[0.5]
36
+ assert_equal [0,0,0,0], bc([1,1,1,1],[-1,1,1,1],[1,-1,-1,-1],[-1,-1,-1,-1])[0.5]
37
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[-1,1,1,1,1],[1,-1,-1,-1,-1],[-1,-1,-1,-1,-1])[0.5]
38
+ end
39
+ def test_split_at
40
+ # not going to test specific points, just that the resulting curves are the same
41
+ # degree 1
42
+ c = bc([23,42],[13,7])
43
+ _c1, _c2 = *c.split_at(0.25)
44
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
45
+ # degree 2
46
+ c = bc([23,42],[13,7])
47
+ _c1, _c2 = *c.split_at(0.25)
48
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
49
+ # degree 3
50
+ c = bc([23,42,],[13,7],[12,54])
51
+ _c1, _c2 = *c.split_at(0.25)
52
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
53
+ # degree 4
54
+ c = bc([23,42],[13,7],[12,54],[103,144])
55
+ _c1, _c2 = *c.split_at(0.25)
56
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
57
+ # degree 5
58
+ c = bc([23,42],[13,7],[12,54],[103,144],[512,360])
59
+ _c1, _c2 = *c.split_at(0.25)
60
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
61
+ end
62
+ # does this really need testing? I really hope not. Oh well.
63
+ def test_points_count
64
+ # 1d,o1
65
+ [2,3,5,8,13,23].each do |n|
66
+ assert_equal n, bc([0,0],[1,1]).points(count:n).count
67
+ end
68
+ # 2d,o2
69
+ [2,3,5,8,13,23].each do |n|
70
+ assert_equal n, bc([0,0,0],[1,1,1],[2,2,2]).points(count:n).count
71
+ end
72
+ # 3d,o3
73
+ [2,3,5,8,13,23].each do |n|
74
+ assert_equal n, bc(*4.times.map{|n|Array.new(4).fill(n)}).points(count:n).count
75
+ end
76
+ # 4d,o4
77
+ [2,3,5,8,13,23].each do |n|
78
+ assert_equal n, bc(*5.times.map{|n|Array.new(5).fill(n)}).points(count:n).count
79
+ end
80
+ # 5d,o5
81
+ [2,3,5,8,13,23].each do |n|
82
+ assert_equal n, bc(*6.times.map{|n|Array.new(6).fill(n)}).points(count:n).count
83
+ end
84
+ end
85
+
86
+ def test_points_tolerance
87
+ pi = Math::PI
88
+ # just make sure all points are under tolerance
89
+ # 2d/o
90
+ p = bc([1,0],[3,2],[7,9]).points(tolerance:pi/53)
91
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
92
+ # 3d/o
93
+ p = bc([1,0,2],[3,2,5],[7,9,8],[13,17,12]).points(tolerance:pi/53)
94
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
95
+ # 4d/o
96
+ p = bc([1,0,2,-1],[3,2,5,4],[7,9,8,6],[13,17,12,15],[20,21,32,25]).points(tolerance:pi/53)
97
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
98
+ # 5d/o
99
+ p = bc([1,0,2,-1,3],[3,2,5,4,6],[7,9,8,6,5],[13,17,12,15,14],[20,21,32,25,23],[34,33,47,39,52]).points(tolerance:pi/53)
100
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
101
+ end
102
+
103
+ end
104
+
@@ -0,0 +1,51 @@
1
+ # test functionality of NPoint module
2
+ require 'test/unit'
3
+ require 'bezier_curve/n_point'
4
+
5
+ class TestNPoint < Test::Unit::TestCase
6
+ def test_to_np
7
+ np = [9,8,7].to_np
8
+
9
+ assert_kind_of NPoint, np, "Array#to_np should return an NPoint"
10
+ assert_same np.to_np, np, "NPoint#to_np should return self"
11
+ end
12
+ def test_x
13
+ np = [9,8,7].to_np
14
+ assert_equal 9, np.x
15
+ end
16
+ def test_y
17
+ np = [9,8,7].to_np
18
+ assert_equal 8, np.y
19
+ end
20
+ def test_z
21
+ np = [9,8,7].to_np
22
+ assert_equal 7, np.z
23
+ end
24
+ def test_to
25
+ a, b = [1,1,1].to_np, [5,5,5].to_np
26
+
27
+ assert_equal [3,3,3], a.to(b,0.5)
28
+ assert_equal [-1,-1,-1], a.to(b,-0.5)
29
+ assert_equal [7,7,7], a.to(b,1.5)
30
+ end
31
+ def test_dist_from
32
+ # various numbers of dimensions
33
+ assert_equal 3, [3].to_np.dist_from([6])
34
+ assert_equal (3**2*2)**0.5, [3,3].to_np.dist_from([6,6])
35
+ assert_equal (3**2*3)**0.5, [3,3,3].to_np.dist_from([6,6,6])
36
+ assert_equal (3**2*4)**0.5, [3,3,3,3].to_np.dist_from([6,6,6,6])
37
+ assert_equal (3**2*5)**0.5, [3,3,3,3,3].to_np.dist_from([6,6,6,6,6])
38
+
39
+ # across the origin
40
+ assert_in_delta 8**0.5, [1,1].to_np.dist_from([-1,-1]), 0.00001
41
+ end
42
+ def test_angle_to
43
+ pi = Math::PI
44
+ pi_eps = (pi.next_float-pi)*16
45
+ assert_in_epsilon pi/2, [1,0].to_np.angle_to([0,0],[0,1]), pi_eps
46
+ assert_in_epsilon pi, [1,1].to_np.angle_to([0,0],[1,1]), pi_eps
47
+ assert_in_epsilon 0, [1,1].to_np.angle_to([0,0],[-1,-1]), pi_eps
48
+ assert_in_epsilon pi/4, [-1,0].to_np.angle_to([0,0],[1,1]), pi_eps
49
+ end
50
+ end
51
+
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bezier_curve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Mark Hubbart
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A bézier curve library for Ruby, supporting n-dimensional, nth-degree
14
+ curves
15
+ email: mark.hubbart@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - lib/bezier_curve.rb
23
+ - lib/bezier_curve/n_point.rb
24
+ - lib/bezier_curve/version.rb
25
+ - test/test_bezier_curve.rb
26
+ - test/test_n_point.rb
27
+ homepage: https://github.com/marcuserronius/bezier_curve
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.4.5
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: N-dimensional, nth-degree bézier curves
51
+ test_files: []