rubylabs 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/spherelab.rb CHANGED
@@ -7,40 +7,86 @@ Definition of Vector and Body objects used for n-body simulations.
7
7
 
8
8
  =end
9
9
 
10
+ include Math
11
+
10
12
  module RubyLabs
11
13
 
12
14
  module SphereLab
15
+
16
+ =begin rdoc
17
+ These class variables maintain the state of the display and other miscellaneous
18
+ global values.
19
+ =end
20
+
21
+ @@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'spheres')
22
+
23
+ @@viewerOptions = {
24
+ :dotColor => '#000080',
25
+ :dotRadius => 1.0,
26
+ :origin => :center,
27
+ }
28
+
29
+ @@droppingOptions = {
30
+ :canvasSize => 400,
31
+ :mxmin => 100,
32
+ :mymin => 50,
33
+ :hmax => 100,
34
+ }
35
+
36
+ @@robotOptions = {
37
+ :canvasSize => 400,
38
+ :polygon => [4,0,0,10,4,8,8,10],
39
+ }
40
+
41
+ @@drawing = nil
42
+ @@delay = 0.1
43
+
44
+ NBodyView = Struct.new(:bodies, :origin, :scale, :options)
45
+ TurtleView = Struct.new(:turtle, :center)
46
+ MelonView = Struct.new(:bodies, :scale, :ground, :startloc, :options)
13
47
 
14
48
  =begin rdoc
15
49
  The constant G is the universal gravitational constant, assuming mass is
16
50
  in units of kilograms, distances are in meters, and time is in seconds.
17
51
  =end
18
52
 
19
- G = 6.67E-11
53
+ G = 6.67E-11
20
54
 
55
+ =begin rdoc
56
+ A Vector is a 3-tuple of (x,y,z) coordinates. Operators defined for
57
+ vector objects (where v is a vector and a is a scalar):
58
+ v == v
59
+ v + v
60
+ v - v
61
+ v * a
62
+ v.add(v) (equivalent of v += v)
63
+ v.sub(v) (equivalent of v -= v)
64
+ v.scale(a) (equivalent of v *= a)
65
+ v.norm
66
+ =end
21
67
 
22
- class Vector
23
- attr_accessor :x, :y, :z
68
+ class Vector
69
+ attr_accessor :x, :y, :z
24
70
 
25
71
  =begin rdoc
26
72
  Make a new vector with the specified x, y, and z components.
27
73
  =end
28
74
 
29
- def initialize(*args)
30
- @x, @y, @z = args
31
- end
75
+ def initialize(*args)
76
+ @x, @y, @z = args
77
+ end
32
78
 
33
- def inspect
34
- sprintf "<%s, %s, %s>", @x.to_s, @y.to_s, @z.to_s
35
- end
79
+ def inspect
80
+ sprintf "<%s, %s, %s>", @x.to_s, @y.to_s, @z.to_s
81
+ end
36
82
 
37
83
  =begin rdoc
38
84
  v1 == v2 if the three components are the same
39
85
  =end
40
86
 
41
- def ==(v)
42
- return (@x == v.x) && (@y == v.y) && (@z == v.z)
43
- end
87
+ def ==(v)
88
+ return (@x == v.x) && (@y == v.y) && (@z == v.z)
89
+ end
44
90
 
45
91
  =begin rdoc
46
92
  Arithmetic methods are invoked for a vector v1 when Ruby evaluates an expression of the
@@ -49,51 +95,84 @@ class Vector
49
95
  is a vector-scalar multiplication.
50
96
  =end
51
97
 
52
- def +(v)
53
- Vector.new(@x + v.x, @y + v.y, @z + v.z)
54
- end
98
+ def +(v)
99
+ Vector.new(@x + v.x, @y + v.y, @z + v.z)
100
+ end
55
101
 
56
- def -(v)
57
- Vector.new(@x - v.x, @y - v.y, @z - v.z)
58
- end
102
+ def -(v)
103
+ Vector.new(@x - v.x, @y - v.y, @z - v.z)
104
+ end
59
105
 
60
- def *(a)
61
- Vector.new(@x * a, @y * a, @z * a)
62
- end
106
+ def *(a)
107
+ Vector.new(@x * a, @y * a, @z * a)
108
+ end
63
109
 
