xrvg 0.0.7 → 0.0.8

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.
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