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/CHANGES +34 -0
- data/README +2 -2
- data/Rakefile +111 -29
- data/examples/bezierbasic.rb +7 -0
- data/examples/bezierbasicvector.rb +7 -0
- data/examples/foreach.rb +1 -1
- data/examples/geodash.rb +8 -0
- data/examples/geodash2.rb +8 -0
- data/examples/hellocrown.rb +1 -1
- data/examples/hellocrown2.rb +1 -1
- data/examples/hellocrownrecurse.rb +1 -1
- data/examples/multibezierbasic.rb +8 -0
- data/examples/randomdash.rb +8 -0
- data/examples/range_examples.rb +16 -0
- data/examples/range_examples2.rb +10 -0
- data/examples/sample.rb +4 -2
- data/examples/simpledash.rb +8 -0
- data/examples/uplets.rb +1 -1
- data/lib/bezier.rb +461 -0
- data/lib/bezierspline.rb +266 -0
- data/lib/color.rb +44 -5
- data/lib/geometry2D.rb +116 -90
- data/lib/interpolation.rb +4 -2
- data/lib/samplation.rb +16 -10
- data/lib/shape.rb +101 -53
- data/lib/style.rb +13 -12
- data/lib/utils.rb +4 -3
- data/lib/xrvg.rb +2 -3
- data/test/test_bezier.rb +151 -0
- data/test/test_geometry2D.rb +38 -25
- data/test/test_shape.rb +67 -0
- data/test/test_utils.rb +45 -0
- metadata +28 -14
- data/examples/helloworld.rb +0 -5
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
|
+
|