64
110
  =begin rdoc
65
111
  v1.add(v2) adds the components of v2 to v1 -- would be v1 += v2 if Ruby
66
112
  allowed us to overload +=
67
113
  =end
68
114
 
69
- def add(v)
70
- @x += v.x
71
- @y += v.y
72
- @z += v.z
73
- self
74
- end
115
+ def add(v)
116
+ @x += v.x
117
+ @y += v.y
118
+ @z += v.z
119
+ self
120
+ end
75
121
 
76
122
  =begin rdoc
77
123
  v1.sub(v2) subtracts the components of v2 from v1 -- would be v1 -= v2 if Ruby
78
124
  allowed us to overload -=
79
125
  =end
80
126
 
81
- def sub(v)
82
- @x -= v.x
83
- @y -= v.y
84
- @z -= v.z
85
- self
86
- end
127
+ def sub(v)
128
+ @x -= v.x
129
+ @y -= v.y
130
+ @z -= v.z
131
+ self
132
+ end
133
+
134
+ =begin rdoc
135
+ v.scale(a) -- multiply each element in v by scalar a
136
+ =end
137
+
138
+ def scale(a)
139
+ @x *= a
140
+ @y *= a
141
+ @z *= a
142
+ end
87
143
 
88
144
  =begin rdoc
89
145
  The magnitude of a vector is the Euclidean norm: sqrt(x**2 + y**2 + z**2)
90
146
  =end
91
147
 
92
- def norm
93
- Math.sqrt(@x*@x + @y*@y + @z*@z)
94
- end
148
+ def norm
149
+ sqrt(@x*@x + @y*@y + @z*@z)
150
+ end
151
+
152
+ =begin rdoc
153
+ Compute the angle between this vector and v -- used when drawing 2D vectors, so
154
+ the z dimension is ignored
155
+ =end
156
+
157
+ def angle(v)
158
+ acos((@x * v.x + @y * v.y) / (norm * v.norm))
159
+ end
160
+
161
+ =begin rdoc
162
+ Accessor functions for the coordinates as a vector of three floats
163
+ =end
164
+
165
+ def coords
166
+ [@x, @y, @z]
167
+ end
168
+
169
+ def coords=(a)
170
+ @x = a[0]
171
+ @y = a[1]
172
+ @z = a[2]
173
+ end
95
174
 
96
- end # Vector
175
+ end # Vector
97
176
 
98
177
 
99
178
  =begin rdoc
@@ -103,8 +182,9 @@ end # Vector
103
182
  the visualization methods.
104
183
  =end
105
184
 
106
- class Body
107
- attr_accessor :mass, :position, :velocity, :force, :name, :size, :color
185
+ class Body
186
+ attr_accessor :mass, :position, :velocity, :force
187
+ attr_accessor :name, :size, :color, :prevx, :prevy, :graphic
108
188
 
109
189
  =begin rdoc
110
190
  The constructor can be called with a variety of different arguments:
@@ -118,233 +198,510 @@ class Body
118
198
  * pass no parameters to get a body with all attributes set to 0
119
199
  =end
120
200
 
121
- def initialize(*args)
122
- if args[1].class == Vector
123
- @mass, @position, @velocity, @name = args
124
- elsif args[0] == :random
125
- @mass = rand(10000) * 1e6
126
- @position = Vector.new(rand(300)-150, rand(300)-150, 0)
127
- @velocity = Vector.new(rand(20)-10, rand(10)-5, 0)
128
- @name = "b" + self.object_id.to_s
129
- elsif args[0].is_a?(Numeric) && args[1].is_a?(Numeric)
130
- @mass = args[0]
131
- @position = Vector.new(args[1], 0.0, 0.0)
132
- @velocity = Vector.new(0.0, 0.0, 0.0)
133
- @name = args[2]
134
- @linear = true
135
- else
136
- @mass = 0.0
137
- @position = Vector.new(0.0, 0.0, 0.0)
138
- @velocity = Vector.new(0.0, 0.0, 0.0)
139
- @name = nil
201
+ def initialize(*args)
202
+ if args[1].class == Vector
203
+ @mass, @position, @velocity, @name = args
204
+ elsif args[0] == :random
205
+ @mass = rand(10000) * 1e6
206
+ @position = Vector.new(rand(300)-150, rand(300)-150, 0)
207
+ @velocity = Vector.new(rand(20)-10, rand(10)-5, 0)
208
+ @name = "b" + self.object_id.to_s
209
+ elsif args[0].is_a?(Numeric) && args[1].is_a?(Numeric)
210
+ @mass = args[0]
211
+ @position = Vector.new(args[1], 0.0, 0.0)
212
+ @velocity = Vector.new(0.0, 0.0, 0.0)
213
+ @name = args[2]
214
+ @linear = true
215
+ else
216
+ @mass = 0.0
217
+ @position = Vector.new(0.0, 0.0, 0.0)
218
+ @velocity = Vector.new(0.0, 0.0, 0.0)
219
+ @name = nil
220
+ end
221
+ @force = Vector.new(0.0, 0.0, 0.0)
140
222
  end
