xrvg 0.0.1 → 0.0.2

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/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
+