xrvg 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fitting.rb ADDED
@@ -0,0 +1,203 @@
1
+ #
2
+ # Fitting file. See +Fitting+
3
+ #
4
+
5
+ require 'matrix'; # for matrix inversion
6
+ require 'bezier.rb'; # for error computation
7
+
8
+ module XRVG
9
+ #
10
+ # = Fitting computation class
11
+ # == Intro
12
+ # Used to compute cubic curve fitting on a list of points (that is sampling inverse operation). Only 2D.
13
+ # == Example
14
+ # Compute the most fitting single piece bezier curve given list of points
15
+ # bezier = Fitting.compute( points )
16
+ # Compute multipieces bezier curve given list of points
17
+ # bezier = Fitting.adaptative_compute( points )
18
+ class Fitting
19
+
20
+ # compute first parameter t estimated values from length between consecutive points
21
+ def Fitting.initparameters( pointlist, parameters=nil ) #:nodoc:
22
+ lengths = [0.0]
23
+ pointlist.pairs do |p1, p2|
24
+ lengths.push( lengths[-1] + (p1-p2).r )
25
+ end
26
+ tlength = lengths[-1]
27
+ if not parameters
28
+ if not tlength == 0.0
29
+ parameters = lengths.map {|length| length / tlength}
30
+ else
31
+ parameters = [0.0] * pointlist.length
32
+ end
33
+ end
34
+ return [parameters, tlength]
35
+ end
36
+
37
+ # compute control points from polynomial bezier representation
38
+ #
39
+ # a, b, c, d are such as
40
+ # piece( t ) = at3 + bt2 + ct + d
41
+ def Fitting.bezierpiece( a, b, c, d )
42
+ p0 = d
43
+ p1 = p0 + c / 3.0
44
+ p2 = p1 + c / 3.0 + b / 3.0
45
+ p3 = p0 + c + b + a
46
+ return [p0, p1, p2, p3]
47
+ end
48
+
49
+
50
+ # Base method
51
+ #
52
+ # Given a pointlist, compute the closest matching cubic bezier curve
53
+ #
54
+ # Result is in the form [p1, pc1, pc2, p2], with [p1, pc1, pc2, p2] V2D
55
+ #
56
+ # maxerror is normalized with curve length. In case of good match (that is pointlist can be modelized by cubic bezier curve),
57
+ # result error will be under maxerror. If not, result may be above maxerror. In that case, computation is stopped because error
58
+ # no longer decrease, or because iteration is too long.
59
+ def Fitting.compute( pointlist, maxerror=0.01, maxiter=100 )
60
+ parameters, tlength = Fitting.initparameters( pointlist )
61
+ perror = 1.0
62
+ niter = 0
63
+ while true
64
+ bezier, coeffs = Fitting.iterate( pointlist, parameters )
65
+ error = Fitting.error( bezier, pointlist, parameters ) / tlength
66
+ parameters = Fitting.renormalize( bezier, coeffs, pointlist, parameters )
67
+ if (error < maxerror || (error-perror).abs < 0.00001 || niter > maxiter )
68
+ break
69
+ end
70
+ niter += 1
71
+ end
72
+ return bezier
73
+ end
74
+
75
+ # adaptative computation with automatic splitting if error not low enough, or if convergence is not fast enough
76
+ #
77
+ #
78
+ def Fitting.adaptative_compute( pointlist, maxerror=0.0001, maxiter=10, tlength=nil )
79
+ parameters, tlengthtmp = Fitting.initparameters( pointlist )
80
+ if parameters == [0] * pointlist.length
81
+ return [Bezier.single(:vector, pointlist[0], V2D::O, pointlist[0], V2D::O),0.0]
82
+ end
83
+ tlength ||= tlengthtmp
84
+ niter = 0
85
+ bezier = nil
86
+ while true
87
+ bezier, coeffs = Fitting.iterate( pointlist, parameters )
88
+ error = Fitting.error( bezier, pointlist, parameters ) / tlength
89
+ parameters = Fitting.renormalize( bezier, coeffs, pointlist, parameters )
90
+
91
+ # pointlist.length > 8 because matching with a bezier needs at least 4 points
92
+ if (niter > maxiter and error > maxerror and pointlist.length > 8)
93
+ pointlists = [pointlist[0..pointlist.length/2 - 1], pointlist[pointlist.length/2 - 1 ..-1]]
94
+ beziers = []
95
+ errors = []
96
+ pointlists.each do |subpointlist|
97
+ subbezier, suberror = Fitting.adaptative_compute( subpointlist, maxerror, maxiter, tlength )
98
+ beziers << subbezier
99
+ errors << suberror
100
+ end
101
+ bezier = beziers.sum
102
+ error = errors.max
103
+ break
104
+ elsif (error < maxerror || niter > maxiter)
105
+ break
106
+ end
107
+ perror = error
108
+ niter += 1
109
+ end
110
+ return [bezier, error]
111
+ end
112
+
113
+ # algo comes from http://www.tinaja.com/glib/bezdist.pdf
114
+ def Fitting.renormalize( bezier, coeffs, pointlist, parameters )
115
+ a3, a2, a1, a0 = coeffs
116
+ dxdu = Proc.new {|u| 3.0*a3.x*u**2 + 2.0*a2.x*u + a1.x}
117
+ dydu = Proc.new {|u| 3.0*a3.y*u**2 + 2.0*a2.y*u + a1.y}
118
+ container = V2D[]
119
+ z = Proc.new {|u,p4| p = bezier.point( u, container, :parameter ); (p.x - p4.x) * dxdu.call( u ) + (p.y - p4.y) * dydu.call( u )}
120
+ newparameters = []
121
+ [pointlist, parameters].forzip do |point, parameter|
122
+ u1 = parameter
123
+ if parameter < 0.99
124
+ u2 = parameter + 0.01
125
+ else
126
+ u2 = parameter - 0.01
127
+ end
128
+ z1 = z.call(u1,point)
129
+ z2 = z.call(u2,point)
130
+ if z1 == z2
131
+ u2 += 0.01
132
+ z2 = z.call(u2,point)
133
+ end
134
+ if z1 == z2
135
+ u2 -= 0.01
136
+ z2 = z.call(u2,point)
137
+ end
138
+ newparameters << (z2 * u1 - z1 * u2)/(z2-z1)
139
+ end
140
+ return newparameters
141
+ end
142
+
143
+ # error is max error between points in pointlist and points sampled from bezier with parameters
144
+ def Fitting.error( bezier, pointlist, parameters )
145
+ maxerror = 0.0
146
+ container = V2D[]
147
+ [pointlist, parameters].forzip do |point, parameter|
148
+ # Trace("point #{point.inspect} parameter #{parameter}")
149
+ error = (point - bezier.point( parameter, container, :parameter )).r
150
+ if error > maxerror
151
+ maxerror = error
152
+ end
153
+ end
154
+ # Trace("Fitting.error #{maxerror}")
155
+ return maxerror
156
+ end
157
+
158
+ # iterate method compute new bezier parameters from pointlist and previous bezier parameters
159
+ #
160
+ # Algo comes from http://www.tinaja.com/glib/bezdist.pdf
161
+ #
162
+ # TODO : optimized
163
+ def Fitting.iterate( pointlist, parameters )
164
+ p0 = pointlist[0]
165
+ p1 = pointlist[-1]
166
+
167
+ sumt0 = parameters.map{ |t| t**0.0 }.sum
168
+ sumt1 = parameters.map{ |t| t**1.0 }.sum
169
+ sumt2 = parameters.map{ |t| t**2.0 }.sum
170
+ sumt3 = parameters.map{ |t| t**3.0 }.sum
171
+ sumt4 = parameters.map{ |t| t**4.0 }.sum
172
+ sumt5 = parameters.map{ |t| t**5.0 }.sum
173
+ sumt6 = parameters.map{ |t| t**6.0 }.sum
174
+
175
+ psumt1 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**1.0) }.inject(V2D::O){|sum, item| sum + item}
176
+ psumt2 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**2.0) }.inject(V2D::O){|sum, item| sum + item}
177
+ psumt3 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**3.0) }.inject(V2D::O){|sum, item| sum + item}
178
+
179
+ coeff11 = sumt6 - 2 * sumt4 + sumt2
180
+ coeff12 = sumt5 - sumt4 - sumt3 + sumt2
181
+
182
+ coeff21 = coeff12
183
+ coeff22 = sumt4 - 2 * sumt3 + sumt2
184
+
185
+ result1 = (p0 - p1) * (sumt4 - sumt2) - p0 * (sumt3 - sumt1) + psumt3 - psumt1
186
+ result2 = (p0 - p1) * (sumt3 - sumt2) - p0 * (sumt2 - sumt1) + psumt2 - psumt1
187
+
188
+ matrix = Matrix[ [coeff11, coeff12], [coeff21, coeff22] ]
189
+ matrixinv = matrix.inverse
190
+ ax, bx = (matrixinv * Vector[result1.x, result2.x])[0..-1]
191
+ ay, by = (matrixinv * Vector[result1.y, result2.y])[0..-1]
192
+
193
+ a = V2D[ax, ay]
194
+ b = V2D[bx, by]
195
+ d = p0
196
+ c = p1- (a + b + p0)
197
+
198
+ piece = Fitting.bezierpiece( a, b, c, d )
199
+ return [Bezier.raw( *piece ), [a, b, c, d] ]
200
+ end
201
+ end
202
+
203
+ end # end XRVG
data/lib/frame.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frame.rb file
2
+ #
3
+ # See +Frame+
4
+ require 'attributable'
5
+
6
+ module XRVG
7
+ #
8
+ # Frame class
9
+ # = Intro
10
+ # Defines a local geometry. Used by +Curve+ interface.
11
+ # = Attributes
12
+ # attribute :center
13
+ # attribute :vector
14
+ # attribute :rotation
15
+ # attribute :scale
16
+ class Frame
17
+ include Attributable
18
+ attribute :center
19
+ attribute :vector
20
+ attribute :rotation
21
+ attribute :scale
22
+
23
+ def ==(other)
24
+ if self.center == other.center and
25
+ self.vector == other.vector and
26
+ self.rotation == other.rotation and
27
+ self.scale == other.scale
28
+ return true
29
+ end
30
+ return false
31
+ end
32
+ end
33
+ end
data/lib/geovariety.rb ADDED
@@ -0,0 +1,128 @@
1
+ # File for GeoVariety
2
+ # See (also):
3
+ # - InterBezier
4
+ # - Offsetvariety
5
+ # - FuseauVariety
6
+
7
+ require 'interbezier'
8
+
9
+ module XRVG
10
+
11
+ # = GeoVariety abstract module
12
+ # == Principle
13
+ # Base module to define geometrical spaces or canvas different from simple euclidean one to draw curves on.
14
+ # It provides three different services:
15
+ # - point computation
16
+ # - geodesic computation
17
+ # - arbitrary bezier computation, this one by computing sampling of euclidean curve on the variety, and then fitting point
18
+ # sequence with FitBezierBuilder
19
+ module GeoVariety
20
+
21
+ # must be overriden
22
+ def point( point )
23
+ raise NotImplementedError.new("#{self.class.name}#point is an abstract method.")
24
+ end
25
+
26
+ # must be overriden
27
+ def line( x1, x2, y )
28
+ raise NotImplementedError.new("#{self.class.name}#line is an abstract method.")
29
+ end
30
+
31
+ # see GeoVariety module description for algorithm
32
+ def bezier( pointrange, bezier )
33
+ bezier = bezier.similar( pointrange )
34
+ points = bezier.samples( 20 )
35
+ points = points.map {|point| self.point( point )}
36
+ return FitBezierBuilder[ :points, points ]
37
+ end
38
+ end
39
+
40
+ # = InterBezier GeoVariety implementation
41
+ # == Principle
42
+ # InterBezier defines a surface by the set of every possible curve sample from one interpolated curve to the other.
43
+ # Geodesic corresponds then to one interpolated result, and point to a point of this curve
44
+ class InterBezier
45
+ include GeoVariety
46
+
47
+ # Compute the geodesic curve by doing self.sample with y coord, and then compute point of this curve with length "x"
48
+ def point( point )
49
+ curve = self.sample( point.y )
50
+ return curve.point( point.x )
51
+ end
52
+
53
+ # Compute the geodesic subcurve with y coord between x1 and x2
54
+ def line( x1, x2, y )
55
+ # Trace("interbezier line x1 #{x1} x2 #{x2} y #{y}")
56
+ curve = self.sample( y )
57
+ result = curve.apply_split( x1, x2 )
58
+ # Trace("interbezier line result #{result.inspect}")
59
+ return result
60
+ end
61
+
62
+ end
63
+
64
+ # = OffsetVariety implementation
65
+ # == Principle
66
+ # Geovariety is defined by the set of offset curves from -ampl to +ampl
67
+ # == Extension
68
+ # Parameter could be a :samplable parameter : in that case, ampl will vary
69
+ #
70
+ # Another extension would be to parametrize range straightforwardly
71
+ #
72
+ # Finally, the two previous remarks must be synthetized :-)
73
+ class OffsetVariety
74
+ include Attributable
75
+ attribute :support
76
+ attribute :ampl, nil, Float
77
+
78
+ include GeoVariety
79
+
80
+ # builder: init static offset range with (-self.ampl..self.ampl)
81
+ def initialize( *args )
82
+ super( *args )
83
+ @range = (-self.ampl..self.ampl)
84
+ end
85
+
86
+ # point computed by computing offset curve with ampl y coord mapped onto offset range, and then sampling the curve with x coord
87
+ def point( point )
88
+ curve = Offset[ :support, @support, :ampl, @range.sample( point.y ) ]
89
+ return curve.point( point.x )
90
+ end
91
+
92
+ # subgeodesic computed by computing offset curve with ampl y coord
93
+ def line( x1, x2, y )
94
+ curve = Offset[ :support, @support, :ampl, @range.sample( y ) ]
95
+ return curve.apply_split( x1, x2 )
96
+ end
97
+
98
+ end
99
+
100
+ # = FuseauVariety implementation
101
+ # == Principle
102
+ # Same as OffsetVariety, with Fuseau shape, that is with linearly varying ampl range
103
+ class FuseauVariety
104
+ include Attributable
105
+ attribute :support
106
+ attribute :ampl, nil, Float
107
+
108
+ include GeoVariety
109
+
110
+ def initialize( *args )
111
+ super( *args )
112
+ @range = (-self.ampl..self.ampl)
113
+ end
114
+
115
+ def point( point )
116
+ curve = Offset[ :support, @support, :ampl, (0.0..@range.sample( point.y ))]
117
+ return curve.point( point.x )
118
+ end
119
+
120
+ def line( x1, x2, y )
121
+ curve = Offset[ :support, @support, :ampl, (0.0..@range.sample( y ))]
122
+ return curve.apply_split( x1, x2 )
123
+ end
124
+ end
125
+
126
+ end # XRVG
127
+
128
+ # see geovariety_test to see tests
@@ -0,0 +1,87 @@
1
+ require 'bezier'
2
+
3
+ module XRVG
4
+ class InterBezier
5
+ include Attributable
6
+ attribute :bezierlist
7
+
8
+ include Interpolation
9
+
10
+ def initialize( *args )
11
+ super( *args )
12
+ self.init_interpolation_structures
13
+ end
14
+
15
+ def init_interpolation_structures
16
+ beziers = []
17
+ indexes = []
18
+ @bezierlist.foreach do |index, bezier|
19
+ beziers.push( bezier )
20
+ indexes.push( index )
21
+ end
22
+
23
+ lengthH = {}
24
+ alllengths = []
25
+ beziers.each do |bezier|
26
+ lengths = bezier.piecelengths
27
+ Trace("bezier lengths #{lengths.inspect}")
28
+ lengthH[ bezier ] = lengths
29
+ alllengths += lengths
30
+ end
31
+ alllengths = Float.sort_float_list( alllengths )
32
+ Trace("alllengths #{alllengths.inspect}")
33
+
34
+ newbezierlist = []
35
+ beziers.each do |bezier|
36
+ newpieces = []
37
+ initlengths = lengthH[ bezier ]
38
+ alllengths.pairs do |l1, l2|
39
+ newpieces += bezier.subbezier( l1, l2 ).pieces
40
+ end
41
+ newbezier = Bezier[ :pieces, newpieces ]
42
+ newbezierlist << newbezier
43
+ end
44
+
45
+ Trace("newbezierlist #{newbezierlist.length}")
46
+ beziers = newbezierlist
47
+ bezierpointlists = beziers.map {|bezier| bezier.pointlist(:vector) }
48
+ Trace("bezierpointlists #{bezierpointlists.map {|list| list.length}.inspect}")
49
+ pointsequencelist = bezierpointlists.forzip
50
+ @interpolatorlist = []
51
+ pointsequencelist.foreach(beziers.size) do |pointsequence|
52
+ interlist = [indexes, pointsequence].forzip
53
+ @interpolatorlist.push( Interpolator.new( :samplelist, interlist ) )
54
+ end
55
+ end
56
+
57
+ def interpolate( abs, container=nil )
58
+ pieces = []
59
+ @interpolatorlist.foreach(4) do |interpiece|
60
+ piece = interpiece.map {|inter| inter.interpolate( abs )}
61
+ pieces.push( [:vector] + piece )
62
+ end
63
+ return Bezier.multi( pieces )
64
+ end
65
+
66
+ include Samplable
67
+ alias apply_sample interpolate
68
+
69
+ end
70
+
71
+ class GradientBezier < InterBezier
72
+
73
+ # TODO : does not work !!
74
+ def samples( nsamples, &block )
75
+ return super( nsamples + 1, &block )
76
+ end
77
+
78
+ def apply_samples( samples )
79
+ samples = super( samples )
80
+ result = []
81
+ samples.pairs do |bezier1, bezier2|
82
+ result.push( ClosureBezier.build( :bezierlist, [bezier1, bezier2.reverse]) )
83
+ end
84
+ return result
85
+ end
86
+ end
87
+ end # XRVG
@@ -0,0 +1,89 @@
1
+ #
2
+ # Contains Ruby geometric V2D extension to deal with intersection.
3
+ # taken from http://geometryalgorithms.com/Archive/algorithm_0108/algorithm_0108.htm
4
+ # See :
5
+ # - +V2D+
6
+
7
+ require 'geometry2D'
8
+
9
+ module XRVG
10
+
11
+ class V2D
12
+
13
+ # compute if self is Left|On|Right of the line p0 to p1
14
+ # >0 for Left, 0 for On, <0 for right
15
+ def isLeft( p0, p1 )
16
+ criteria = (p1.x - p0.x)*(self.y-p0.y) - (self.x - p0.x) * (p1.y - p0.y)
17
+ if criteria > 0.0
18
+ return :left
19
+ elsif criteria < 0.0
20
+ return :right
21
+ else
22
+ return :on
23
+ end
24
+ end
25
+
26
+ alias xyorder <=>
27
+
28
+ end
29
+
30
+ # 2D segment
31
+ class V2DS
32
+ include Attributable
33
+ attribute :p0, nil, V2D
34
+ attribute :p1, nil, V2D
35
+
36
+ # create a new 2D segment
37
+ # v = V2DS[V2D::O,V2D::X]
38
+ def V2DS.[](p0,p1)
39
+ return V2DS.new(p0,p1)
40
+ end
41
+
42
+ # initialize overloading on Attributable to speed up
43
+ def initialize(p0,p1) #:nodoc:
44
+ self.p0 = p0
45
+ self.p1 = p1
46
+ end
47
+
48
+ def left
49
+ return p0.x < p1.x ? p0 : p1
50
+ end
51
+
52
+ def right
53
+ return p0.x > p1.x ? p0 : p1
54
+ end
55
+
56
+ def V2DS.sameside?( s1, s2 )
57
+ lsign = s1.left.isLeft( s2.left, s2.right )
58
+ rsign = s1.right.isLeft( s2.left, s2.right )
59
+ if lsign == rsign and lsign != :on
60
+ return true
61
+ end
62
+ return false
63
+ end
64
+
65
+ def intersect?( other )
66
+ if V2DS.sameside?( other, self )
67
+ return false
68
+ elsif V2DS.sameside?( self, other )
69
+ return false
70
+ end
71
+ return true
72
+ end
73
+
74
+ def intersection( other )
75
+ if not self.intersect?( other )
76
+ return nil
77
+ end
78
+
79
+ x1, y1 = self.p0.coords
80
+ x2, y2 = self.p1.coords
81
+ x3, y3 = other.p0.coords
82
+ x4, y4 = other.p1.coords
83
+
84
+ newx = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4 ) ) / (( x1 - x2) * (y3 - y4) - ( y1 - y2 ) * (x3 - x4))
85
+ newy = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4 ) ) / (( x1 - x2) * (y3 - y4) - ( y1 - y2 ) * (x3 - x4))
86
+ return V2D[ newx, newy ]
87
+ end
88
+ end
89
+ end