141
- @force = Vector.new(0.0, 0.0, 0.0)
142
- end
143
223
 
144
- def inspect
145
- s = ""
146
- s << @name + ": " if @name
147
- s << @mass.to_s + "g "
148
- if @linear
149
- s << "x: " + @position.x.to_s
150
- else
151
- s << @position.inspect + " " + @velocity.inspect
224
+ def inspect
225
+ s = ""
226
+ s << @name + ": " if @name
227
+ s << @mass.to_s + "g "
228
+ if @linear
229
+ s << "x: " + @position.x.to_s
230
+ else
231
+ s << @position.inspect + " " + @velocity.inspect
232
+ end
233
+ return s
152
234
  end
153
- return s
154
- end
155
235
 
156
236
  =begin rdoc
157
237
  Reset the force vector to 0 (to get ready for a new round of interaction calculations).
158
238
  =end
159
239
 
160
- def clear_force
161
- @force.x = 0.0
162
- @force.y = 0.0
163
- @force.z = 0.0
164
- end
240
+ def clear_force
241
+ @force.x = 0.0
242
+ @force.y = 0.0
243
+ @force.z = 0.0
244
+ end
165
245
 
166
246
  =begin rdoc
167
247
  Compute force acting on this body wrt body b
168
248
  =end
169
249
 
170
- def add_force(b)
171
- r = @position - b.position
172
- nr = r.norm ** 3
173
- mr = b.mass / nr
174
- @force.add(r * mr)
175
- end
250
+ def add_force(b)
251
+ r = @position - b.position
252
+ nr = r.norm ** 3
253
+ mr = b.mass / nr
254
+ @force.add(r * mr)
255
+ end
176
256
 
177
257
  =begin rdoc
178
258
  Move this body by applying current force vector for dt seconds
179
259
  =end
180
260
 
181
- def move(dt)
182
- acc = @force * G * -1.0
183
- @velocity.add( acc * dt )
184
- @position.add( @velocity * dt )
185
- end
261
+ def move(dt)
262
+ acc = @force * G * -1.0
263
+ @velocity.add( acc * dt )
264
+ @position.add( @velocity * dt )
265
+ end
186
266
 
187
267
  =begin rdoc
188
268
  Class method to compute the interaction between bodies b1 and b2 and update
189
269
  their force vectors.
190
270
  =end
191
271
 
