technohippy-Pongo 0.1.0
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/History.txt +17 -0
- data/Manifest.txt +52 -0
- data/README.txt +93 -0
- data/lib/pongo.rb +33 -0
- data/lib/pongo/abstract_collection.rb +135 -0
- data/lib/pongo/abstract_constraint.rb +25 -0
- data/lib/pongo/abstract_item.rb +93 -0
- data/lib/pongo/abstract_particle.rb +226 -0
- data/lib/pongo/angular_constraint.rb +108 -0
- data/lib/pongo/apengine.rb +157 -0
- data/lib/pongo/circle_particle.rb +36 -0
- data/lib/pongo/collision.rb +11 -0
- data/lib/pongo/collision_detector.rb +222 -0
- data/lib/pongo/collision_event.rb +23 -0
- data/lib/pongo/collision_resolver.rb +39 -0
- data/lib/pongo/composite.rb +52 -0
- data/lib/pongo/container/container.rb +7 -0
- data/lib/pongo/container/shoes_container.rb +14 -0
- data/lib/pongo/group.rb +166 -0
- data/lib/pongo/iforce.rb +7 -0
- data/lib/pongo/logger/logger.rb +21 -0
- data/lib/pongo/logger/shoes_logger.rb +15 -0
- data/lib/pongo/logger/standard_logger.rb +11 -0
- data/lib/pongo/rectangle_particle.rb +79 -0
- data/lib/pongo/renderer/renderer.rb +61 -0
- data/lib/pongo/renderer/shoes_renderer.rb +51 -0
- data/lib/pongo/renderer/tk_renderer.rb +73 -0
- data/lib/pongo/rim_particle.rb +53 -0
- data/lib/pongo/spring_constraint.rb +128 -0
- data/lib/pongo/spring_constraint_particle.rb +235 -0
- data/lib/pongo/util/interval.rb +13 -0
- data/lib/pongo/util/math_util.rb +27 -0
- data/lib/pongo/util/numeric_ext.rb +4 -0
- data/lib/pongo/util/vector.rb +133 -0
- data/lib/pongo/vector_force.rb +34 -0
- data/lib/pongo/wheel_particle.rb +103 -0
- metadata +90 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
module Pongo
|
2
|
+
# The abstract base class for all particles.
|
3
|
+
#
|
4
|
+
# You should not instantiate this class directly -- instead use one of the subclasses.
|
5
|
+
class AbstractParticle < AbstractItem
|
6
|
+
attr_accessor :curr, :prev, :samp, :interval, :temp, :forces, :force_list, :collision, :first_collision
|
7
|
+
attr_accessor :kfr, :mass, :inv_mass, :friction, :fixed, :collidable, :center, :multisample
|
8
|
+
|
9
|
+
def initialize(x, y, is_fixed, mass, elasticity, friction)
|
10
|
+
super()
|
11
|
+
@interval = Interval.new
|
12
|
+
@curr = Vector.new(x, y)
|
13
|
+
@prev = Vector.new(x, y)
|
14
|
+
@samp = Vector.new
|
15
|
+
@temp = Vector.new
|
16
|
+
@fixed = is_fixed
|
17
|
+
@forces = Vector.new
|
18
|
+
@force_list = []
|
19
|
+
@collision = Collision.new
|
20
|
+
@collidable = true
|
21
|
+
@first_collision = false
|
22
|
+
self.mass = mass
|
23
|
+
self.elasticity = elasticity
|
24
|
+
self.friction = friction
|
25
|
+
set_style
|
26
|
+
@center = Vector.new
|
27
|
+
@multisample = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def mass=(m)
|
31
|
+
raise ArgumentError.new('mass may not be set <= 0') if m <= 0
|
32
|
+
@mass = m
|
33
|
+
@inv_mass = 1.0 / m
|
34
|
+
end
|
35
|
+
|
36
|
+
# The elasticity of the particle. Standard values are between 0 and 1.
|
37
|
+
# The higher the value, the greater the elasticity.
|
38
|
+
#
|
39
|
+
# During collisions the elasticity values are combined. If one particle's
|
40
|
+
# elasticity is set to 0.4 and the other is set to 0.4 then the collision will
|
41
|
+
# be have a total elasticity of 0.8. The result will be the same if one particle
|
42
|
+
# has an elasticity of 0 and the other 0.8.
|
43
|
+
#
|
44
|
+
# Setting the elasticity to greater than 1 (of a single particle, or in a combined
|
45
|
+
# collision) will cause particles to bounce with energy greater than naturally
|
46
|
+
# possible.
|
47
|
+
def elasticity
|
48
|
+
@kfr
|
49
|
+
end
|
50
|
+
|
51
|
+
def elasticity=(k)
|
52
|
+
@kfr = k
|
53
|
+
end
|
54
|
+
|
55
|
+
def center
|
56
|
+
@center.set_to(px, py)
|
57
|
+
@center
|
58
|
+
end
|
59
|
+
|
60
|
+
# The surface friction of the particle. Values must be in the range of 0 to 1.
|
61
|
+
#
|
62
|
+
# 0 is no friction (slippery), 1 is full friction (sticky).
|
63
|
+
#
|
64
|
+
# During collisions, the friction values are summed, but are clamped between 1
|
65
|
+
# and 0. For example, If two particles have 0.7 as their surface friction, then
|
66
|
+
# the resulting friction between the two particles will be 1 (full friction).
|
67
|
+
#
|
68
|
+
# There is a bug in the current release where colliding non-fixed particles with
|
69
|
+
# friction greater than 0 will behave erratically. A workaround is to only set
|
70
|
+
# the friction of fixed particles.
|
71
|
+
def friction=(f)
|
72
|
+
raise ArgumentError.new('Legal friction must be >= 0 and <= 1') if f < 0 or 1 < f
|
73
|
+
@friction = f
|
74
|
+
end
|
75
|
+
|
76
|
+
def fixed?
|
77
|
+
@fixed
|
78
|
+
end
|
79
|
+
|
80
|
+
# The position of the particle. Getting the position of the particle is useful
|
81
|
+
# for drawing it or testing it for some custom purpose.
|
82
|
+
#
|
83
|
+
# <p>
|
84
|
+
# When you get the <code>position</code> of a particle you are given a copy of
|
85
|
+
# the current location. Because of this you cannot change the position of a
|
86
|
+
# particle by altering the <code>x</code> and <code>y</code> components of the
|
87
|
+
# Vector you have retrieved from the position property. You have to do something
|
88
|
+
# instead like: <code> position = new Vector(100,100)</code>, or you can use the
|
89
|
+
# <code>px</code> and <code>py</code> properties instead.
|
90
|
+
# </p>
|
91
|
+
#
|
92
|
+
# <p>
|
93
|
+
# You can alter the position of a particle three ways: change its position, set
|
94
|
+
# its velocity, or apply a force to it. Setting the position of a non-fixed
|
95
|
+
# particle is not the same as setting its fixed property to true. A particle held
|
96
|
+
# in place by its position will behave as if it's attached there by a 0 length
|
97
|
+
# spring constraint.
|
98
|
+
def position
|
99
|
+
@curr.dup
|
100
|
+
end
|
101
|
+
|
102
|
+
def position=(p)
|
103
|
+
@curr.copy(p)
|
104
|
+
@prev.copy(p)
|
105
|
+
end
|
106
|
+
|
107
|
+
def px
|
108
|
+
@curr.x
|
109
|
+
end
|
110
|
+
|
111
|
+
def px=(x)
|
112
|
+
@curr.x = x
|
113
|
+
@prev.x = x
|
114
|
+
end
|
115
|
+
|
116
|
+
def py
|
117
|
+
@curr.y
|
118
|
+
end
|
119
|
+
|
120
|
+
def py=(y)
|
121
|
+
@curr.y = y
|
122
|
+
@prev.y = y
|
123
|
+
end
|
124
|
+
|
125
|
+
# The velocity of the particle. If you need to change the motion of a particle,
|
126
|
+
# you should either use this property, or one of the addForce methods. Generally,
|
127
|
+
# the addForce methods are best for slowly altering the motion. The velocity
|
128
|
+
# property is good for instantaneously setting the velocity, e.g., for
|
129
|
+
# projectiles.
|
130
|
+
def velocity
|
131
|
+
@curr - @prev
|
132
|
+
end
|
133
|
+
|
134
|
+
def velocity=(v)
|
135
|
+
@prev = @curr - v
|
136
|
+
end
|
137
|
+
|
138
|
+
# Determines if the particle can collide with other particles or constraints.
|
139
|
+
# The default state is true.
|
140
|
+
def collidable?
|
141
|
+
@collidable
|
142
|
+
end
|
143
|
+
|
144
|
+
def collidable!
|
145
|
+
@collidable = true
|
146
|
+
end
|
147
|
+
|
148
|
+
# Adds a force to the particle. Using this method to a force directly to the
|
149
|
+
# particle will only apply that force for a single APEngine.step() cycle.
|
150
|
+
def add_force(force)
|
151
|
+
@force_list << force
|
152
|
+
end
|
153
|
+
|
154
|
+
# The <code>update()</code> method is called automatically during the
|
155
|
+
# APEngine.step() cycle. This method integrates the particle.
|
156
|
+
def update(dt2)
|
157
|
+
return if fixed?
|
158
|
+
|
159
|
+
accumulate_forces
|
160
|
+
|
161
|
+
@temp.copy(@curr)
|
162
|
+
nv = velocity + @forces.mult!(dt2)
|
163
|
+
@curr.plus!(nv.mult!(APEngine.damping))
|
164
|
+
@prev.copy(@temp)
|
165
|
+
|
166
|
+
clear_forces
|
167
|
+
end
|
168
|
+
|
169
|
+
# Resets the collision state of the particle. This value is used in conjuction
|
170
|
+
# with the CollisionEvent.FIRST_COLLISION event.
|
171
|
+
def reset_first_collision!
|
172
|
+
@first_collision = false
|
173
|
+
end
|
174
|
+
alias reset_first_collision reset_first_collision!
|
175
|
+
|
176
|
+
def components(collision_normal)
|
177
|
+
vel = velocity
|
178
|
+
vdotn = collision_normal.dot(vel)
|
179
|
+
@collision.vn = collision_normal * vdotn
|
180
|
+
@collision.vt = vel - collision.vn
|
181
|
+
@collision
|
182
|
+
end
|
183
|
+
alias get_components components
|
184
|
+
|
185
|
+
# Make sure to align the overriden versions of this method in
|
186
|
+
# WheelParticle
|
187
|
+
def resolve_collision(mtd, vel, n, d, order, particle)
|
188
|
+
test_particle_events(particle)
|
189
|
+
return if fixed? or (not solid?) or (not particle.solid?)
|
190
|
+
@curr.copy(@samp)
|
191
|
+
@curr.plus!(mtd)
|
192
|
+
self.velocity = vel
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_particle_events(p)
|
196
|
+
if has_event_listener(CollisionEvent::COLLIDE)
|
197
|
+
dispatch_event(CollisionEvent.new(CollisionEvent::COLLIDE, p))
|
198
|
+
end
|
199
|
+
if has_event_listener(CollisionEvent::FIRST_COLLIDE) and not @first_collision
|
200
|
+
@first_collision = true
|
201
|
+
dispatch_event(CollisionEvent.new(CollisionEvent::FIRST_COLLIDE, p))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def inv_mass
|
206
|
+
fixed? ? 0 : @inv_mass
|
207
|
+
end
|
208
|
+
|
209
|
+
# Accumulates both the particle forces and the global forces
|
210
|
+
def accumulate_forces
|
211
|
+
@force_list.each {|f| @forces.plus!(f.get_value(@inv_mass))}
|
212
|
+
APEngine.forces.each {|f| @forces.plus!(f.get_value(@inv_mass))}
|
213
|
+
@forces
|
214
|
+
end
|
215
|
+
|
216
|
+
# Clears out all forces on the particle
|
217
|
+
def clear_forces
|
218
|
+
@force_list.clear
|
219
|
+
@forces.set_to(0, 0)
|
220
|
+
end
|
221
|
+
|
222
|
+
def solid?
|
223
|
+
@solid
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'pongo/spring_constraint'
|
2
|
+
|
3
|
+
module Pongo
|
4
|
+
# An Angular Constraint between 3 particles
|
5
|
+
class AngularConstraint < SpringConstraint
|
6
|
+
attr_accessor :p3, :min_ang, :max_ang, :min_break_ang, :max_break_ang
|
7
|
+
|
8
|
+
def initialize(p1, p2, p3, min_ang, max_ang, options={})
|
9
|
+
options = {:min_break_ang => -10, :max_break_ang => 10, :stiffness => 0.5,
|
10
|
+
:dependent => false, :collidable => false, :rect_height => 1, :rect_scale => 1,
|
11
|
+
:scale_to_length => false}.update(options)
|
12
|
+
|
13
|
+
super(p2, p2, stiffness, false, dependent, collidable, rect_height, rect_scale, scale_to_length)
|
14
|
+
|
15
|
+
self.p3 = p3
|
16
|
+
if min_ang == 10
|
17
|
+
self.min_ang = ac_radian
|
18
|
+
self.max_ang = ac_radian
|
19
|
+
else
|
20
|
+
self.min_ang = min_ang
|
21
|
+
self.max_ang = max_ang
|
22
|
+
end
|
23
|
+
self.min_break_ang = min_break_ang
|
24
|
+
self.max_break_ang = max_break_ang
|
25
|
+
end
|
26
|
+
|
27
|
+
# The current difference between the angle of p1, p2, and p3 and a straight line (pi)
|
28
|
+
def ac_radian
|
29
|
+
ang12 = Math.atan2(@p2.curr.y - @p1.curr.y, @p2.curr.x - @p1.curr.x)
|
30
|
+
ang23 = Math.atan2(@p3.curr.y - @p2.curr.y, @p3.curr.x - @p2.curr.x)
|
31
|
+
ang12 - ang23
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if the passed particle is one of the three particles attached
|
35
|
+
# to this AngularConstraint.
|
36
|
+
def is_connected_to?(p)
|
37
|
+
[@p1, @p2, @p3].include?(p)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if any connected particle's <code>fixed</code> property is true.
|
41
|
+
def fixed?
|
42
|
+
@p1.fixed? and @p2.fixed? and @p3.fixed?
|
43
|
+
end
|
44
|
+
alias fixed fixed?
|
45
|
+
|
46
|
+
def resolve
|
47
|
+
return if broken
|
48
|
+
|
49
|
+
ang12 = Math.atan2(@p2.curr.y - @p1.curr.y, @p2.curr.x - @p1.curr.x)
|
50
|
+
ang23 = Math.atan2(self.p3.curr.y - @p2.curr.y, self.p3.curr.x - @p2.curr.x)
|
51
|
+
|
52
|
+
ang_diff = normalize_angle(ang12 - ang23)
|
53
|
+
|
54
|
+
p2_inv_mass = dependent ? 0 : @p2.inv_mass
|
55
|
+
|
56
|
+
sum_inv_mass = @p1.inv_mass + p2_inv_mass
|
57
|
+
mult1 = @p1.inv_mass / sum_inv_mass
|
58
|
+
mult2 = p2_inv_mass / sum_inv_mass
|
59
|
+
ang_change = 0
|
60
|
+
|
61
|
+
low_mid = (self.max_ang - self.min_ang) / 2
|
62
|
+
high_mid = (self.max_ang + self.min_ang) / 2
|
63
|
+
break_ang = (self.max_break_ang - self.min_break_ang) / 2
|
64
|
+
|
65
|
+
new_diff = normalize_angle(high_mid - ang_diff)
|
66
|
+
|
67
|
+
if new_diff > low_mid
|
68
|
+
if new_diff > break_ang
|
69
|
+
diff = new_diff - break_ang
|
70
|
+
broken = true
|
71
|
+
if has_event_listener(BreakEvent::ANGULAR)
|
72
|
+
dispatch_event(BreakEvent.new(BreakEvent::ANGULAR, diff))
|
73
|
+
end
|
74
|
+
return
|
75
|
+
end
|
76
|
+
ang_change = new_diff - low_mid
|
77
|
+
elsif new_diff < -low_mid
|
78
|
+
if new_diff < -break_ang
|
79
|
+
diff = new_dif + break_ang
|
80
|
+
broken = true
|
81
|
+
if has_event_listener(BreakEvent::ANGULAR)
|
82
|
+
dispatch_event(BreakEvent.new(BreakEvent::ANGULAR, diff))
|
83
|
+
end
|
84
|
+
return
|
85
|
+
end
|
86
|
+
ang_change = new_diff + low_mid
|
87
|
+
end
|
88
|
+
|
89
|
+
final_ang = ang_change * self.stiffness + ang12
|
90
|
+
displace_x = @p1.curr.x + (@p2.curr.x - @p1.curr.x) * mult1
|
91
|
+
displace_y = @p1.curr.y + (@p2.curr.y - @p1.curr.y) * mult1
|
92
|
+
|
93
|
+
@p1.curr.x = displace_x + Math.cos(final_ang + Math::PI) * rest_length * mult1
|
94
|
+
@p1.curr.x = displace_y + Math.sin(final_ang + Math::PI) * rest_length * mult1
|
95
|
+
@p2.curr.x = displace_x + Math.cos(final_ang) * rest_length * mult2
|
96
|
+
@p2.curr.y = displace_y + Math.sin(final_ang) * rest_length * mult2
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def normilize_angle(angle)
|
102
|
+
pi2 = Math::PI * 2
|
103
|
+
angle -= pi2 while angle > Math::PI
|
104
|
+
angle += pi2 while angle < -Math::PI
|
105
|
+
angle
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'pongo/group'
|
2
|
+
module Pongo
|
3
|
+
# The main engine class.
|
4
|
+
class PhysicsEngine
|
5
|
+
attr_accessor :forces, :groups, :tile_step, :damping, :container,
|
6
|
+
:constraint_cycles, :constraint_collision_cycles, :renderer, :logger
|
7
|
+
|
8
|
+
def container=(c)
|
9
|
+
@container = c
|
10
|
+
@renderer = c.renderer
|
11
|
+
@logger = c.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
# Initializes the engine. You must call this method prior to adding any
|
15
|
+
# particles or constraints.
|
16
|
+
def setup(options={}) #dt=0.25)
|
17
|
+
options = {
|
18
|
+
:dt => 0.25, :damping => 1, :constraint_cycles => 0,
|
19
|
+
:constraint_collision_cycles => 1
|
20
|
+
}.update(options.is_a?(Numeric) ? {:dt => options} : options)
|
21
|
+
|
22
|
+
@time_step = options[:dt] * options[:dt]
|
23
|
+
@groups = []
|
24
|
+
@forces = []
|
25
|
+
self.damping = options[:damping]
|
26
|
+
self.constraint_cycles = options[:constraint_cycles]
|
27
|
+
self.constraint_collision_cycles = options[:constraint_collision_cycles]
|
28
|
+
|
29
|
+
self.container = options[:container] if options[:container]
|
30
|
+
self.renderer = options[:renderer] if options[:renderer]
|
31
|
+
self.logger = options[:logger] if options[:logger]
|
32
|
+
self.gravity = options[:gravity] if options[:gravity]
|
33
|
+
end
|
34
|
+
alias init setup
|
35
|
+
|
36
|
+
# Adds a force to all particles in the system. The forces added to the APEngine
|
37
|
+
# class are persistent - once a force is added it is continually applied each
|
38
|
+
# APEngine.step() cycle.
|
39
|
+
def add_force(force)
|
40
|
+
@forces << force
|
41
|
+
end
|
42
|
+
|
43
|
+
def gravity=(val)
|
44
|
+
case val
|
45
|
+
when Numeric
|
46
|
+
add_force(VectorForce.new(false, 0, val))
|
47
|
+
when Array
|
48
|
+
add_force(VectorForce.new(false, *val))
|
49
|
+
when VectorForce
|
50
|
+
add_force(val)
|
51
|
+
else
|
52
|
+
raise ArgumentError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Removes a force from the engine.
|
57
|
+
def remove_force(force)
|
58
|
+
@forces.remove(force)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Removes all forces from the engine.
|
62
|
+
def remove_all_forces
|
63
|
+
@forces.clear
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_group(collide_internal=false)
|
67
|
+
group = Group.new(collide_internal)
|
68
|
+
@groups << group
|
69
|
+
group
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds a Group to the engine.
|
73
|
+
def add_group(group)
|
74
|
+
@groups << group
|
75
|
+
group.is_parented = true
|
76
|
+
group.init
|
77
|
+
end
|
78
|
+
|
79
|
+
# Removes a Group from the engine.
|
80
|
+
def remove_group(group)
|
81
|
+
if @groups.delete(group)
|
82
|
+
group.is_parented = false
|
83
|
+
group.cleanup
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# The main step function of the engine. This method should be called
|
88
|
+
# continously to advance the simulation. The faster this method is
|
89
|
+
# called, the faster the simulation will run. Usually you would call
|
90
|
+
# this in your main program loop.
|
91
|
+
def step
|
92
|
+
integrate
|
93
|
+
constraint_cycles.times do
|
94
|
+
satisfy_constraints
|
95
|
+
end
|
96
|
+
constraint_collision_cycles.times do
|
97
|
+
satisfy_constraints
|
98
|
+
check_collisions
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Calling this method will in turn call each Group's paint() method.
|
103
|
+
# Generally you would call this method after stepping the engine in
|
104
|
+
# the main program cycle.
|
105
|
+
def draw
|
106
|
+
@groups.each {|g| g.draw}
|
107
|
+
end
|
108
|
+
alias paint draw
|
109
|
+
|
110
|
+
def integrate
|
111
|
+
@groups.each {|g| g.integrate(@time_step)}
|
112
|
+
end
|
113
|
+
|
114
|
+
def satisfy_constraints
|
115
|
+
@groups.each {|g| g.satisfy_constraints}
|
116
|
+
end
|
117
|
+
|
118
|
+
def check_collisions
|
119
|
+
@groups.each {|g| g.check_collisions}
|
120
|
+
end
|
121
|
+
|
122
|
+
def <<(item)
|
123
|
+
if item.is_a?(Group)
|
124
|
+
add_group(item)
|
125
|
+
else
|
126
|
+
if @groups.empty?
|
127
|
+
@groups << Group.new(true)
|
128
|
+
@groups.first.is_parented = true
|
129
|
+
@groups.first.init
|
130
|
+
end
|
131
|
+
@groups.last << item
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def create_group(collide_internal=true, &block)
|
136
|
+
@groups << Group.new(collide_internal)
|
137
|
+
block.call(@groups.last) if block
|
138
|
+
@groups.last
|
139
|
+
end
|
140
|
+
|
141
|
+
def next_frame(options={})
|
142
|
+
options[:before].call if options[:before]
|
143
|
+
step
|
144
|
+
draw
|
145
|
+
options[:after].call if options[:after]
|
146
|
+
rescue
|
147
|
+
log($!)
|
148
|
+
raise $!
|
149
|
+
end
|
150
|
+
|
151
|
+
def log(message, level=:info)
|
152
|
+
message = message.message + "\n" + message.backtrace.join("\n") if message.is_a?(Exception)
|
153
|
+
@logger.send(level, message) if @logger
|
154
|
+
end
|
155
|
+
end
|
156
|
+
APEngine = PhysicsEngine.new
|
157
|
+
end
|