1 /*
2
3 dsh-piccolo-physics Piccolo2D particle system physics integration.
4 Copyright (c) 2009-2013 held jointly by the individual authors.
5
6 This library is free software; you can redistribute it and/or modify it
7 under the terms of the GNU Lesser General Public License as published
8 by the Free Software Foundation; either version 3 of the License, or (at
9 your option) any later version.
10
11 This library is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; with out even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
15
16 You should have received a copy of the GNU Lesser General Public License
17 along with this library; if not, write to the Free Software Foundation,
18 Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
19
20 > http://www.fsf.org/licensing/licenses/lgpl.html
21 > http://www.opensource.org/licenses/lgpl-license.php
22
23 */
24 package org.dishevelled.piccolo.physics;
25
26 import java.awt.geom.Point2D;
27
28 import java.util.HashMap;
29 import java.util.Map;
30
31 import org.piccolo2d.PNode;
32
33 import org.piccolo2d.activities.PActivity;
34
35 import org.piccolo2d.util.PBounds;
36 import org.piccolo2d.util.PUtil;
37
38 import org.dishevelled.multimap.BinaryKeyMap;
39
40 import static org.dishevelled.multimap.impl.BinaryKeyMaps.createBinaryKeyMap;
41
42 import traer.physics.Attraction;
43 import traer.physics.Particle;
44 import traer.physics.ParticleSystem;
45 import traer.physics.Spring;
46
47 /**
48 * Particle system activity.
49 *
50 * @author Michael Heuer
51 * @version $Revision$ $Date$
52 */
53 public class ParticleSystemActivity
54 extends PActivity
55 {
56 /** Particle system for this particle system activity. */
57 private final ParticleSystem particleSystem;
58
59 /** Map of attractions keyed by source and target nodes. */
60 private final BinaryKeyMap<PNode, PNode, Attraction> attractions;
61
62 /** Map of particles keyed by node. */
63 private final Map<PNode, Particle> particles;
64
65 /** Map of springs keyed by source and target nodes. */
66 private final BinaryKeyMap<PNode, PNode, Spring> springs;
67
68
69 /**
70 * Create a new particle system activity with the specified duration in milliseconds.
71 *
72 * @param duration duration in milliseconds, or <code>-1</code> ms for an infinite duration
73 */
74 public ParticleSystemActivity(final long duration)
75 {
76 this(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, System.currentTimeMillis());
77 }
78
79 /**
80 * Create a new particle system activity with the specified duration, step rate, and start time in milliseconds.
81 *
82 * @param duration duration in milliseconds, or <code>-1</code> ms for an infinite duration
83 * @param stepRate step rate in milliseconds
84 * @param startTime start time in milliseconds
85 */
86 public ParticleSystemActivity(final long duration, final long stepRate, final long startTime)
87 {
88 super(duration, stepRate, startTime);
89 particleSystem = new ParticleSystem();
90 attractions = createBinaryKeyMap();
91 particles = new HashMap<PNode, Particle>();
92 springs = createBinaryKeyMap();
93 }
94
95
96 /**
97 * Set the drag force for the particle system to <code>drag</code>.
98 *
99 * @param drag drag force
100 */
101 public void setDrag(final float drag)
102 {
103 particleSystem.setDrag(drag);
104 }
105
106 /**
107 * Set the strength of gravity for the particle system to <code>gravity</code>.
108 *
109 * @param gravity strength of gravity
110 */
111 public void setGravity(final float gravity)
112 {
113 particleSystem.setGravity(gravity);
114 }
115
116 /**
117 * Create a new particle for the specified node with the specified mass.
118 *
119 * @param node node, must not be null
120 * @param mass particle mass
121 */
122 public void createParticle(final PNode node, final float mass)
123 {
124 if (node == null)
125 {
126 throw new IllegalArgumentException("node must not be null");
127 }
128 PBounds fullBounds = node.getFullBoundsReference();
129 float x = (float) fullBounds.getX();
130 float y = (float) fullBounds.getY();
131 Particle particle = particleSystem.makeParticle(mass, x, y, 1.0f);
132 particles.put(node, particle);
133 }
134
135 /**
136 * Return the velocity for particle associated with the specified node.
137 * A particle must have already been created for the specified node.
138 *
139 * @param node node, must not be null
140 * @return the velocity for particle associated with the specified node
141 */
142 public Point2D getVelocity(final PNode node)
143 {
144 return getVelocity(node, new Point2D.Float(0.0f, 0.0f));
145 }
146
147 /**
148 * Return the specified velocity, set to the velocity for particle associated with the specified node.
149 * A particle must have already been created for the specified node.
150 *
151 * @param node node, must not be null
152 * @param velocity velocity, must not be null
153 * @return the specified velocity, set to the velocity for particle associated with
154 * the specified node
155 */
156 public Point2D getVelocity(final PNode node, final Point2D velocity)
157 {
158 checkParticleArgs(node);
159 if (velocity == null)
160 {
161 throw new IllegalArgumentException("velocity must not be null");
162 }
163 Particle particle = particles.get(node);
164 velocity.setLocation(particle.velocity().x(), particle.velocity().y());
165 return velocity;
166 }
167
168 /**
169 * Set the velocity for the particle associated with the specified node
170 * to <code>[x, y]</code>. A particle must have already been created
171 * for the specified node.
172 *
173 * @param node node, must not be null
174 * @param x velocity x
175 * @param y velocity y
176 */
177 public void setVelocity(final PNode node, final float x, final float y)
178 {
179 checkParticleArgs(node);
180 particles.get(node).velocity().set(x, y, 0.0f);
181 }
182
183 /**
184 * Set the velocity for the particle associated with the specified node
185 * to <code>velocity</code>. A particle must have already been created
186 * for the specified node.
187 *
188 * @param node node, must not be null
189 * @param velocity velocity, must not be null
190 */
191 public void setVelocity(final PNode node, final Point2D velocity)
192 {
193 checkParticleArgs(node);
194 if (velocity == null)
195 {
196 throw new IllegalArgumentException("velocity must not be null");
197 }
198 particles.get(node).velocity().set((float) velocity.getX(), (float) velocity.getY(), 0.0f);
199 }
200
201 /**
202 * Return the mass of the particle associated with the specified node. A particle
203 * must have already been created for the specified node.
204 *
205 * @param node node, must not be null
206 * @return the mass of the particle associated with the specified node
207 */
208 public float getParticleMass(final PNode node)
209 {
210 checkParticleArgs(node);
211 return particles.get(node).mass();
212 }
213
214 /**
215 * Set the mass of the particle associated with the specified node to <code>mass</code>.
216 * A particle must have already been created for the specified node.
217 *
218 * @param node node, must not be null
219 * @param mass particle mass
220 */
221 public void setParticleMass(final PNode node, final float mass)
222 {
223 checkParticleArgs(node);
224 particles.get(node).setMass(mass);
225 }
226
227 /**
228 * Clamp the velocity for the particle associated with the specified node
229 * to <code>[0.0f, 0.0f]</code>. A particle must have already been created
230 * for the specified node. A clamped particle will receive updated position
231 * data from the full bounds of the specified node.
232 *
233 * @param node node, must not be null
234 */
235 public void clamp(final PNode node)
236 {
237 checkParticleArgs(node);
238 particles.get(node).makeFixed();
239 }
240
241 /**
242 * Release or unclamp the velocity for the particle associated with the specified node.
243 * A particle must have already been created for the specified node.
244 *
245 * @param node node, must not be null
246 */
247 public void release(final PNode node)
248 {
249 checkParticleArgs(node);
250 particles.get(node).makeFree();
251 }
252
253 /**
254 * Create a new spring between the specified source and target nodes with the
255 * specified strength, damping factor, and rest length. A particle must have already
256 * been created for both the specified source and target nodes.
257 *
258 * @param source source node, must not be null
259 * @param target target node, must not be null
260 * @param strength spring strength
261 * @param damping damping factor
262 * @param restLength rest length
263 */
264 public void createSpring(final PNode source,
265 final PNode target,
266 final float strength,
267 final float damping,
268 final float restLength)
269 {
270 if (source == null)
271 {
272 throw new IllegalArgumentException("source node must not be null");
273 }
274 if (target == null)
275 {
276 throw new IllegalArgumentException("target node must not be null");
277 }
278 if (!particles.containsKey(source))
279 {
280 throw new IllegalArgumentException("no particle exists for source node " + source);
281 }
282 if (!particles.containsKey(target))
283 {
284 throw new IllegalArgumentException("no particle exists for target node " + target);
285 }
286 Spring spring = particleSystem.makeSpring(particles.get(source),
287 particles.get(target),
288 strength,
289 damping,
290 restLength);
291 springs.put(source, target, spring);
292 }
293
294 /**
295 * Enable the spring between the specified source and target nodes. A spring
296 * must have already been created for the specified source and target nodes.
297 *
298 * @param source source node, must not be null
299 * @param target target node, must not be null
300 */
301 public void enableSpring(final PNode source, final PNode target)
302 {
303 checkSpringArgs(source, target);
304 springs.get(source, target).turnOn();
305 }
306
307 /**
308 * Disable the spring between the specified source and target nodes. A spring
309 * must have already been created for the specified source and target nodes.
310 *
311 * @param source source node, must not be null
312 * @param target target node, must not be null
313 */
314 public void disableSpring(final PNode source, final PNode target)
315 {
316 checkSpringArgs(source, target);
317 springs.get(source, target).turnOff();
318 }
319
320 /**
321 * Return the rest length for the spring between the specified source and target nodes.
322 * A spring must have already been created for the specified source and target nodes.
323 *
324 * @param source source node, must not be null
325 * @param target target node, must not be null
326 * @return the rest length for the spring between the specified source and target nodes
327 */
328 public float getSpringRestLength(final PNode source, final PNode target)
329 {
330 checkSpringArgs(source, target);
331 return springs.get(source, target).restLength();
332 }
333
334 /**
335 * Set the rest length for the spring between the specified source and target nodes to <code>restLength</code>.
336 * A spring must have already been created for the specified source and target nodes.
337 *
338 * @param source source node, must not be null
339 * @param target target node, must not be null
340 * @param restLength rest length
341 */
342 public void setSpringRestLength(final PNode source, final PNode target, final float restLength)
343 {
344 checkSpringArgs(source, target);
345 springs.get(source, target).setRestLength(restLength);
346 }
347
348 /**
349 * Return the strength of the spring between the specified source and target nodes.
350 * A spring must have already been created for the specified source and target nodes.
351 *
352 * @param source source node, must not be null
353 * @param target target node, must not be null
354 * @return the strength of the spring between the specified source and target nodes
355 */
356 public float getSpringStrength(final PNode source, final PNode target)
357 {
358 checkSpringArgs(source, target);
359 return springs.get(source, target).strength();
360 }
361
362 /**
363 * Set the strength of the spring between the specified source and target nodes to <code>strength</code>.
364 * A spring must have already been created for the specified source and target nodes.
365 *
366 * @param source source node, must not be null
367 * @param target target node, must not be null
368 * @param strength spring strength
369 */
370 public void setSpringStrength(final PNode source, final PNode target, final float strength)
371 {
372 checkSpringArgs(source, target);
373 springs.get(source, target).setStrength(strength);
374 }
375
376 /**
377 * Return the damping factor for the spring between the specified source and target nodes.
378 * A spring must have already been created for the specified source and target nodes.
379 *
380 * @param source source node, must not be null
381 * @param target target node, must not be null
382 * @return the damping factor for the spring between the specified source and target nodes
383 */
384 public float getSpringDamping(final PNode source, final PNode target)
385 {
386 checkSpringArgs(source, target);
387 return springs.get(source, target).damping();
388 }
389
390 /**
391 * Set the damping factor for the spring between the specified source and target nodes to
392 * <code>dampingFactor</code>. A spring must have already been created for the specified
393 * source and target nodes.
394 *
395 * @param source source node, must not be null
396 * @param target target node, must not be null
397 * @param damping damping factor
398 */
399 public void setSpringDamping(final PNode source, final PNode target, final float damping)
400 {
401 checkSpringArgs(source, target);
402 springs.get(source, target).setDamping(damping);
403 }
404
405 /**
406 * Create a new attraction (or repulsion) force between the specified source and target
407 * nodes with the specified strength and minimum distance. A particle must have already
408 * been created for both the specified source and target nodes.
409 *
410 * @param source source node, must not be null
411 * @param target target node, must not be null
412 * @param strength attraction (or repulsion) force strength
413 * @param minimumDistance minimum distance
414 */
415 public void createAttraction(final PNode source,
416 final PNode target,
417 final float strength,
418 final float minimumDistance)
419 {
420 if (source == null)
421 {
422 throw new IllegalArgumentException("source node must not be null");
423 }
424 if (target == null)
425 {
426 throw new IllegalArgumentException("target node must not be null");
427 }
428 if (!particles.containsKey(source))
429 {
430 throw new IllegalArgumentException("no particle exists for source node " + source);
431 }
432 if (!particles.containsKey(target))
433 {
434 throw new IllegalArgumentException("no particle exists for target node " + target);
435 }
436 Attraction attraction = particleSystem.makeAttraction(particles.get(source),
437 particles.get(target),
438 strength,
439 minimumDistance);
440 attractions.put(source, target, attraction);
441 }
442
443 /**
444 * Enable the attraction between the specified source and target nodes. An attraction
445 * must have already been created for the specified source and target nodes.
446 *
447 * @param source source node, must not be null
448 * @param target target node, must not be null
449 */
450 public void enableAttraction(final PNode source, final PNode target)
451 {
452 checkAttractionArgs(source, target);
453 attractions.get(source, target).turnOn();
454 }
455
456 /**
457 * Disable the attraction between the specified source and target nodes. An attraction
458 * must have already been created for the specified source and target nodes.
459 *
460 * @param source source node, must not be null
461 * @param target target node, must not be null
462 */
463 public void disableAttraction(final PNode source, final PNode target)
464 {
465 checkAttractionArgs(source, target);
466 attractions.get(source, target).turnOff();
467 }
468
469 /**
470 * Return the strength of the attraction between the specified source and target nodes. An attraction
471 * must have already been created for the specified source and target nodes.
472 *
473 * @param source source node, must not be null
474 * @param target target node, must not be null
475 * @return the strength of the attraction between the specified source and target nodes
476 */
477 public float getAttractionStrength(final PNode source, final PNode target)
478 {
479 checkAttractionArgs(source, target);
480 return attractions.get(source, target).getStrength();
481 }
482
483 /**
484 * Set the strength of the attraction between the specified source and target nodes to <code>strength</code>.
485 * An attraction must have already been created for the specified source and target nodes.
486 *
487 * @param source source node, must not be null
488 * @param target target node, must not be null
489 * @param strength attraction (or repulsion) force strength
490 */
491 public void setAttractionStrength(final PNode source, final PNode target, final float strength)
492 {
493 checkAttractionArgs(source, target);
494 attractions.get(source, target).setStrength(strength);
495 }
496
497 /**
498 * Return the minimum distance for the attraction between the specified source and target nodes. An attraction
499 * must have already been created for the specified source and target nodes.
500 *
501 * @param source source node, must not be null
502 * @param target target node, must not be null
503 * @return the minimum distance for the attraction between the specified source and target nodes
504 */
505 public float getAttractionMinimumDistance(final PNode source, final PNode target)
506 {
507 checkAttractionArgs(source, target);
508 return attractions.get(source, target).getMinimumDistance();
509 }
510
511 /**
512 * Set the minimum distance for the attraction between the specified source and target nodes
513 * to <code>minimumDistance</code>. An attraction must have already been created for the
514 * specified source and target nodes.
515 *
516 * @param source source node, must not be null
517 * @param target target node, must not be null
518 * @param minimumDistance minimum distance
519 */
520 public void setAttractionMinimumDistance(final PNode source, final PNode target, final float minimumDistance)
521 {
522 checkAttractionArgs(source, target);
523 attractions.get(source, target).setMinimumDistance(minimumDistance);
524 }
525
526 /**
527 * {@inheritDoc}
528 *
529 * <p>
530 * If subclasses override this method, they must call <code>super.activityFinished()</code>.
531 * </p>
532 */
533 protected void activityStep(final long elapsedTime)
534 {
535 super.activityStep(elapsedTime);
536 particleSystem.tick();
537 for (Map.Entry<PNode, Particle> entry : particles.entrySet())
538 {
539 PNode node = entry.getKey();
540 Particle particle = entry.getValue();
541 // todo: use local bounds or full bounds?
542 if (particle.isFree())
543 {
544 node.setOffset(particle.position().x(), particle.position().y());
545 }
546 else
547 {
548 PBounds fullBounds = node.getFullBoundsReference();
549 particle.position().set((float) fullBounds.getX(), (float) fullBounds.getY(), 0.0f);
550 }
551 }
552 }
553
554 /**
555 * {@inheritDoc}
556 *
557 * <p>
558 * If subclasses override this method, they must call <code>super.activityFinished()</code>.
559 * </p>
560 */
561 protected void activityFinished()
562 {
563 super.activityFinished();
564 attractions.clear();
565 particles.clear();
566 springs.clear();
567 particleSystem.clear();
568 }
569
570
571 /**
572 * Check the specified particle arguments are valid. Throws an {@link IllegalArgumentException}
573 * if the specified node is null or if no particle exists for the specified node.
574 *
575 * @param node node, must not be null
576 */
577 private void checkParticleArgs(final PNode node)
578 {
579 if (node == null)
580 {
581 throw new IllegalArgumentException("node must not be null");
582 }
583 Particle particle = particles.get(node);
584 if (particle == null)
585 {
586 throw new IllegalArgumentException("no particle exists for node " + node);
587 }
588 }
589
590 /**
591 * Check the specified spring arguments are valid. Throws an {@link IllegalArgumentException}
592 * if either the specified nodes are null or if no spring exists for the specified source and target
593 * nodes.
594 *
595 * @param source source node, must not be null
596 * @param target target node, must not be null
597 */
598 private void checkSpringArgs(final PNode source, final PNode target)
599 {
600 if (source == null)
601 {
602 throw new IllegalArgumentException("source node must not be null");
603 }
604 if (target == null)
605 {
606 throw new IllegalArgumentException("target node must not be null");
607 }
608 if (!springs.containsKey(source, target))
609 {
610 throw new IllegalArgumentException("no spring exists between source node "
611 + source + " and target node " + target);
612 }
613 }
614
615 /**
616 * Check the specified attraction arguments are valid. Throws an {@link IllegalArgumentException}
617 * if either the specified nodes are null or if no attraction exists for the specified source and target
618 * nodes.
619 *
620 * @param source source node, must not be null
621 * @param target target node, must not be null
622 */
623 private void checkAttractionArgs(final PNode source, final PNode target)
624 {
625 if (source == null)
626 {
627 throw new IllegalArgumentException("source node must not be null");
628 }
629 if (target == null)
630 {
631 throw new IllegalArgumentException("target node must not be null");
632 }
633 if (!attractions.containsKey(source, target))
634 {
635 throw new IllegalArgumentException("no attraction exists between source node "
636 + source + " and target node " + target);
637 }
638 }
639 }