192
- def Body.interaction(b1, b2)
193
- r = b1.position - b2.position
194
- a = r.norm ** 3
195
- b1.force.add(r * (b2.mass / a))
196
- b2.force.add(r * (-b1.mass / a))
272
+ def Body.interaction(b1, b2)
273
+ r = b1.position - b2.position
274
+ a = r.norm ** 3
275
+ b1.force.add(r * (b2.mass / a))
276
+ b2.force.add(r * (-b1.mass / a))
277
+ end
278
+
279
+ end # Body
280
+
281
+ =begin rdoc
282
+ A class for rudimentary Turtle graphics. All we need is enough functionality to
283
+ implement a robot that draws a circle by moving forward, then correcting its direction.
284
+ Attributes: location and velocity vectors.
285
+ =end
286
+
287
+ class Turtle
288
+
289
+ attr_accessor :position, :velocity, :graphic
290
+
291
+ @@turtleOptions = {
292
+ :x => 20,
293
+ :y => 100,
294
+ :heading => 0,
295
+ :speed => 10,
296
+ }
297
+
298
+ @@north = Vector.new(0, 10, 0)
299
+
300
+ def initialize(args = nil)
301
+ args = { } if args.nil?
302
+ raise "Turtle: initialize: args must be a Hash" unless args.class == Hash
303
+ options = @@turtleOptions.merge(args)
304
+ alpha = Canvas.radians(options[:heading] + 90.0)
305
+ @position = Vector.new(options[:x], options[:y], 0.0)
306
+ @velocity = Vector.new(-1.0 * options[:speed] * cos(alpha), options[:speed] * sin(alpha), 0.0)
307
+ @graphic = nil
308
+ end
309
+
310
+ def inspect
311
+ sprintf "#<SphereLab::Turtle x: %d y: %d heading: %d speed: %.2f>", @position.x, @position.y, heading, speed
312
+ end
313
+
314
+ def location
315
+ return [@position.x, @position.y]
316
+ end
317
+
318
+ def heading
319
+ d = Canvas.degrees( @velocity.angle(@@north) )
320
+ return (@velocity.x < 0) ? (360 - d) : d
321
+ end
322
+
323
+ def speed
324
+ @velocity.norm
325
+ end
326
+
327
+ def turn(alpha)
328
+ theta = -1.0 * Canvas.radians(alpha)
329
+ x = @velocity.x * cos(theta) - @velocity.y * sin(theta)
330
+ y = @velocity.x * sin(theta) + @velocity.y * cos(theta)
331
+ @velocity.x = x
332
+ @velocity.y = y
333
+ if graphic
334
+ Canvas.rotate(graphic,alpha)
335
+ Canvas.sync
336
+ end
337
+ return alpha
338
+ end
339
+
340
+ def advance(dt)
341
+ prevx = @position.x
342
+ prevy = @position.y
343
+ @position.add( @velocity * dt )
344
+ if graphic
345
+ Canvas.move(graphic, @position.x-prevx, prevy-@position.y, :track)
346
+ Canvas.sync
347
+ end
348
+ return dt
349
+ end
350
+ end
351
+
352
+ =begin rdoc
353
+ Set up an experiment with a robot/turtle. Initialize the canvas, make the turtle,
354
+ assign it a graphic. These methods are all defined with respect to a canvas, and
355
+ they will print an error message if the experiment has not been set up with a call
356
+ to init_robot (which sets @@drawing).
357
+ =end
358
+
359
+ def view_robot(userOptions = {})
360
+ options = @@robotOptions.merge(userOptions)
361
+ poly = options[:polygon].clone
362
+ edge = options[:canvasSize]
363
+ (0...poly.length).step(2) do |i|
364
+ poly[i] += edge/10
365
+ poly[i+1] += edge/2
366
+ end
367
+ Canvas.init(edge, edge, "SphereLab")
368
+ turtle = Turtle.new( :x => edge/10, :y => edge/2, :heading => 0, :speed => 10 )
369
+ turtle.graphic = Canvas.polygon(poly, :outline => 'black', :fill => '#00ff88')
370
+ Canvas.circle( edge/2, edge/2, 3, :fill => 'darkblue' )
371
+ @@drawing = TurtleView.new( turtle, [edge/2, edge/2] )
372
+ Canvas.sync
373
+ return true
374
+ end
375
+
376
+ def robot
377
+ if @@drawing.nil?
378
+ puts "No robot; call view_robot to initialize"
379
+ return nil
380
+ else
381
+ return @@drawing.turtle
382
+ end
383
+ end
384
+
385
+ # TODO -- aim toward point on circle at original radius, not current radius...
386
+
387
+ def orient_robot
388
+ r = robot
389
+ mx, my = @@drawing.center
390
+ tx, ty = r.position.x, r.position.y
391
+ mid = Vector.new(tx-my, ty-my, 0.0)
392
+ theta = Canvas.degrees(mid.angle(r.velocity))
393
+ alpha = 2 * (90.0 - theta)
394
+ r.turn(alpha)
395
+ Canvas.sync
396
+ return alpha
197
397
  end
198
398
 
