xrvg 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENCE +21 -0
- data/README +88 -0
- data/Rakefile +263 -0
- data/examples/foreach.rb +9 -0
- data/examples/hellocrown.rb +7 -0
- data/examples/hellocrown2.rb +7 -0
- data/examples/hellocrownrecurse.rb +9 -0
- data/examples/helloworld.rb +5 -0
- data/examples/helloworldcompact.rb +5 -0
- data/examples/helloworldexpanded.rb +5 -0
- data/examples/palette_circle.rb +10 -0
- data/examples/sample.rb +10 -0
- data/examples/uplets.rb +9 -0
- data/lib/assertion.rb +14 -0
- data/lib/attributable.rb +152 -0
- data/lib/color.rb +295 -0
- data/lib/frame.rb +32 -0
- data/lib/geometry2D.rb +207 -0
- data/lib/interpolation.rb +59 -0
- data/lib/render.rb +269 -0
- data/lib/samplation.rb +416 -0
- data/lib/shape.rb +338 -0
- data/lib/style.rb +74 -0
- data/lib/trace.rb +28 -0
- data/lib/utils.rb +404 -0
- data/lib/xrvg.rb +37 -0
- data/test/test_attributable.rb +24 -0
- data/test/test_color.rb +79 -0
- data/test/test_frame.rb +12 -0
- data/test/test_geometry2D.rb +76 -0
- data/test/test_render.rb +18 -0
- data/test/test_style.rb +16 -0
- data/test/test_utils.rb +207 -0
- metadata +80 -0
data/lib/render.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
#
|
2
|
+
# Render file. See
|
3
|
+
# - +Render+ for render "abstraction"
|
4
|
+
# - +SVGRender+ for effective SVG render
|
5
|
+
|
6
|
+
require 'color'
|
7
|
+
require 'attributable'
|
8
|
+
require 'style'
|
9
|
+
|
10
|
+
# Render abstract class
|
11
|
+
#
|
12
|
+
# Is pretty useless for the moment
|
13
|
+
class Render
|
14
|
+
include Attributable
|
15
|
+
attr_accessor :width
|
16
|
+
attr_accessor :height
|
17
|
+
end
|
18
|
+
|
19
|
+
# SVG Render class
|
20
|
+
#
|
21
|
+
# In charge of generating a svg output file from different object passed to it
|
22
|
+
# = Use
|
23
|
+
# Canonical use of the class
|
24
|
+
# render = SVGRender[ :filename, "essai.svg" ]
|
25
|
+
# render.add( Circle[] )
|
26
|
+
# render.end
|
27
|
+
# = Improvements
|
28
|
+
# Allows also the "with" syntax
|
29
|
+
# = Attributes
|
30
|
+
# attribute :filename, "", String
|
31
|
+
# attribute :imagesize, "2cm", String
|
32
|
+
# attribute :background, Color.white, [Color, String]
|
33
|
+
class SVGRender < Render
|
34
|
+
attribute :filename, "", String
|
35
|
+
attribute :imagesize, "2cm", String
|
36
|
+
attribute :background, Color.white, [Color, String]
|
37
|
+
attr_reader :viewbox
|
38
|
+
|
39
|
+
# SVGRender builder
|
40
|
+
#
|
41
|
+
# Allows to pass a block, to avoid using .end
|
42
|
+
# SVGRender.[] do |render|
|
43
|
+
# render.add( Circle[] )
|
44
|
+
# end
|
45
|
+
def SVGRender.[](*args,&block)
|
46
|
+
result = self.new( *args )
|
47
|
+
if block
|
48
|
+
yield result
|
49
|
+
result.end
|
50
|
+
end
|
51
|
+
return result
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def initialize ( *args, &block ) #:nodoc:
|
56
|
+
super( *args )
|
57
|
+
@layers = {}
|
58
|
+
@defs = ""
|
59
|
+
@ngradients = 0
|
60
|
+
if @filename.length == 0
|
61
|
+
@filename = $0.split(".")[0..-2].join(".") + ".svg"
|
62
|
+
Trace("filename is #{filename}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def layers=( backtofront ) #:nodoc:
|
67
|
+
@sortlayers = backtofront
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_content (string, layer) #:nodoc:
|
71
|
+
if not @layers.key? layer
|
72
|
+
@layers[ layer ] = ""
|
73
|
+
end
|
74
|
+
@layers[ layer ] += string
|
75
|
+
end
|
76
|
+
|
77
|
+
def svg_template #:nodoc:
|
78
|
+
return '<?xml version="1.0" standalone="no"?>
|
79
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
80
|
+
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
81
|
+
<svg width="%SIZE%" height="%SIZE%" %VIEWBOX% version="1.1"
|
82
|
+
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
83
|
+
%DEFS%
|
84
|
+
%BACKGROUND%
|
85
|
+
%CONTENT%
|
86
|
+
</svg>'
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_def (object) #:nodoc:
|
90
|
+
@defs += object
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_gradient( gradient ) #:nodoc:
|
94
|
+
id = "gradient#{@ngradients}"
|
95
|
+
add_def( gradient.svgdef.subreplace( {"%ID%" => id} ) )
|
96
|
+
@ngradients += 1
|
97
|
+
return id
|
98
|
+
end
|
99
|
+
|
100
|
+
# render fundamental method
|
101
|
+
#
|
102
|
+
# used to render an object, with a particular style, on an optional layer.
|
103
|
+
# render.add( Circle[], Style[ :fill, Color.black ], 1 )
|
104
|
+
# If style is not provided, render asked to object its default_style, if any
|
105
|
+
def add(object, style=nil, layer=0, type=:object)
|
106
|
+
add_content( render( object, style ), layer)
|
107
|
+
refresh_viewbox( object )
|
108
|
+
end
|
109
|
+
|
110
|
+
def adds (objects) #:nodoc:
|
111
|
+
objects.each { |object| add( object )}
|
112
|
+
end
|
113
|
+
|
114
|
+
def render (object, style=nil) #:nodoc:
|
115
|
+
owidth, oheight = object.size
|
116
|
+
|
117
|
+
res = 0.0000001
|
118
|
+
if owidth < res and oheight < res
|
119
|
+
return ""
|
120
|
+
end
|
121
|
+
|
122
|
+
if not style
|
123
|
+
style = object.default_style
|
124
|
+
end
|
125
|
+
|
126
|
+
result = "<g #{style.svgline}>\n"
|
127
|
+
result += object.svg + "\n"
|
128
|
+
result += "</g>\n"
|
129
|
+
|
130
|
+
# puts "result #{result}"
|
131
|
+
|
132
|
+
if style.fill.is_a? Gradient
|
133
|
+
gradientID = add_gradient( style.fill )
|
134
|
+
result = result.subreplace( {"%fillgradient%" => "url(##{gradientID})"} )
|
135
|
+
end
|
136
|
+
|
137
|
+
if style.stroke.is_a? Gradient
|
138
|
+
gradientID = add_gradient( style.stroke )
|
139
|
+
result = result.subreplace( {"%strokegradient%" => "url(##{gradientID})"} )
|
140
|
+
end
|
141
|
+
return result
|
142
|
+
end
|
143
|
+
|
144
|
+
def viewbox #:nodoc:
|
145
|
+
return @viewbox
|
146
|
+
end
|
147
|
+
|
148
|
+
def size #:nodoc:
|
149
|
+
xmin, ymin, xmax, ymax = viewbox
|
150
|
+
return [xmax - xmin, ymax - ymin]
|
151
|
+
end
|
152
|
+
|
153
|
+
def refresh_viewbox (object) #:nodoc:
|
154
|
+
newviewbox = object.viewbox
|
155
|
+
if newviewbox.length > 0
|
156
|
+
if @viewbox == nil
|
157
|
+
@viewbox = newviewbox
|
158
|
+
else
|
159
|
+
newxmin, newymin, newxmax, newymax = newviewbox
|
160
|
+
xmin, ymin, xmax, ymax = viewbox
|
161
|
+
|
162
|
+
if newxmin < xmin
|
163
|
+
xmin = newxmin
|
164
|
+
end
|
165
|
+
if newymin < ymin
|
166
|
+
ymin = newymin
|
167
|
+
end
|
168
|
+
if newxmax > xmax
|
169
|
+
xmax = newxmax
|
170
|
+
end
|
171
|
+
if newymax > ymax
|
172
|
+
ymax = newymax
|
173
|
+
end
|
174
|
+
|
175
|
+
@viewbox = [xmin, ymin, xmax, ymax]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def get_background_svg #:nodoc:
|
181
|
+
xmin, ymin, width, height = get_carre_viewbox( get_final_viewbox() )
|
182
|
+
template = '<rect x="%x%" y="%y%" width="%width%" height="%height%" fill="%fill%"/>'
|
183
|
+
bg = self.background
|
184
|
+
if bg.is_a? Color
|
185
|
+
bg = bg.svg
|
186
|
+
end
|
187
|
+
return template.subreplace( {"%x%" => xmin,
|
188
|
+
"%y%" => ymin,
|
189
|
+
"%width%" => width,
|
190
|
+
"%height%" => height,
|
191
|
+
"%fill%" => bg} )
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_final_viewbox #:nodoc:
|
195
|
+
marginfactor = 0.2
|
196
|
+
xmin, ymin, xmax, ymax = viewbox()
|
197
|
+
width, height = size()
|
198
|
+
|
199
|
+
xcenter = (xmin + xmax)/2.0
|
200
|
+
ycenter = (ymin + ymax)/2.0
|
201
|
+
|
202
|
+
width *= 1.0 + marginfactor
|
203
|
+
height *= 1.0 + marginfactor
|
204
|
+
|
205
|
+
if width == 0.0
|
206
|
+
width = 1.0
|
207
|
+
end
|
208
|
+
if height == 0.0
|
209
|
+
height = 1.0
|
210
|
+
end
|
211
|
+
|
212
|
+
xmin = xcenter - width / 2.0
|
213
|
+
ymin = ycenter - height / 2.0
|
214
|
+
|
215
|
+
return xmin, ymin, width, height
|
216
|
+
end
|
217
|
+
|
218
|
+
def get_viewbox_svg #:nodoc:
|
219
|
+
return viewbox_svg( get_final_viewbox() )
|
220
|
+
end
|
221
|
+
|
222
|
+
def get_carre_viewbox( viewbox ) #:nodoc:
|
223
|
+
xmin, ymin, width, height = viewbox
|
224
|
+
xcenter = xmin + width / 2.0
|
225
|
+
ycenter = ymin + height / 2.0
|
226
|
+
maxsize = width < height ? height : width
|
227
|
+
return [xcenter - maxsize/2.0, ycenter - maxsize/2.0, maxsize, maxsize]
|
228
|
+
end
|
229
|
+
|
230
|
+
def viewbox_svg( viewbox ) #:nodoc:
|
231
|
+
xmin, ymin, width, height = viewbox
|
232
|
+
return "viewBox=\"#{xmin} #{ymin} #{width} #{height}\""
|
233
|
+
end
|
234
|
+
|
235
|
+
def content #:nodoc:
|
236
|
+
keys = @sortlayers ? @sortlayers : @layers.keys.sort
|
237
|
+
return keys.inject("") {|result,key| result += @layers[key]}
|
238
|
+
end
|
239
|
+
|
240
|
+
def svgdef #:nodoc:
|
241
|
+
return "<defs>\n#{@defs}\n</defs>\n"
|
242
|
+
end
|
243
|
+
|
244
|
+
def end () #:nodoc:
|
245
|
+
svgcontent = content()
|
246
|
+
svgviewbox = get_viewbox_svg()
|
247
|
+
svgbackground = get_background_svg()
|
248
|
+
|
249
|
+
content = svg_template().subreplace( {"%VIEWBOX%" => svgviewbox,
|
250
|
+
"%SIZE%" => @imagesize,
|
251
|
+
"%DEFS%" => svgdef,
|
252
|
+
"%BACKGROUND%" => svgbackground,
|
253
|
+
"%CONTENT%" => svgcontent})
|
254
|
+
|
255
|
+
File.open(filename(), "w") do |f|
|
256
|
+
f << content
|
257
|
+
end
|
258
|
+
|
259
|
+
puts "render #{filename()} OK"; # necessary for Emacs to get output name !!!!
|
260
|
+
end
|
261
|
+
|
262
|
+
def raster () #:nodoc:
|
263
|
+
# bg = background.format255
|
264
|
+
|
265
|
+
# Kernel.system( "ruby", "svg2png.rb", filename(), "2.0" )
|
266
|
+
# Kernel.system( "i_view32", filename().subreplace( ".svg" => ".png" ), "/fs" )
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
data/lib/samplation.rb
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
#
|
2
|
+
# Define float series functional processing
|
3
|
+
# The base module is FloatFunctor, used by Samplable and Splittable
|
4
|
+
#
|
5
|
+
|
6
|
+
#
|
7
|
+
# Base module to define float lists processing and computation.
|
8
|
+
# = Design principle
|
9
|
+
# Composite design pattern, to be able to chain functors
|
10
|
+
# = Concepts
|
11
|
+
# on each functor object :
|
12
|
+
# - "trigger" method triggers float processing, that is
|
13
|
+
# * if arg is integer, call "generate" to get values
|
14
|
+
# * else (array float values), just keep them
|
15
|
+
# - pass inputs to "process" method, which wraps "compute" method to be able to call block and iterate
|
16
|
+
# - "compute" method first call "modify" which is the recursive processing function
|
17
|
+
# - then call "apply" which is specific processing method of the functor
|
18
|
+
# - "apply" then dispatchs processing according to the processing "type", ie "sample" or "split"
|
19
|
+
# - by default, FloatFunctor provides "transforms" and "transform" to be used by "type" processing
|
20
|
+
module FloatFunctor
|
21
|
+
# -------------------------------------------------------------
|
22
|
+
# essential recursivity chaining method
|
23
|
+
# -------------------------------------------------------------
|
24
|
+
|
25
|
+
# building recursivity method
|
26
|
+
#
|
27
|
+
# is private ?
|
28
|
+
def addfilter( newfilter )
|
29
|
+
# Trace("Sampler addfilter method self #{self.inspect}")
|
30
|
+
if not @subfilter
|
31
|
+
@subfilter = newfilter
|
32
|
+
else
|
33
|
+
# Trace("Sampler addfilter recurse on subfilter #{@subfilter.inspect}")
|
34
|
+
@subfilter.addfilter( newfilter )
|
35
|
+
end
|
36
|
+
return self
|
37
|
+
end
|
38
|
+
|
39
|
+
# -------------------------------------------------------------
|
40
|
+
# generic generator method
|
41
|
+
# -------------------------------------------------------------
|
42
|
+
|
43
|
+
# must not be overloaded
|
44
|
+
def trigger( nsamples, type, &block )
|
45
|
+
if nsamples.is_a? Integer
|
46
|
+
indata = self.generate( nsamples )
|
47
|
+
else
|
48
|
+
indata = nsamples
|
49
|
+
end
|
50
|
+
return self.process( indata, type, &block )
|
51
|
+
end
|
52
|
+
|
53
|
+
# hook for Array
|
54
|
+
def compute( indata, type )
|
55
|
+
return self.apply( self.modify( indata ), type )
|
56
|
+
end
|
57
|
+
|
58
|
+
# hook for rand()
|
59
|
+
def process( indata, type, &block )
|
60
|
+
outdata = self.compute( indata, type )
|
61
|
+
if not block
|
62
|
+
# Trace("Samplable#trigger object #{self.inspect} indata #{indata.inspect} outdata #{outdata.inspect}")
|
63
|
+
return outdata
|
64
|
+
else
|
65
|
+
outdata.foreach(nil,&block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# recursive method to compose modifications.
|
70
|
+
#
|
71
|
+
# must not be overloaded
|
72
|
+
def modify( inputs )
|
73
|
+
# Trace("Samplable#modify object #{self.inspect} inputs #{inputs.inspect}")
|
74
|
+
if @subfilter
|
75
|
+
inputs = @subfilter.modify( inputs )
|
76
|
+
end
|
77
|
+
result = self.transforms( inputs )
|
78
|
+
# Trace("Samplable#filter object #{self.inspect} inputs #{inputs.inspect} result #{result.inspect}")
|
79
|
+
return result
|
80
|
+
end
|
81
|
+
|
82
|
+
# to be overloaded if needed
|
83
|
+
def transforms( inputs )
|
84
|
+
return inputs.map {|abs| self.transform( abs )}
|
85
|
+
end
|
86
|
+
|
87
|
+
# to be overloaded
|
88
|
+
def transform( abs )
|
89
|
+
return abs
|
90
|
+
end
|
91
|
+
|
92
|
+
# default generator method
|
93
|
+
def generate( nsamples )
|
94
|
+
# Trace("Samplable#generate object #{self.inspect} nsamples #{nsamples}")
|
95
|
+
return (0.0..1.0).generate( nsamples )
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# apply interface
|
100
|
+
#
|
101
|
+
# it is a registration interface
|
102
|
+
#
|
103
|
+
# must not be overloaded
|
104
|
+
# must be refactored with some meta hooks
|
105
|
+
def applyhash
|
106
|
+
result = {}
|
107
|
+
result[:sample] = :apply_samples
|
108
|
+
result[:split] = :apply_splits
|
109
|
+
return result
|
110
|
+
end
|
111
|
+
|
112
|
+
# must not be overloaded (apart from Samplable containers)
|
113
|
+
def apply( data, type )
|
114
|
+
@applyhash = self.applyhash
|
115
|
+
if not @applyhash.key? type
|
116
|
+
Kernel::Raise("FloatFunctor::apply no regsitration for type #{type} and object #{object.inspect}")
|
117
|
+
else
|
118
|
+
return self.send(@applyhash[type], data )
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# -------------------------------------------------------------
|
123
|
+
# filters management
|
124
|
+
# -------------------------------------------------------------
|
125
|
+
|
126
|
+
# geometric filter
|
127
|
+
def geo( speed )
|
128
|
+
return self.addfilter( Filter.with {|x| 1.0 - Math.exp(-speed * x)} )
|
129
|
+
end
|
130
|
+
|
131
|
+
# random filter
|
132
|
+
def random()
|
133
|
+
return self.addfilter( RandomFilter.new )
|
134
|
+
end
|
135
|
+
|
136
|
+
# sorting filter
|
137
|
+
def ssort()
|
138
|
+
return self.addfilter( SortFilter.new )
|
139
|
+
end
|
140
|
+
|
141
|
+
# shortcut method to build a sampler from self and a block
|
142
|
+
def filter(samplemethod=:sample,&block)
|
143
|
+
return Filter.new( self, samplemethod, &block )
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
# -------------------------------------------------------------
|
148
|
+
# old methods to be refactored
|
149
|
+
# -------------------------------------------------------------
|
150
|
+
|
151
|
+
# deprecated
|
152
|
+
#
|
153
|
+
# ratios sum must be equal to 1.0
|
154
|
+
def multisamples( nsamples, ratios )
|
155
|
+
ratiosum = ratios.sum
|
156
|
+
samplesum = nsamples.sum
|
157
|
+
ratios = ratios.map {|ratio| ratio / ratiosum}
|
158
|
+
|
159
|
+
rratios = ratios
|
160
|
+
index = 0
|
161
|
+
ratios.each do |ratio|
|
162
|
+
rratios[index] = ratio / nsamples[index]
|
163
|
+
index += 1
|
164
|
+
end
|
165
|
+
|
166
|
+
sum = 0.0
|
167
|
+
samples = [0.0]
|
168
|
+
periodindex = 0
|
169
|
+
while sum <= 1.0
|
170
|
+
sum += rratios[ periodindex ]
|
171
|
+
if sum > 1.0
|
172
|
+
break
|
173
|
+
end
|
174
|
+
samples += [sum]
|
175
|
+
periodindex += 1
|
176
|
+
if periodindex >= rratios.length
|
177
|
+
periodindex = 0.0
|
178
|
+
end
|
179
|
+
end
|
180
|
+
return self.samples( samples )
|
181
|
+
end
|
182
|
+
|
183
|
+
# deprecated
|
184
|
+
#
|
185
|
+
# TODO : must add gauss parameters
|
186
|
+
def randgauss()
|
187
|
+
begin
|
188
|
+
x1 = 2.0 * Kernel::rand - 1.0
|
189
|
+
x2 = 2.0 * Kernel::rand - 1.0
|
190
|
+
w = x1 * x1 + x2 * x2
|
191
|
+
end while w >= 1.0
|
192
|
+
w = Math.sqrt( ( -2.0 * Math.log( w ) ) / w )
|
193
|
+
return 1.0/2.0 * ( 1.0 + x1 * w )
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
# -------------------------------------------------------------
|
199
|
+
# Samplable interface
|
200
|
+
# -------------------------------------------------------------
|
201
|
+
|
202
|
+
#
|
203
|
+
# Samplable module, based on FloatFunctor
|
204
|
+
# = Concept
|
205
|
+
# Basically allows advanced item computations from an continuous "Interval" object.
|
206
|
+
# Is used by :
|
207
|
+
# - Range for computing float values
|
208
|
+
# - Curve for computing points
|
209
|
+
# - Palette for computing colors
|
210
|
+
# - ...
|
211
|
+
module Samplable
|
212
|
+
include FloatFunctor
|
213
|
+
|
214
|
+
# -------------------------------------------------------------
|
215
|
+
# sampling generator methods
|
216
|
+
# -------------------------------------------------------------
|
217
|
+
|
218
|
+
# fundamental method of the module
|
219
|
+
#
|
220
|
+
# call the FloatFunctor trigger method, with type :sample
|
221
|
+
#
|
222
|
+
# (0.0..1.0).samples( 3 ) => [0.0, 0.5, 1.0]
|
223
|
+
def samples( nsamples, &block )
|
224
|
+
return self.trigger( nsamples, :sample, &block )
|
225
|
+
end
|
226
|
+
|
227
|
+
# shortcut method to call float processing for one input
|
228
|
+
#
|
229
|
+
# is basically .samples([abs]).pop
|
230
|
+
def sample( abs )
|
231
|
+
return self.apply( self.modify( [abs] ), :sample ).pop
|
232
|
+
end
|
233
|
+
|
234
|
+
# alias for sample( 0.5 )
|
235
|
+
def mean()
|
236
|
+
return self.sample( 0.5 )
|
237
|
+
end
|
238
|
+
|
239
|
+
# alias for mean
|
240
|
+
alias middle mean
|
241
|
+
|
242
|
+
# alias for .random.samples
|
243
|
+
def rand(nsamples=1,&block)
|
244
|
+
inputs = []
|
245
|
+
nsamples.times {|v| inputs.push( Kernel::rand )}
|
246
|
+
result = self.process( inputs, :sample, &block )
|
247
|
+
return nsamples == 1 ? result[0] : result
|
248
|
+
end
|
249
|
+
|
250
|
+
# to be overloaded if needed
|
251
|
+
#
|
252
|
+
# called by FloatFunctor apply method
|
253
|
+
def apply_samples( inputs )
|
254
|
+
return inputs.map {|abs| self.apply_sample( abs ) }
|
255
|
+
end
|
256
|
+
|
257
|
+
# to be overloaded if needed
|
258
|
+
def apply_sample( abs )
|
259
|
+
return abs
|
260
|
+
end
|
261
|
+
|
262
|
+
# apply_register( :sample, :apply_samples )
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
# -------------------------------------------------------------
|
267
|
+
# Splittable interface
|
268
|
+
# -------------------------------------------------------------
|
269
|
+
|
270
|
+
#
|
271
|
+
# Splittable module, based on FloatFunctor
|
272
|
+
# = Concept
|
273
|
+
# Basically allows advanced subdivision computations from an continuous "Interval" object.
|
274
|
+
# = Example
|
275
|
+
# (1.0..2.0).splits(2) => [(1.0..1.5), (1.5..2.0)]
|
276
|
+
module Splittable
|
277
|
+
include FloatFunctor
|
278
|
+
|
279
|
+
# generator method for split interface.
|
280
|
+
# Must not be overriden
|
281
|
+
#
|
282
|
+
# Basically call FloatFunctor trigger method with at least 2 data !!
|
283
|
+
def splits( nsamples, &block )
|
284
|
+
if nsamples.is_a? Integer
|
285
|
+
nsamples += 1
|
286
|
+
if nsamples < 2
|
287
|
+
Kernel::Raise("Samplable#split method needs at least two data, instead of #{nsamples}")
|
288
|
+
end
|
289
|
+
else
|
290
|
+
if nsamples.size < 2
|
291
|
+
Kernel::Raise("Samplable#split method needs at least two data, instead of #{nsamples.inspect}")
|
292
|
+
end
|
293
|
+
end
|
294
|
+
return self.trigger( nsamples, :split, &block )
|
295
|
+
end
|
296
|
+
|
297
|
+
# must not be overriden
|
298
|
+
def split( abs1, abs2 )
|
299
|
+
return self.apply( self.filter( [abs1,abs2] ), :split ).pop
|
300
|
+
end
|
301
|
+
|
302
|
+
# to be overloaded if needed
|
303
|
+
def apply_splits( inputs )
|
304
|
+
return inputs.pairs.map {|t1,t2| self.apply_split( t1, t2 )}
|
305
|
+
end
|
306
|
+
|
307
|
+
# to be overloaded if needed
|
308
|
+
def apply_split( t1, t2 )
|
309
|
+
return self.class.new( t1, t2 )
|
310
|
+
end
|
311
|
+
|
312
|
+
# apply_register( :split, :apply_splits )
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
# Filter class
|
317
|
+
# = Intro
|
318
|
+
# Filter class allows to call generically a method with arg between 0.0..0.1 on a given object, for use in a +FloatFunctor+ context.
|
319
|
+
# = Example
|
320
|
+
# this allow to do for example
|
321
|
+
# [bezier1.filter(:point), bezier1.filter(:tangent), palette].samples( 10 ) do |point, tangent, color|
|
322
|
+
class Filter
|
323
|
+
|
324
|
+
include Samplable
|
325
|
+
|
326
|
+
# identity functor
|
327
|
+
def Filter.identity
|
328
|
+
return Filter.new(nil,nil) {|x| x}
|
329
|
+
end
|
330
|
+
|
331
|
+
# to define a filter on "object" with method "samplemethod", or with a block (exclusive)
|
332
|
+
#
|
333
|
+
# from these two alternatives, returns a proc to call for float processing
|
334
|
+
def initialize(object=nil, samplemethod=:sample, &block)
|
335
|
+
if object
|
336
|
+
# Trace("Filter::initialize object #{object.inspect} method #{samplemethod.inspect} ")
|
337
|
+
@proc = object.method( samplemethod )
|
338
|
+
else
|
339
|
+
@proc = Proc.new( &block )
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Samplable redefinition : call previous proc built from initialize to process float "abs"
|
344
|
+
def transform( abs )
|
345
|
+
result = @proc.call( abs )
|
346
|
+
# Trace("Filter::transform abs #{abs} result #{result.inspect}")
|
347
|
+
return result
|
348
|
+
end
|
349
|
+
|
350
|
+
# shortcut method aliasing Filter.new( nil, nil, &block)
|
351
|
+
#
|
352
|
+
# must change the name, because "with" seems to refer to LISP "with" design pattern, and this is not the case here
|
353
|
+
def Filter.with( &block )
|
354
|
+
return Filter.new( nil, nil, &block)
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
# RandomFilter class, to resample randomly
|
360
|
+
class RandomFilter < Filter
|
361
|
+
def initialize(*args) #:nodoc:
|
362
|
+
end
|
363
|
+
|
364
|
+
def transform(abs) #:nodoc:
|
365
|
+
return (0.0..1.0).rand
|
366
|
+
end
|
367
|
+
|
368
|
+
def transforms( inputs ) #:nodoc:
|
369
|
+
result = inputs.map { |v| self.transform( v ) }
|
370
|
+
result[0] = 0.0
|
371
|
+
result[-1] = 1.0
|
372
|
+
return result
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# SortFilter, to sort inputs (in particular from Random)
|
377
|
+
class SortFilter < Filter
|
378
|
+
def initialize(*args) #:nodoc:
|
379
|
+
end
|
380
|
+
|
381
|
+
def transforms( inputs ) #:nodoc:
|
382
|
+
return inputs.sort
|
383
|
+
end
|
384
|
+
|
385
|
+
end
|
386
|
+
|
387
|
+
# Experimental class to add discrete processing in float processing chains
|
388
|
+
# Roller["white","black"].samples(3) => ["white","black","white"]
|
389
|
+
class Roller
|
390
|
+
include Samplable
|
391
|
+
|
392
|
+
def initialize( *args ) #:nodoc:
|
393
|
+
@index = 0
|
394
|
+
@items = args
|
395
|
+
end
|
396
|
+
|
397
|
+
def Roller.[](*args) #:nodoc:
|
398
|
+
return self.new(*args)
|
399
|
+
end
|
400
|
+
|
401
|
+
def transform( abs ) #:nodoc:
|
402
|
+
result = @items[@index]
|
403
|
+
@index += 1
|
404
|
+
if @index >= @items.size
|
405
|
+
@index = 0
|
406
|
+
end
|
407
|
+
return result
|
408
|
+
end
|
409
|
+
|
410
|
+
# TODO : must be generalized
|
411
|
+
def rand() #:nodoc:
|
412
|
+
index = (0.0..@items.size).rand.to_i
|
413
|
+
return @items[index]
|
414
|
+
end
|
415
|
+
|
416
|
+
end
|