xrvg 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/bezier.rb ADDED
@@ -0,0 +1,461 @@
1
+ # +Bezier+ source
2
+ #
3
+
4
+ require 'bezierspline'
5
+ require 'shape'
6
+
7
+ # = Base class for cubic bezier curves
8
+ # == Basics
9
+ # See http://en.wikipedia.org/wiki/B%C3%A9zier_curve
10
+ # == Examples
11
+ # Basically, a Bezier curve is a multi-pieces cubic bezier curve. As a first example, you can create bezier curves as follows :
12
+ # b = Bezier[ :pieces, [[:raw, p1, pc1, pc2, p2]] ]; # raw description, as SVG
13
+ # b = Bezier[ :pieces, [[:vector, p1, v1, p2, v2]] ]; # more "symetrical" description.
14
+ # For more extensive description, see http://xrvg.rubyforge.org/XRVGBezierCurve.html
15
+ # == Discussion
16
+ # In XRVG, bezier curves must be also viewed as a way to create smooth and non linear interpolation between values (see +Interpolation+)
17
+ #
18
+ # Other point : to run along a bezier curve, you can use two different parametrization :
19
+ # - the curve generic one, that is "curviligne" abscissa, that is length
20
+ # - the bezier parameter, as bezier curves are parametrized curves. For multi-pieces curve, by extension, parameter goes from one integer value to the next one
21
+ # As Bezier class provides several methods with a parameter input, it is necessary to specify with parameter type you want to use ! For example,
22
+ # to compute a point from a bezier curve, Bezier class defines the point method as follows :
23
+ # def point( t, parametertype=:length )
24
+ # This is a general declaration : every method with a parameter input will propose such a kind of interface :
25
+ # - t as Float parameter value
26
+ # - parametertype, by default :length, that can have two values, :length or :parameter. :parameter is kept because is far faster than other indexation.
27
+ # == Attributes
28
+ # attribute :pieces
29
+ class Bezier < Curve
30
+ attribute :pieces
31
+
32
+ # -------------------------------------------------------------
33
+ # builders
34
+ # -------------------------------------------------------------
35
+
36
+ # Initialize with the Attributable format
37
+ #
38
+ #
39
+ # Licit formats :
40
+ # b = Bezier.new( :pieces, [BezierSpline[:raw, p1, pc1, pc2, p2]] )
41
+ # b = Bezier[ :pieces, [BezierSpline[:vector, p1, v1, p2, v2]] ]
42
+ # However, prefer the use of the following builders
43
+ # b = Bezier.vector( p1, v1, p2, v2 )
44
+ # b = Bezier.raw( p1, pc1, pc2, p2 )
45
+ # b = Bezier.single( :raw, p1, pc1, pc2, p2 )
46
+ # b = Bezier.multi( [[:raw, p1, pc1, pc2, p2], [:raw, p1, pc1, pc2, p2]] )
47
+ # The two last syntaxes are provided as shortcuts, as used quite frequently, and must be used instead of :pieces attributable builder
48
+ def Bezier.[]( *args )
49
+ self.new( *args )
50
+ end
51
+
52
+ def Bezier.single( type, p1, p2, p3, p4 )
53
+ return Bezier.new( :pieces, [BezierSpline[type, p1, p2, p3, p4]] )
54
+ end
55
+
56
+ def Bezier.raw( p1, pc1, pc2, p2 )
57
+ return Bezier.single( :raw, p1, pc1, pc2, p2 )
58
+ end
59
+
60
+ def Bezier.vector( p1, v1, p2, v2 )
61
+ return Bezier.single( :vector, p1, v1, p2, v2 )
62
+ end
63
+
64
+ def Bezier.multi( rawpieces )
65
+ return Bezier.new( :pieces, rawpieces.map {|piece| BezierSpline[*piece]} )
66
+ end
67
+
68
+
69
+ # bezier point, as
70
+ # Bezier[:raw, V2D::O, V2D::O, V2D::O, V2D::O]
71
+ O = Bezier.raw( V2D::O, V2D::O, V2D::O, V2D::O )
72
+
73
+
74
+ # return piece of index "index",as BezierSpline object
75
+ #
76
+ # index can be
77
+ # - integer : in that case, simple return @pieces[index]
78
+ # - float : in that case, use second default argument
79
+ # this method must actually be very rarely called, as usually
80
+ # we want to compute something with index, and in that case we
81
+ # want to delegate computation to a BezierSpline, with parameter
82
+ # mapping parametermapping
83
+ def piece( index, parametertype=:length )
84
+ pieceindex = index
85
+ if index.is_a? Float
86
+ pieceindex, t = self.parametermapping( index, parametertype )
87
+ end
88
+ return @pieces[ pieceindex ]
89
+ end
90
+
91
+ # return number of pieces
92
+ def piecenumber
93
+ return @pieces.length
94
+ end
95
+
96
+ # -------------------------------------------------------------
97
+ # curve interface
98
+ # -------------------------------------------------------------
99
+
100
+ # overload Curve::viewbox
101
+ def viewbox
102
+ return V2D.viewbox( self.pointlist() )
103
+ end
104
+
105
+
106
+ # -------------------------------------------------------------
107
+ # piece shortcuts
108
+ # -------------------------------------------------------------
109
+
110
+ # generic method to return points list of a curve
111
+ # b = Bezier[ :pieces, [[:raw, p1, pc1, pc2, p2], [:raw, p2, pc2b, pc3, p3]] ]
112
+ # b.pointlist #=> equiv to b.pointlist(:raw)
113
+ # b.pointlist(:raw) #=> [p1, pc1, pc2, p2, p2, pc2b, pc3, p3]
114
+ # if you want to get a particular piece pointlist, do
115
+ # b.piece( t ).pointlist(nil|:raw|:vector)
116
+ # TODO : result must be cached by type
117
+ def pointlist( type=:raw )
118
+ result = []
119
+ @pieces.each {|piece| result = result + piece.pointlist(type)}
120
+ return result
121
+ end
122
+
123
+ # shortcut method to get curve first point
124
+ def firstpoint
125
+ return self.pointlist()[0]
126
+ end
127
+
128
+ # shortcut method to get curve last point
129
+ def lastpoint
130
+ return self.pointlist()[-1]
131
+ end
132
+
133
+ # shortcut method to build Bezier objects for each piece
134
+ def beziers
135
+ return self.pieces.map{ |piece| Bezier.single( *piece.data ) }
136
+ end
137
+
138
+ # -------------------------------------------------------------
139
+ # piece delegation computation
140
+ # -------------------------------------------------------------
141
+
142
+ # with index (must be Float) and parametertype as inputs, must compute :
143
+ # - the index of the piece on which the computation must take place
144
+ # - the new parameter value corresponding to bezier computation input
145
+ def parametermapping( index, parametertype=:length ) #:nodoc:
146
+ check_parametertype( parametertype )
147
+ result = []
148
+ if parametertype == :length
149
+ if index < 0.0
150
+ index = 0.0
151
+ elsif index > 1.0
152
+ index = 1.0
153
+ end
154
+ result = length_parameter_mapping( index )
155
+ else # no need to test parametertype value, as check_parametertype already do it
156
+ if index < 0.0
157
+ index = 0.0
158
+ elsif index > self.piecenumber
159
+ index = self.piecenumber.to_f
160
+ end
161
+
162
+ pieceindex = index < self.piecenumber ? index.to_i : (index-1).to_i
163
+ t = index - pieceindex
164
+ result = [pieceindex, t]
165
+ end
166
+
167
+ return result
168
+ end
169
+
170
+ # utilitary method to factorize abscissa parameter type value checking
171
+ def check_parametertype( parametertype ) #:nodoc:
172
+ if !(parametertype == :parameter or parametertype == :length )
173
+ Kernel::raise("Invalid parametertype value #{parametertype}")
174
+ end
175
+ end
176
+
177
+ # -------------------------------------------------------------
178
+ # bezier computations
179
+ # -------------------------------------------------------------
180
+
181
+ # compute a point at curviligne abscissa or parameter t
182
+ #
183
+ # curve method redefinition
184
+ def point( t, container=nil, parametertype=:length )
185
+ pieceindex, t = parametermapping( t, parametertype )
186
+ return self.piece( pieceindex ).point( t, container )
187
+ end
188
+
189
+ # compute tangent at curviligne abscissa or parameter t
190
+ #
191
+ # curve method redefinition
192
+ def tangent ( t, container=nil, parametertype=:length )
193
+ pieceindex, t = parametermapping( t, parametertype )
194
+ return self.piece( pieceindex ).tangent( t, container )
195
+ end
196
+
197
+ # compute acceleration at curviligne abscissa or parameter t
198
+ #
199
+ # curve method redefinition
200
+ def acc( t, container=nil, parametertype=:length )
201
+ pieceindex, t = parametermapping( t, parametertype )
202
+ return self.piece( pieceindex ).acc( t, container )
203
+ end
204
+
205
+ # curve method redefinition to factorize parametermapping
206
+ def frame( t, parametertype=:length )
207
+ pieceindex, t = parametermapping( t, parametertype )
208
+ tangent = self.piece( pieceindex ).tangent( t )
209
+ rotation = self.rotation( nil, tangent )
210
+ scale = self.scale( nil, tangent )
211
+ return Frame[ :center, self.piece( pieceindex ).point( t ), :vector, tangent, :rotation, rotation, :scale, scale ]
212
+ end
213
+
214
+ # -------------------------------------------------------------
215
+ # subpiece computation
216
+ # -------------------------------------------------------------
217
+
218
+ # generalize Bezier method
219
+ def subpieces (t1, t2) #:nodoc:
220
+ pieceindex1, t1 = parametermapping( t1 )
221
+ pieceindex2, t2 = parametermapping( t2 )
222
+ result = []
223
+
224
+ if pieceindex1 == pieceindex2
225
+ result = [self.piece( pieceindex1 ).subpiece( t1, t2 )]
226
+ else
227
+ result << self.piece( pieceindex1 ).subpiece( t1, 1.0 )
228
+ if pieceindex1 + 1 != pieceindex2
229
+ result += self.pieces[pieceindex1+1..pieceindex2-1]
230
+ end
231
+ result << self.piece( pieceindex1 ).subpiece( 0, t2 )
232
+ end
233
+ return result
234
+ end
235
+
236
+ # compute the sub curve between abscissa t1 and t2
237
+ #
238
+ # may result in a multi-pieces Bezier
239
+ def subbezier( t1, t2)
240
+ return Bezier.new( :pieces, self.subpieces( t1, t2 ) )
241
+ end
242
+
243
+ # split method
244
+ if nil
245
+ def split(nchildren=2, type=:regular)
246
+ return self.range.split(nchildren,type).map { |range| self.subbezier( range.begin, range.end ) }
247
+ end
248
+
249
+ def subdivise( samples )
250
+ return samples.pairs.map { |t1, t2| self.subbezier( t1, t2 ) }
251
+ end
252
+ end
253
+
254
+ # -------------------------------------------------------------
255
+ # reverse
256
+ # -------------------------------------------------------------
257
+
258
+ # return a new Bezier curve reversed from current one
259
+ #
260
+ # simply reverse BezierSpline pieces, both internally and in the :pieces list
261
+ def reverse
262
+ newpieces = @pieces.map {|piece| piece.reverse()}
263
+ return Bezier.new( :pieces, newpieces.reverse )
264
+ end
265
+
266
+ # -------------------------------------------------------------
267
+ # translation
268
+ # -------------------------------------------------------------
269
+
270
+ # translate the Bezier curve, by translating its points
271
+ def translate( v )
272
+ return Bezier.new( :pieces, @pieces.map { |piece| piece.translate( v ) } )
273
+ end
274
+
275
+ # -------------------------------------------------------------
276
+ # similar (transform is used for samplation)
277
+ # see XRVG#33
278
+ # -------------------------------------------------------------
279
+
280
+ # "Similitude" geometric transformation
281
+ #
282
+ # See http://en.wikipedia.org/wiki/Similitude_%28geometry%29
283
+ #
284
+ # Similtude geometric transformation is (firspoint..lastpoint) -> pointrange
285
+ #
286
+ # TODO : method must be put in Curve interface
287
+ def similar( pointrange )
288
+ oldRepere = [self.firstpoint, self.lastpoint - self.firstpoint]
289
+ newRepere = [pointrange.begin, pointrange.end - pointrange.begin]
290
+ rotation = V2D.angle( newRepere[1], oldRepere[1] )
291
+ if oldRepere[1].r == 0.0
292
+ Kernel::raise("similar error : bezier is length 0")
293
+ end
294
+ scale = newRepere[1].r / oldRepere[1].r
295
+ newpoints = []
296
+ self.pointlist.each do |oldpoint|
297
+ oldvector = oldpoint - oldRepere[0]
298
+ newvector = oldvector.rotate( rotation ) * scale
299
+ newpoints.push( newRepere[0] + newvector )
300
+ end
301
+ splines = []
302
+ newpoints.foreach do |p1, p2, p3, p4|
303
+ splines.push( BezierSpline[:raw, p1, p2, p3, p4] )
304
+ end
305
+ return Bezier[ :pieces, splines ]
306
+ end
307
+
308
+ # -------------------------------------------------------------
309
+ # concatenation
310
+ # -------------------------------------------------------------
311
+
312
+ # Bezier curve concatenation
313
+ def +( other )
314
+ return Bezier.new( :pieces, self.pieces + other.pieces )
315
+ end
316
+
317
+ # -------------------------------------------------------------
318
+ # range
319
+ # -------------------------------------------------------------
320
+
321
+ # TODO
322
+ def ranges #:nodoc:
323
+ (0..self.piecenumber).to_a.pairs.map {|start,stop| Range.new( start, stop )}
324
+ end
325
+
326
+ # TODO
327
+ def range #:nodoc:
328
+ return (0..self.piecenumber)
329
+ end
330
+
331
+ # -------------------------------------------------------------
332
+ # svg
333
+ # -------------------------------------------------------------
334
+
335
+ # return the svg description of the curve
336
+ #
337
+ # if firstpoint == lastpoint, curve is considered as closed
338
+ def svg()
339
+ # Trace("Bezier::svg #{self.inspect}")
340
+ path = ""
341
+ previous = nil
342
+ self.pieces().each do |piece|
343
+ p1, pc1, pc2, p2 = piece.pointlist
344
+ # Trace("previous #{previous.inspect} p1 #{p1.inspect}")
345
+ if previous == nil or not (previous - p1).r <= 0.0000001
346
+ # Trace("svg bezier not equal => M")
347
+ path += "M #{p1.x},#{p1.y}"
348
+ end
349
+ previous = p2
350
+ path += "C #{pc1.x},#{pc1.y} #{pc2.x},#{pc2.y} #{p2.x},#{p2.y}"
351
+ end
352
+
353
+ if self.firstpoint == self.lastpoint
354
+ path += " z"
355
+ end
356
+
357
+ result = "<path d=\"#{path}\"/>"
358
+ return result
359
+ end
360
+
361
+ # -------------------------------------------------------------
362
+ # gdebug
363
+ # -------------------------------------------------------------
364
+
365
+ # Display Bezier curve decorated with points and control points
366
+ def gdebug(render)
367
+ self.pieces.each {|piece| piece.gdebug(render)}
368
+ end
369
+
370
+
371
+ # -------------------------------------------------------------
372
+ # length computation
373
+ # -------------------------------------------------------------
374
+
375
+ # return the length of the bezier curve
376
+ #
377
+ # simply add pieces lengths
378
+ def length
379
+ if not @length
380
+ @length = compute_length
381
+ end
382
+ return @length
383
+ end
384
+
385
+ # Note : lengthranges building should be more functional ...
386
+ # must use an interpolator ?
387
+ def compute_length #:nodoc:
388
+ lengths = self.pieces.map {|piece| piece.length}
389
+ result = lengths.sum
390
+ if result == 0.0
391
+ lengths = [1.0]
392
+ else
393
+ psum = 0.0
394
+ lengths = lengths.map {|v| psum += v/result}
395
+ end
396
+ lmin = 0.0
397
+ @lengthranges = []
398
+ lengths.each do |llength|
399
+ @lengthranges << (lmin..llength)
400
+ lmin = llength
401
+ end
402
+ return result
403
+ end
404
+
405
+ def length_parameter_mapping( t ) #:nodoc:
406
+ if not @lengthranges
407
+ compute_length
408
+ end
409
+ pieceindex = -1
410
+ @lengthranges.each_with_index do |lrange,i|
411
+ if lrange.include?( t )
412
+ pieceindex = i
413
+ t = lrange.abscissa( t )
414
+ t = self.piece( i ).parameterfromlength( t )
415
+ break
416
+ end
417
+ end
418
+ if pieceindex == -1
419
+ Kernel::raise("length_parameter_mapping error : t #{t} is not in length range #{@lengthranges.inspect}")
420
+ end
421
+ return [pieceindex, t]
422
+ end
423
+
424
+
425
+
426
+ # -------------------------------------------------------------
427
+ # sampler computation
428
+ # -------------------------------------------------------------
429
+ include Samplable
430
+ include Splittable
431
+
432
+ # filter, sampler methods
433
+ #
434
+ # just a shortcut to define easily specific sampler on bezier curve
435
+ #
436
+ # TODO : must be defined on Curve interface !!
437
+ def filter(type=:point, &block)
438
+ if type == :length
439
+ return super(:pointbylength, &block )
440
+ else
441
+ return super(type, &block).addfilter( self.range )
442
+ end
443
+ end
444
+
445
+ def apply_split( t1, t2 ) #:nodoc:
446
+ return self.subbezier( t1, t2 )
447
+ end
448
+
449
+ alias apply_sample point
450
+ # alias apply_split subbezier
451
+ alias sampler filter
452
+
453
+ # TODO : add generic bezier builder from points : must be adaptative !! (use Fitting)
454
+ end
455
+
456
+
457
+
458
+
459
+
460
+
461
+