199
- end # Body
399
+ # :begin :make_circle
400
+ def make_circle(nseg, time, angle)
401
+ view_robot
402
+ turn_robot( angle/2 )
403
+ nseg.times { advance_robot(time); turn_robot(angle) }
404
+ turn_robot( -angle/2 )
405
+ return true
406
+ end
407
+ # :end :make_circle
408
+
200
409
 
201
410
  =begin rdoc
202
- Make a list of Body objects using data in the file +fn+
411
+ These methods create a set of bodies with random size, location, and initial
412
+ velocity.
203
413
  =end
204
414
 
205
- def read_bodies(fn)
206
- bodies = []
207
- File.open(fn).each do |line|
208
- line.strip!
209
- next if line.length == 0
210
- next if line[0] == ?#
211
- a = line.chomp.split
212
- for i in 1..7
213
- a[i] = a[i].to_f
415
+ def random_vectors(r, i, n)
416
+ theta = (2 * PI / n) * i + (PI * rand / n)
417
+ radius = r + (r/3)*(rand-0.5)
418
+ x = radius * cos(theta)
419
+ y = radius * sin(theta)
420
+ vtheta = (PI - theta) * -1 + PI * (rand-0.5)
421
+ vx = radius/20 * cos(vtheta)
422
+ vy = radius/20 * sin(vtheta)
423
+ return Vector.new(x, y, 0), Vector.new(vx, vy, 0)
424
+ end
425
+
426
+ def random_velocity(v, r)
427
+ res = Vector.new(-r * cos)
428
+ end
429
+
430
+ def random_bodies(n, big = 1)
431
+ res = []
432
+ mm = 1e12 # average mass
433
+ mr = 150 # average distance from origin
434
+ # write_tuple( :scale, [ 1.0 ] )
435
+ big.times do |i|
436
+ r0, v0 = random_vectors(mr/2, i, big)
437
+ res << Body.new(mm*100/big, r0, v0)
438
+ # write_tuple( :view, [i, 10, "#0080ff"] )
439
+ end
440
+ (n-big).times do |i|
441
+ r, v = random_vectors(mr, i, n-big)
442
+ b = Body.new(mm, r, v)
443
+ # write_tuple( :view, [i+big, 5, "#0080ff"] )
444
+ res << b
214
445
  end
215
- b = Body.new( a[1], Vector.new(a[2],a[3],a[4]), Vector.new(a[5],a[6],a[7]), a[0] )
216
- b.size = a[-2].to_i
217
- b.color = a[-1]
218
- bodies << b
446
+ res.each_with_index { |b,i| b.name = "b#{i}"; b.color = '#0080ff'; b.size = 5 }
447
+ return res
219
448
  end
220
- return bodies
221
- end
449
+
450
+ =begin rdoc
451
+ Initialize a new n-body system, returning an array of body objects. The
452
+ parameter is the name of a predefined system, the name of a file, or the
453
+ symbol :random.
454
+ =end
222
455
 
456
+ def make_system(*args)
457
+ bodies = []
458
+ begin
459
+ raise "usage: make_system(id)" unless args.length > 0
460
+ if args[0] == :random
461
+ raise "usage: make_system(:random, n)" unless args.length == 2 && args[1].class == Fixnum
462
+ return random_bodies(args[1])
463
+ end
464
+ filename = args[0]
465
+ if filename.class == Symbol
466
+ filename = File.join(@@dataDirectory, filename.to_s + ".txt")
467
+ end
468
+ File.open(filename).each do |line|
469
+ line.strip!
470
+ next if line.length == 0
471
+ next if line[0] == ?#
472
+ a = line.chomp.split
473
+ for i in 1..7
474
+ a[i] = a[i].to_f
475
+ end
476
+ b = Body.new( a[1], Vector.new(a[2],a[3],a[4]), Vector.new(a[5],a[6],a[7]), a[0] )
477
+ b.size = a[-2].to_i
478
+ b.color = a[-1]
479
+ bodies << b
480
+ end
481
+ rescue
482
+ puts "error: #{$!}"
483
+ return nil
484
+ end
485
+ return bodies
486
+ end
487
+
488
+ =begin rdoc
489
+ Initialize the drawing canvas by drawing a circle for each body in list b.
490
+ =end
491
+
492
+ def view_system(blist, userOptions = {})
493
+ Canvas.init(700, 700, "SphereLab")
494
+ options = @@viewerOptions.merge(userOptions)
495
+ origin = setOrigin(options[:origin])
496
+ sf = setScale(blist, options[:origin], options[:scale])
497
+ blist.each do |b|
498
+ x, y = scale(b.position, origin, sf)
499
+ circle = Canvas.circle(x, y, b.size, :fill => b.color)
500
+ b.graphic = circle
501
+ b.prevx = x
502
+ b.prevy = y
503
+ end
504
+ @@drawing = NBodyView.new(blist, origin, sf, options)
505
+ if options.has_key?(:dash)
506
+ options[:dashcount] = 0
507
+ options[:pendown] = :track
508
+ elsif options.has_key?(:pendown)
509
+ options[:pendown] = :track
510
+ end
511
+ Canvas.sync
512
+ return true
513
+ end
514
+
223
515
  =begin rdoc
