3402f361c84e34855a45df45a78dff0271886481
[debian/openrocket] / core / src / net / sf / openrocket / gui / figure3d / RocketFigure3d.java
1 package net.sf.openrocket.gui.figure3d;\r
2 \r
3 import java.awt.BorderLayout;\r
4 import java.awt.Color;\r
5 import java.awt.Graphics2D;\r
6 import java.awt.Point;\r
7 import java.awt.Rectangle;\r
8 import java.awt.RenderingHints;\r
9 import java.awt.event.HierarchyEvent;\r
10 import java.awt.event.HierarchyListener;\r
11 import java.awt.event.MouseEvent;\r
12 import java.awt.geom.AffineTransform;\r
13 import java.awt.image.BufferedImage;\r
14 import java.util.ArrayList;\r
15 import java.util.Collection;\r
16 import java.util.HashSet;\r
17 import java.util.Set;\r
18 \r
19 import javax.media.opengl.GL;\r
20 import javax.media.opengl.GL2;\r
21 import javax.media.opengl.GLAutoDrawable;\r
22 import javax.media.opengl.GLCapabilities;\r
23 import javax.media.opengl.GLEventListener;\r
24 import javax.media.opengl.GLProfile;\r
25 import javax.media.opengl.awt.GLCanvas;\r
26 import javax.media.opengl.fixedfunc.GLLightingFunc;\r
27 import javax.media.opengl.fixedfunc.GLMatrixFunc;\r
28 import javax.media.opengl.glu.GLU;\r
29 import javax.swing.JLabel;\r
30 import javax.swing.JPanel;\r
31 import javax.swing.JPopupMenu;\r
32 import javax.swing.SwingUtilities;\r
33 import javax.swing.event.MouseInputAdapter;\r
34 \r
35 import net.sf.openrocket.gui.figureelements.CGCaret;\r
36 import net.sf.openrocket.gui.figureelements.CPCaret;\r
37 import net.sf.openrocket.gui.figureelements.FigureElement;\r
38 import net.sf.openrocket.logging.LogHelper;\r
39 import net.sf.openrocket.rocketcomponent.Configuration;\r
40 import net.sf.openrocket.rocketcomponent.RocketComponent;\r
41 import net.sf.openrocket.startup.Application;\r
42 import net.sf.openrocket.util.Coordinate;\r
43 import net.sf.openrocket.util.MathUtil;\r
44 \r
45 import com.jogamp.opengl.util.awt.Overlay;\r
46 \r
47 /*\r
48  * @author Bill Kuker <bkuker@billkuker.com>\r
49  */\r
50 public class RocketFigure3d extends JPanel implements GLEventListener {\r
51         private static final long serialVersionUID = 1L;\r
52         private static final LogHelper log = Application.getLogger();\r
53         \r
54         static {\r
55                 //this allows the GL canvas and things like the motor selection\r
56                 //drop down to z-order themselves.\r
57                 JPopupMenu.setDefaultLightWeightPopupEnabled(false);\r
58         }\r
59 \r
60         private static final double fovY = 15.0;\r
61         private static double fovX = Double.NaN;\r
62         private static final int CARET_SIZE = 20;\r
63         \r
64         private Configuration configuration;\r
65         private GLCanvas canvas;\r
66 \r
67 \r
68         \r
69         private Overlay extrasOverlay, caretOverlay;\r
70         private BufferedImage cgCaretRaster, cpCaretRaster;\r
71         private volatile boolean redrawExtras = true;\r
72 \r
73         private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();\r
74         private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();\r
75 \r
76         private double roll = 0;\r
77         private double yaw = 0;\r
78 \r
79         Point pickPoint = null;\r
80         MouseEvent pickEvent;\r
81 \r
82         float[] lightPosition = new float[] { 1, 4, 1, 0 };\r
83 \r
84         RocketRenderer rr = new RocketRenderer();\r
85 \r
86         public RocketFigure3d(Configuration config) {\r
87                 this.configuration = config;\r
88                 this.setLayout(new BorderLayout());\r
89 \r
90                 addHierarchyListener(new HierarchyListener() {\r
91                         @Override\r
92                         public void hierarchyChanged(HierarchyEvent e) {\r
93                                 initGLCanvas();\r
94                                 RocketFigure3d.this.removeHierarchyListener(this);\r
95                         }\r
96                 });\r
97         }\r
98         \r
99         private void initGLCanvas(){\r
100                 log.debug("Initializing RocketFigure3D OpenGL Canvas");\r
101                 try {\r
102                         log.debug("Setting up GL capabilities...");\r
103                         GLProfile glp = GLProfile.getDefault();\r
104                         GLCapabilities caps = new GLCapabilities(glp);\r
105                         caps.setSampleBuffers(true);\r
106                         caps.setNumSamples(6);\r
107                         caps.setStencilBits(1);\r
108 \r
109                         log.debug("Creating OpenGL Canvas");\r
110                         canvas = new GLCanvas(caps);\r
111 \r
112                         canvas.addGLEventListener(this);\r
113                         this.add(canvas, BorderLayout.CENTER);\r
114 \r
115                         setupMouseListeners();\r
116                         rasterizeCarets();\r
117                         \r
118                 } catch (Throwable t) {\r
119                         log.error("An error occurred creating 3d View", t);\r
120                         canvas = null;\r
121                         this.add(new JLabel("Unable to load 3d Libraries: "\r
122                                         + t.getMessage()));\r
123                 }\r
124         }\r
125         \r
126         /**\r
127          * Set up the standard rendering hints on the Graphics2D\r
128          */\r
129         private static void setRenderingHints(Graphics2D g){\r
130                 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,\r
131                                 RenderingHints.VALUE_STROKE_NORMALIZE);\r
132                 g.setRenderingHint(RenderingHints.KEY_RENDERING,\r
133                                 RenderingHints.VALUE_RENDER_QUALITY);\r
134                 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\r
135                                 RenderingHints.VALUE_ANTIALIAS_ON);\r
136         }\r
137         \r
138         /**\r
139          * Rasterize the carets into 2 buffered images that I can blit onto the\r
140          * 3d display every redraw without all of the caret shape rendering overhead\r
141          */\r
142         private void rasterizeCarets(){\r
143                 Graphics2D g2d;\r
144                 \r
145                 //Rasterize a CG Caret\r
146                 cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);\r
147                 g2d = cgCaretRaster.createGraphics();\r
148                 setRenderingHints(g2d);\r
149                 \r
150                 g2d.setBackground(new Color(0, 0, 0, 0));\r
151                 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);\r
152                 \r
153                 new CGCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);\r
154                 \r
155                 g2d.dispose();\r
156 \r
157                 //Rasterize a CP Caret\r
158                 cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);\r
159                 g2d = cpCaretRaster.createGraphics();\r
160                 setRenderingHints(g2d);\r
161                 \r
162                 g2d.setBackground(new Color(0, 0, 0, 0));\r
163                 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);\r
164                 \r
165                 new CPCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);\r
166                 \r
167                 g2d.dispose();\r
168                 \r
169         }\r
170 \r
171         private void setupMouseListeners() {\r
172                 MouseInputAdapter a = new MouseInputAdapter() {\r
173                         int lastX;\r
174                         int lastY;\r
175                         MouseEvent pressEvent;\r
176 \r
177                         @Override\r
178                         public void mousePressed(MouseEvent e) {\r
179                                 lastX = e.getX();\r
180                                 lastY = e.getY();\r
181                                 pressEvent = e;\r
182                         }\r
183 \r
184                         @Override\r
185                         public void mouseClicked(MouseEvent e) {\r
186                                 pickPoint = new Point(lastX, canvas.getHeight() - lastY);\r
187                                 pickEvent = e;\r
188                                 internalRepaint();\r
189                         }\r
190 \r
191                         @Override\r
192                         public void mouseDragged(MouseEvent e) {\r
193                                 int dx = lastX - e.getX();\r
194                                 int dy = lastY - e.getY();\r
195                                 lastX = e.getX();\r
196                                 lastY = e.getY();\r
197 \r
198                                 if (pressEvent.getButton() == MouseEvent.BUTTON1) {\r
199                                         if (Math.abs(dx) > Math.abs(dy)) {\r
200                                                 setYaw(yaw - (float) dx / 100.0);\r
201                                         } else {\r
202                                                 if ( yaw > Math.PI/2.0 && yaw < 3.0*Math.PI/2.0 ){\r
203                                                         dy = -dy;\r
204                                                 }\r
205                                                 setRoll(roll - (float) dy / 100.0);\r
206                                         }\r
207                                 } else {\r
208                                         lightPosition[0] -= 0.1f * dx;\r
209                                         lightPosition[1] += 0.1f * dy;\r
210                                         internalRepaint();\r
211                                 }\r
212                         }\r
213                 };\r
214                 canvas.addMouseMotionListener(a);\r
215                 canvas.addMouseListener(a);\r
216         }\r
217 \r
218         public void setConfiguration(Configuration configuration) {\r
219                 this.configuration = configuration;\r
220                 updateFigure();\r
221         }\r
222 \r
223         @Override\r
224         public void display(GLAutoDrawable drawable) {\r
225                 GL2 gl = drawable.getGL().getGL2();\r
226                 GLU glu = new GLU();\r
227 \r
228                 gl.glEnable(GL.GL_MULTISAMPLE);\r
229 \r
230                 gl.glClearColor(1, 1, 1, 1);\r
231                 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);\r
232 \r
233                 setupView(gl, glu);\r
234 \r
235                 if (pickPoint != null) {\r
236                         gl.glDisable(GLLightingFunc.GL_LIGHTING);\r
237                         final RocketComponent picked = rr.pick(drawable, configuration,\r
238                                         pickPoint, pickEvent.isShiftDown()?selection:null );\r
239                         if (csl != null && picked != null) {\r
240                                 final MouseEvent e = pickEvent;\r
241                                 SwingUtilities.invokeLater(new Runnable() {\r
242                                         @Override\r
243                                         public void run() {\r
244                                                 csl.componentClicked(new RocketComponent[] { picked },\r
245                                                                 e);\r
246                                         }\r
247                                 });\r
248 \r
249                         }\r
250                         pickPoint = null;\r
251 \r
252                         gl.glClearColor(1, 1, 1, 1);\r
253                         gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);\r
254 \r
255                         gl.glEnable(GLLightingFunc.GL_LIGHTING);\r
256                 }\r
257                 rr.render(drawable, configuration, selection);\r
258                 \r
259                 drawExtras(gl, glu);\r
260                 drawCarets(gl, glu);\r
261         }\r
262 \r
263         \r
264         private void drawCarets(GL2 gl, GLU glu) {\r
265                 final Graphics2D og2d = caretOverlay.createGraphics();\r
266                 setRenderingHints(og2d);\r
267                 \r
268                 og2d.setBackground(new Color(0, 0, 0, 0));\r
269                 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());\r
270                 caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());\r
271 \r
272                 // The existing relative Extras don't really work right for 3d.\r
273                 Coordinate pCP = project(cp, gl, glu);\r
274                 Coordinate pCG = project(cg, gl, glu);\r
275 \r
276                 final int d = CARET_SIZE/2;\r
277                 \r
278                 //z order the carets \r
279                 if (pCG.z < pCP.z) {\r
280                         //Subtract half of the caret size, so they are centered ( The +/- d in each translate)\r
281                         //Flip the sense of the Y coordinate from GL to normal (Y+ up/down)\r
282                         og2d.drawRenderedImage(\r
283                                         cpCaretRaster,\r
284                                         AffineTransform.getTranslateInstance((pCP.x - d),\r
285                                                         canvas.getHeight() - (pCP.y + d)));\r
286                         og2d.drawRenderedImage(\r
287                                         cgCaretRaster,\r
288                                         AffineTransform.getTranslateInstance((pCG.x - d),\r
289                                                         canvas.getHeight() - (pCG.y + d)));\r
290                 } else {\r
291                         og2d.drawRenderedImage(\r
292                                         cgCaretRaster,\r
293                                         AffineTransform.getTranslateInstance((pCG.x - d),\r
294                                                         canvas.getHeight() - (pCG.y + d)));\r
295                         og2d.drawRenderedImage(\r
296                                         cpCaretRaster,\r
297                                         AffineTransform.getTranslateInstance((pCP.x - d),\r
298                                                         canvas.getHeight() - (pCP.y + d)));\r
299                 }\r
300                 og2d.dispose();\r
301                 \r
302                 gl.glEnable(GL.GL_BLEND);\r
303                 caretOverlay.drawAll();\r
304                 gl.glDisable(GL.GL_BLEND);\r
305         }\r
306         \r
307         /**\r
308          * Draw the extras overlay to the gl canvas.\r
309          * Re-blits the overlay every frame. Only re-renders the overlay\r
310          * when needed.\r
311          */\r
312         private void drawExtras(GL2 gl, GLU glu){\r
313                 //Only re-render if needed\r
314                 //      redrawExtras: Some external change (new simulation data) means\r
315                 //              the data is out of date.\r
316                 //      extrasOverlay.contentsLost(): For some reason the buffer with this\r
317                 //              data is lost.\r
318                 if ( redrawExtras || extrasOverlay.contentsLost() ){\r
319                         log.debug("Redrawing Overlay");\r
320                         \r
321                         final Graphics2D og2d = extrasOverlay.createGraphics(); \r
322                         setRenderingHints(og2d);\r
323 \r
324                         og2d.setBackground(new Color(0, 0, 0, 0));\r
325                         og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());\r
326                         extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());\r
327                         \r
328                         for (FigureElement e : relativeExtra) {\r
329                                 e.paint(og2d, 1);\r
330                         }\r
331                         Rectangle rect = this.getVisibleRect();\r
332         \r
333                         for (FigureElement e : absoluteExtra) {\r
334                                 e.paint(og2d, 1.0, rect);\r
335                         }\r
336                         og2d.dispose();\r
337                         \r
338                         redrawExtras = false;\r
339                 }\r
340 \r
341                 //Re-blit to gl canvas every time\r
342                 gl.glEnable(GL.GL_BLEND);\r
343                 extrasOverlay.drawAll();\r
344                 gl.glDisable(GL.GL_BLEND);\r
345         }\r
346 \r
347         @Override\r
348         public void dispose(GLAutoDrawable drawable) {\r
349         }\r
350 \r
351         @Override\r
352         public void init(GLAutoDrawable drawable) {\r
353                 rr.init(drawable);\r
354 \r
355                 GL2 gl = drawable.getGL().getGL2();\r
356                 gl.glClearDepth(1.0f); // clear z-buffer to the farthest\r
357 \r
358                 gl.glDepthFunc(GL.GL_LEQUAL); // the type of depth test to do\r
359 \r
360                 float amb = 0.5f;\r
361                 float dif = 1.0f;\r
362                 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT,\r
363                                 new float[] { amb, amb, amb, 1 }, 0);\r
364                 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE,\r
365                                 new float[] { dif, dif, dif, 1 }, 0);\r
366                 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR,\r
367                                 new float[] { dif, dif, dif, 1 }, 0);\r
368 \r
369                 gl.glEnable(GLLightingFunc.GL_LIGHT1);\r
370                 gl.glEnable(GLLightingFunc.GL_LIGHTING);\r
371                 gl.glShadeModel(GLLightingFunc.GL_SMOOTH);\r
372 \r
373                 gl.glEnable(GLLightingFunc.GL_NORMALIZE);\r
374 \r
375                 extrasOverlay = new Overlay(drawable);\r
376                 caretOverlay = new Overlay(drawable);\r
377         }\r
378 \r
379         @Override\r
380         public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {\r
381                 GL2 gl = drawable.getGL().getGL2();\r
382                 GLU glu = new GLU();\r
383 \r
384                 double ratio = (double) w / (double) h;\r
385                 fovX = fovY * ratio;\r
386 \r
387                 gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);\r
388                 gl.glLoadIdentity();\r
389                 glu.gluPerspective(fovY, ratio, 0.05f, 100f);\r
390                 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);\r
391                 \r
392                 redrawExtras = true;\r
393         }\r
394 \r
395         @SuppressWarnings("unused")\r
396         private static class Bounds {\r
397                 double xMin, xMax, xSize;\r
398                 double yMin, yMax, ySize;\r
399                 double zMin, zMax, zSize;\r
400                 double rMax;\r
401         }\r
402 \r
403         /**\r
404          * Calculates the bounds for the current configuration\r
405          * \r
406          * @return\r
407          */\r
408         private Bounds calculateBounds() {\r
409                 Bounds ret = new Bounds();\r
410                 Collection<Coordinate> bounds = configuration.getBounds();\r
411                 for (Coordinate c : bounds) {\r
412                         ret.xMax = Math.max(ret.xMax, c.x);\r
413                         ret.xMin = Math.min(ret.xMin, c.x);\r
414 \r
415                         ret.yMax = Math.max(ret.yMax, c.y);\r
416                         ret.yMin = Math.min(ret.yMin, c.y);\r
417 \r
418                         ret.zMax = Math.max(ret.zMax, c.z);\r
419                         ret.zMin = Math.min(ret.zMin, c.z);\r
420 \r
421                         double r = MathUtil.hypot(c.y, c.z);\r
422                         ret.rMax = Math.max(ret.rMax, r);\r
423                 }\r
424                 ret.xSize = ret.xMax - ret.xMin;\r
425                 ret.ySize = ret.yMax - ret.yMin;\r
426                 ret.zSize = ret.zMax - ret.zMin;\r
427                 return ret;\r
428         }\r
429 \r
430         private void setupView(GL2 gl, GLU glu) {\r
431                 gl.glLoadIdentity();\r
432 \r
433                 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION,\r
434                                 lightPosition, 0);\r
435 \r
436                 // Get the bounds\r
437                 Bounds b = calculateBounds();\r
438 \r
439                 // Calculate the distance needed to fit the bounds in both the X and Y\r
440                 // direction\r
441                 // Add 10% for space around it.\r
442                 double dX = (b.xSize * 1.2 / 2.0)\r
443                                 / Math.tan(Math.toRadians(fovX / 2.0));\r
444                 double dY = (b.rMax * 2.0 * 1.2 / 2.0)\r
445                                 / Math.tan(Math.toRadians(fovY / 2.0));\r
446 \r
447                 // Move back the greater of the 2 distances\r
448                 glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0);\r
449 \r
450                 gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0);\r
451                 gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0);\r
452 \r
453                 // Center the rocket in the view.\r
454                 gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0);\r
455                 \r
456                 //Change to LEFT Handed coordinates\r
457                 gl.glScaled(1, 1, -1);\r
458                 gl.glFrontFace(GL.GL_CW);\r
459                 \r
460                 //Flip textures for LEFT handed coords\r
461                 gl.glMatrixMode(GL.GL_TEXTURE);\r
462                 gl.glLoadIdentity();\r
463                 gl.glScaled(-1,1,1);\r
464                 gl.glTranslated(-1,0,0);\r
465                 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);\r
466         }\r
467 \r
468         /**\r
469          * Call when the rocket has changed\r
470          */\r
471         public void updateFigure() {\r
472                 log.debug("3D Figure Updated");\r
473                 rr.updateFigure();\r
474                 internalRepaint();\r
475         }\r
476 \r
477         private void internalRepaint(){\r
478                 super.repaint();\r
479                 if (canvas != null)\r
480                         canvas.display();\r
481         }\r
482         \r
483         @Override\r
484         public void repaint() {\r
485                 redrawExtras = true;\r
486                 internalRepaint();\r
487         }\r
488 \r
489         private Set<RocketComponent> selection = new HashSet<RocketComponent>();\r
490 \r
491         public void setSelection(RocketComponent[] selection) {\r
492                 this.selection.clear();\r
493                 if (selection != null) {\r
494                         for (RocketComponent c : selection)\r
495                                 this.selection.add(c);\r
496                 }\r
497                 internalRepaint();\r
498         }\r
499 \r
500         private void setRoll(double rot) {\r
501                 if (MathUtil.equals(roll, rot))\r
502                         return;\r
503                 this.roll = MathUtil.reduce360(rot);\r
504                 internalRepaint();\r
505         }\r
506 \r
507         private void setYaw(double rot) {\r
508                 if (MathUtil.equals(yaw, rot))\r
509                         return;\r
510                 this.yaw = MathUtil.reduce360(rot);\r
511                 internalRepaint();\r
512         }\r
513 \r
514         // ///////////// Extra methods\r
515 \r
516         private Coordinate project(Coordinate c, GL2 gl, GLU glu) {\r
517                 double[] mvmatrix = new double[16];\r
518                 double[] projmatrix = new double[16];\r
519                 int[] viewport = new int[4];\r
520 \r
521                 gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\r
522                 gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0);\r
523                 gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0);\r
524 \r
525                 double out[] = new double[4];\r
526                 glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0,\r
527                                 out, 0);\r
528 \r
529                 return new Coordinate(out[0], out[1], out[2]);\r
530         }\r
531 \r
532         private Coordinate cp = new Coordinate(0, 0, 0);\r
533         private Coordinate cg = new Coordinate(0, 0, 0);\r
534 \r
535         public void setCG(Coordinate cg) {\r
536                 this.cg = cg;\r
537                 redrawExtras = true;\r
538         }\r
539 \r
540         public void setCP(Coordinate cp) {\r
541                 this.cp = cp;\r
542                 redrawExtras = true;\r
543         }\r
544 \r
545         public void addRelativeExtra(FigureElement p) {\r
546                 relativeExtra.add(p);\r
547                 redrawExtras = true;\r
548         }\r
549 \r
550         public void removeRelativeExtra(FigureElement p) {\r
551                 relativeExtra.remove(p);\r
552                 redrawExtras = true;\r
553         }\r
554 \r
555         public void clearRelativeExtra() {\r
556                 relativeExtra.clear();\r
557                 redrawExtras = true;\r
558         }\r
559 \r
560         public void addAbsoluteExtra(FigureElement p) {\r
561                 absoluteExtra.add(p);\r
562                 redrawExtras = true;\r
563         }\r
564 \r
565         public void removeAbsoluteExtra(FigureElement p) {\r
566                 absoluteExtra.remove(p);\r
567                 redrawExtras = true;\r
568         }\r
569 \r
570         public void clearAbsoluteExtra() {\r
571                 absoluteExtra.clear();\r
572                 redrawExtras = true;\r
573         }\r
574 \r
575         private ComponentSelectionListener csl;\r
576 \r
577         public static interface ComponentSelectionListener {\r
578                 public void componentClicked(RocketComponent[] components, MouseEvent e);\r
579         }\r
580 \r
581         public void addComponentSelectionListener(\r
582                         ComponentSelectionListener newListener) {\r
583                 this.csl = newListener;\r
584         }\r
585 \r
586 }\r