224
516
  Call SphereLab::step(bodies, time) to calculate the pairwise interactions between all
225
517
  Body objects in the array +bodies+ and then compute their new positions after +time+ seconds.
226
518
  =end
227
519
 
228
- # :begin :step
229
- def step(bodies, time)
230
- nb = bodies.length
520
+ # :begin :step_system
521
+ def step_system(bodies, time)
522
+ nb = bodies.length
231
523
 
232
- for i in 0...nb # compute all pairwise interactions
233
- for j in (i+1)...nb
234
- Body.interaction( bodies[i], bodies[j] )
524
+ for i in 0...nb # compute all pairwise interactions
525
+ for j in (i+1)...nb
526
+ Body.interaction( bodies[i], bodies[j] )
527
+ end
235
528
  end
236
- end
237
529
 
238
- bodies.each do |b|
239
- b.move(time) # apply the accumulated forces
240
- b.clear_force # reset force to 0 for next round
241
- end
242
- end
243
- # :end :step
244
-
245
- def random_vectors(r, i, n)
246
- theta = (2 * Math::PI / n) * i + (Math::PI * rand / n)
247
- radius = r + (r/3)*(rand-0.5)
248
- x = radius * Math.cos(theta)
249
- y = radius * Math.sin(theta)
250
- vtheta = (Math::PI - theta) * -1 + Math::PI * (rand-0.5)
251
- vx = radius/20 * Math.cos(vtheta)
252
- vy = radius/20 * Math.sin(vtheta)
253
- return Vector.new(x, y, 0), Vector.new(vx, vy, 0)
254
- end
255
-
256
- def random_velocity(v, r)
257
- res = Vector.new(-r * Math.cos)
258
- end
259
-
260
- def random_bodies(n, big = 1)
261
- res = []
262
- mm = 1e12 # average mass
263
- mr = 150 # average distance from origin
264
- write_tuple( :scale, [ 1.0 ] )
265
- big.times do |i|
266
- r0, v0 = random_vectors(mr/2, i, big)
267
- res << Body.new(mm*100/big, r0, v0)
268
- write_tuple( :view, [i, 10, "#0080ff"] )
530
+ bodies.each do |b|
531
+ b.move(time) # apply the accumulated forces
532
+ b.clear_force # reset force to 0 for next round
533
+ end
269
534
  end
270
- (n-big).times do |i|
271
- r, v = random_vectors(mr, i, n-big)
272
- b = Body.new(mm, r, v)
273
- b.name = "b" + (i+1).to_s
274
- write_tuple( :view, [i+big, 5, "#0080ff"] )
275
- res << b
535
+ # :end :step_system
536
+
537
+ =begin rdoc
538
+ After making a canvas with view_system, call this method to move the bodies on the canvas.
539
+ =end
540
+
541
+ def update_system(bodies, dt)
542
+ return false unless @@drawing
543
+ step_system(bodies, dt)
544
+ if @@drawing.options.has_key?(:dash)
545
+ @@drawing.options[:dashcount] = (@@drawing.options[:dashcount] + 1) % @@drawing.options[:dash]
546
+ if @@drawing.options[:dashcount] == 0
547
+ @@drawing.options[:pendown] = @@drawing.options[:pendown].nil? ? :track : nil
548
+ end
549
+ end
550
+ bodies.each do |b|
551
+ next unless b.graphic
552
+ newx, newy = scale(b.position, @@drawing.origin, @@drawing.scale)
553
+ Canvas.move(b.graphic, newx-b.prevx, newy-b.prevy, @@drawing.options[:pendown])
554
+ b.prevx = newx
555
+ b.prevy = newy
556
+ end
557
+ Canvas.sync
558
+ return true
276
559
  end
277
- return res
278
- end
279
-
280
- def urey_hall
281
- write_tuple( :scale, [ 1e-7 ] )
282
- # a 6.6 lb watermelon 7 stories (84 feet) above the surface of the earth:
283
- w = Body.new(3000, 6.37101e6 + 25)
284
- w.name = "watermelon"
285
- w.size = 2
286
- w.color = "#ff6666"
287
- write_tuple( :view, [1, w.size, w.color] )
288
- # the earth:
289
- e = Body.new(5.9736E+24, 0)
290
- e.name = "earth"
291
- e.size = 100
292
- e.color = "#004080"
293
- write_tuple( :view, [0, e.size, e.color] )
294
- return [e, w]
295
- end
296
-
297
- def four_body
298
- write_tuple( :scale, [ 0.5 ] )
299
- a = Body.new(1.4e18, Vector.new(50, 70, 0), Vector.new(0, 1, 0))
300
- write_tuple( :view, [0, 10, "#ff6666"] )
301
- b = Body.new(1.2e16, Vector.new(50, 225, 0), Vector.new(-1000, 2, 0))
302
- write_tuple( :view, [1, 3, "#ff6666"] )
303
- c = Body.new(2.1e16, Vector.new(175, -75, 0), Vector.new(-1, -5, 0))
304
- write_tuple( :view, [2, 5, "#ff6666"] )
305
- d = Body.new(4.5e16, Vector.new(300, -75, 0), Vector.new(-5, 2, 0))
306
- write_tuple( :view, [3, 6, "#ff6666"] )
307
- return [a,b,c,d]
308
- end
309
-
310
- =begin rdoc
311
- Attach a probe to the step method, have it call this method to show the
312
- current state of the simulation.
313
- =end
314
-
315
- def display(bodies)
316
- for i in 0...bodies.length
317
- write_tuple( :body, [i, bodies[i].position.x, bodies[i].position.y] )
560
+
561
+ =begin rdoc
562
+ Map a simulation's (x,y) coordinates to screen coordinates using origin
563
+ and scale factor
564
+ =end
565
+
566
+ def scale(vec, origin, sf)
567
+ loc = vec.clone
568
+ loc.scale(sf)
569
+ loc.add(origin)
570
+ return loc.x, loc.y
318
571
  end
319
- write_tuple( :timestep, [] )
320
- return true
321
- end
572
+
573
+ =begin rdoc
574
+ Make a vector that defines the location of the origin (in pixels).
575
+ =end
322
576
 
577
+ def setOrigin(type)
578
+ case type
579
+ when :center
580
+ Vector.new( Canvas.width/2, Canvas.height/2, 0)
581
+ else
582
+ Vector.new(0,0,0)
583
+ end
584
+ end
585
+
323
586
  =begin rdoc
324
- Call this method to create a "scope" for viewing the state of an n-body simulation.
325
- Also creates a proc that will be executed when IRB exits to shut down the tuple
326
- space and viewer.
587
+ Set the scale factor. Use the parameter passed by the user, or find the
588
+ largest coordinate in a list of bodies. Add 20% for a margin
327
589
  =end
328
590
 
329
- def make_viewer
330
- include Viewer
331
- launch_viewer("nbview.rb")
332
- end
591
+ def setScale(blist, origin, scale)
592
+ if scale == nil
593
+ dmax = 0.0
594
+ blist.each do |b|
595
+ b.position.coords.each { |val| dmax = val.abs if val.abs > dmax }
596
+ end
597
+ else
598
+ dmax = scale
599
+ end
600
+ sf = (origin == :center) ? (Canvas.width / 2.0) : Canvas.width
601
+ return (sf / dmax) * 0.8
602
+ end
603
+
604
+ =begin rdoc
605
+ Initialize the drawing for a "2-body system" with a small object and the
606
+ earth. The parameters are the list of Body objects and viewing options.
607
+ =end
608
+
609
+ def view_melon(blist, userOptions = {})
610
+ options = @@droppingOptions.merge(userOptions)
611
+ options[:dash] = 1
612
+ options[:dashcount] = 0
613
+ options[:pendown] = :track
614
+ edge = options[:canvasSize]
615
+ mxmin = options[:mxmin]
616
+ mymin = options[:mymin]
617
+ hmax = options[:hmax]
618
+ Canvas.init(edge, edge, "SphereLab")
619
+ earth = Canvas.circle(200, 2150, 1800, :fill => blist[1].color)
620
+ blist[1].graphic = earth
621
+ mymax = earth.coords[1]
622
+ melon = Canvas.circle(mxmin, mymax, 5, :fill => blist[0].color)
623
+ blist[0].graphic = melon
624
+ scale = (mymax-mymin) / hmax.to_f
625
+ @@drawing = MelonView.new(blist, scale, blist[0].position.y, melon.coords, options)
626
+ Canvas.sync
627
+ return true
628
+ end
629
+
630
+ =begin rdoc
631
+ Position the melon (body 0) at a specified height. If no drawing, save the current
632
+ position in prevy, but if drawing, get the earth's surface from the drawing (this allows
633
+ students to move the melon up or down in the middle of an experiment)
634
+ =end
635
+
636
+ def position_melon(blist, height)
637
+ melon = blist[0]
638
+ if @@drawing
639
+ if height > @@drawing.options[:hmax]
640
+ puts "max height: #{@@drawing.options[:hmax]}"
641
+ return false
642
+ end
643
+ melon.prevy = @@drawing.ground
644
+ melon.position.y = @@drawing.ground + height
645
+ a = @@drawing.startloc.clone
646
+ a[1] -= height * @@drawing.scale
647
+ a[3] -= height * @@drawing.scale
648
+ melon.graphic.coords = a
649
+ Canvas.sync
650
+ else
651
+ melon.prevy = melon.position.y
652
+ melon.position.y += height
653
+ end
654
+ melon.velocity.y = 0.0
655
+ return height
656
+ end
657
+
658
+ def update_melon(blist, dt)
659
+ melon = blist[0]
660
+ mx = melon.position.x
661
+ my = melon.position.y
662
+ return "splat" if melon.prevy.nil? || my < melon.prevy
663
+ step_system(blist, dt)
664
+ if @@drawing
665
+ @@drawing.options[:dashcount] = (@@drawing.options[:dashcount] + 1) % @@drawing.options[:dash]
666
+ if @@drawing.options[:dashcount] == 0
667
+ @@drawing.options[:pendown] = @@drawing.options[:pendown].nil? ? :track : nil
668
+ end
669
+ dx = (melon.position.x - mx) * @@drawing.scale
670
+ dy = (my - melon.position.y) * @@drawing.scale
671
+ Canvas.move(melon.graphic, dx, dy, @@drawing.options[:pendown])
672
+ Canvas.sync
673
+ end
674
+ return dt
675
+ end
676
+
677
+ # :begin :drop_melon
678
+ def drop_melon(blist, dt)
679
+ count = 0
680
+ loop do
681
+ res = update_melon(blist, dt)
682
+ break if res == "splat"
683
+ count += 1
684
+ end
685
+ Canvas.sync if @@drawing
686
+ return count * dt
687
+ end
688
+ # :end :drop_melon
333
689
 
690
+
334
691
  =begin rdoc
335
692
  Methods used in IRB sessions.
336
693
  =end
337
694
 
338
695
  # :begin :dist
339
- def dist(r, t)
340
- return r * t
341
- end
696
+ def dist(r, t)
697
+ return r * t
698
+ end
342
699
  # :end :dist
343
700
 
344
701
  # :begin :falling
345
- def falling(t)
346
- return 0.5 * 9.8 * t**2
347
- end
702
+ def falling(t)
703
+ return 0.5 * 9.8 * t**2
704
+ end
348
705
  # :end :falling
349
706
 
350
707
  end # SphereLab