+package net.sf.openrocket.gui.figure3d;\r
+\r
+import java.awt.BorderLayout;\r
+import java.awt.Color;\r
+import java.awt.Graphics2D;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.RenderingHints;\r
+import java.awt.event.HierarchyEvent;\r
+import java.awt.event.HierarchyListener;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.image.BufferedImage;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.HashSet;\r
+import java.util.Set;\r
+\r
+import javax.media.opengl.GL;\r
+import javax.media.opengl.GL2;\r
+import javax.media.opengl.GLAutoDrawable;\r
+import javax.media.opengl.GLCapabilities;\r
+import javax.media.opengl.GLEventListener;\r
+import javax.media.opengl.GLProfile;\r
+import javax.media.opengl.awt.GLCanvas;\r
+import javax.media.opengl.fixedfunc.GLLightingFunc;\r
+import javax.media.opengl.fixedfunc.GLMatrixFunc;\r
+import javax.media.opengl.glu.GLU;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JPopupMenu;\r
+import javax.swing.SwingUtilities;\r
+import javax.swing.event.MouseInputAdapter;\r
+\r
+import net.sf.openrocket.gui.figureelements.CGCaret;\r
+import net.sf.openrocket.gui.figureelements.CPCaret;\r
+import net.sf.openrocket.gui.figureelements.FigureElement;\r
+import net.sf.openrocket.logging.LogHelper;\r
+import net.sf.openrocket.rocketcomponent.Configuration;\r
+import net.sf.openrocket.rocketcomponent.RocketComponent;\r
+import net.sf.openrocket.startup.Application;\r
+import net.sf.openrocket.util.Coordinate;\r
+import net.sf.openrocket.util.MathUtil;\r
+\r
+import com.jogamp.opengl.util.awt.Overlay;\r
+\r
+/*\r
+ * @author Bill Kuker <bkuker@billkuker.com>\r
+ */\r
+public class RocketFigure3d extends JPanel implements GLEventListener {\r
+ private static final long serialVersionUID = 1L;\r
+ private static final LogHelper log = Application.getLogger();\r
+ \r
+ static {\r
+ //this allows the GL canvas and things like the motor selection\r
+ //drop down to z-order themselves.\r
+ JPopupMenu.setDefaultLightWeightPopupEnabled(false);\r
+ }\r
+\r
+ private static final double fovY = 15.0;\r
+ private static double fovX = Double.NaN;\r
+ private static final int CARET_SIZE = 20;\r
+ \r
+ private Configuration configuration;\r
+ private GLCanvas canvas;\r
+\r
+\r
+ \r
+ private Overlay extrasOverlay, caretOverlay;\r
+ private BufferedImage cgCaretRaster, cpCaretRaster;\r
+ private volatile boolean redrawExtras = true;\r
+\r
+ private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();\r
+ private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();\r
+\r
+ private double roll = 0;\r
+ private double yaw = 0;\r
+\r
+ Point pickPoint = null;\r
+ MouseEvent pickEvent;\r
+\r
+ float[] lightPosition = new float[] { 1, 4, 1, 0 };\r
+\r
+ RocketRenderer rr = new RocketRenderer();\r
+\r
+ public RocketFigure3d(Configuration config) {\r
+ this.configuration = config;\r
+ this.setLayout(new BorderLayout());\r
+\r
+ addHierarchyListener(new HierarchyListener() {\r
+ @Override\r
+ public void hierarchyChanged(HierarchyEvent e) {\r
+ initGLCanvas();\r
+ RocketFigure3d.this.removeHierarchyListener(this);\r
+ }\r
+ });\r
+ }\r
+ \r
+ private void initGLCanvas(){\r
+ log.debug("Initializing RocketFigure3D OpenGL Canvas");\r
+ try {\r
+ log.debug("Setting up GL capabilities...");\r
+ GLProfile glp = GLProfile.getDefault();\r
+ GLCapabilities caps = new GLCapabilities(glp);\r
+ caps.setSampleBuffers(true);\r
+ caps.setNumSamples(6);\r
+ caps.setStencilBits(1);\r
+\r
+ log.debug("Creating OpenGL Canvas");\r
+ canvas = new GLCanvas(caps);\r
+\r
+ canvas.addGLEventListener(this);\r
+ this.add(canvas, BorderLayout.CENTER);\r
+\r
+ setupMouseListeners();\r
+ rasterizeCarets();\r
+ \r
+ } catch (Throwable t) {\r
+ log.error("An error occurred creating 3d View", t);\r
+ canvas = null;\r
+ this.add(new JLabel("Unable to load 3d Libraries: "\r
+ + t.getMessage()));\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * Set up the standard rendering hints on the Graphics2D\r
+ */\r
+ private static void setRenderingHints(Graphics2D g){\r
+ g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,\r
+ RenderingHints.VALUE_STROKE_NORMALIZE);\r
+ g.setRenderingHint(RenderingHints.KEY_RENDERING,\r
+ RenderingHints.VALUE_RENDER_QUALITY);\r
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\r
+ RenderingHints.VALUE_ANTIALIAS_ON);\r
+ }\r
+ \r
+ /**\r
+ * Rasterize the carets into 2 buffered images that I can blit onto the\r
+ * 3d display every redraw without all of the caret shape rendering overhead\r
+ */\r
+ private void rasterizeCarets(){\r
+ Graphics2D g2d;\r
+ \r
+ //Rasterize a CG Caret\r
+ cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);\r
+ g2d = cgCaretRaster.createGraphics();\r
+ setRenderingHints(g2d);\r
+ \r
+ g2d.setBackground(new Color(0, 0, 0, 0));\r
+ g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);\r
+ \r
+ new CGCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);\r
+ \r
+ g2d.dispose();\r
+\r
+ //Rasterize a CP Caret\r
+ cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);\r
+ g2d = cpCaretRaster.createGraphics();\r
+ setRenderingHints(g2d);\r
+ \r
+ g2d.setBackground(new Color(0, 0, 0, 0));\r
+ g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);\r
+ \r
+ new CPCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);\r
+ \r
+ g2d.dispose();\r
+ \r
+ }\r
+\r
+ private void setupMouseListeners() {\r
+ MouseInputAdapter a = new MouseInputAdapter() {\r
+ int lastX;\r
+ int lastY;\r
+ MouseEvent pressEvent;\r
+\r
+ @Override\r
+ public void mousePressed(MouseEvent e) {\r
+ lastX = e.getX();\r
+ lastY = e.getY();\r
+ pressEvent = e;\r
+ }\r
+\r
+ @Override\r
+ public void mouseClicked(MouseEvent e) {\r
+ pickPoint = new Point(lastX, canvas.getHeight() - lastY);\r
+ pickEvent = e;\r
+ internalRepaint();\r
+ }\r
+\r
+ @Override\r
+ public void mouseDragged(MouseEvent e) {\r
+ int dx = lastX - e.getX();\r
+ int dy = lastY - e.getY();\r
+ lastX = e.getX();\r
+ lastY = e.getY();\r
+\r
+ if (pressEvent.getButton() == MouseEvent.BUTTON1) {\r
+ if (Math.abs(dx) > Math.abs(dy)) {\r
+ setYaw(yaw - (float) dx / 100.0);\r
+ } else {\r
+ if ( yaw > Math.PI/2.0 && yaw < 3.0*Math.PI/2.0 ){\r
+ dy = -dy;\r
+ }\r
+ setRoll(roll - (float) dy / 100.0);\r
+ }\r
+ } else {\r
+ lightPosition[0] -= 0.1f * dx;\r
+ lightPosition[1] += 0.1f * dy;\r
+ internalRepaint();\r
+ }\r
+ }\r
+ };\r
+ canvas.addMouseMotionListener(a);\r
+ canvas.addMouseListener(a);\r
+ }\r
+\r
+ public void setConfiguration(Configuration configuration) {\r
+ this.configuration = configuration;\r
+ updateFigure();\r
+ }\r
+\r
+ @Override\r
+ public void display(GLAutoDrawable drawable) {\r
+ GL2 gl = drawable.getGL().getGL2();\r
+ GLU glu = new GLU();\r
+\r
+ gl.glEnable(GL.GL_MULTISAMPLE);\r
+\r
+ gl.glClearColor(1, 1, 1, 1);\r
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);\r
+\r
+ setupView(gl, glu);\r
+\r
+ if (pickPoint != null) {\r
+ gl.glDisable(GLLightingFunc.GL_LIGHTING);\r
+ final RocketComponent picked = rr.pick(drawable, configuration,\r
+ pickPoint, pickEvent.isShiftDown()?selection:null );\r
+ if (csl != null && picked != null) {\r
+ final MouseEvent e = pickEvent;\r
+ SwingUtilities.invokeLater(new Runnable() {\r
+ @Override\r
+ public void run() {\r
+ csl.componentClicked(new RocketComponent[] { picked },\r
+ e);\r
+ }\r
+ });\r
+\r
+ }\r
+ pickPoint = null;\r
+\r
+ gl.glClearColor(1, 1, 1, 1);\r
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);\r
+\r
+ gl.glEnable(GLLightingFunc.GL_LIGHTING);\r
+ }\r
+ rr.render(drawable, configuration, selection);\r
+ \r
+ drawExtras(gl, glu);\r
+ drawCarets(gl, glu);\r
+ }\r
+\r
+ \r
+ private void drawCarets(GL2 gl, GLU glu) {\r
+ final Graphics2D og2d = caretOverlay.createGraphics();\r
+ setRenderingHints(og2d);\r
+ \r
+ og2d.setBackground(new Color(0, 0, 0, 0));\r
+ og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());\r
+ caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());\r
+\r
+ // The existing relative Extras don't really work right for 3d.\r
+ Coordinate pCP = project(cp, gl, glu);\r
+ Coordinate pCG = project(cg, gl, glu);\r
+\r
+ final int d = CARET_SIZE/2;\r
+ \r
+ //z order the carets \r
+ if (pCG.z < pCP.z) {\r
+ //Subtract half of the caret size, so they are centered ( The +/- d in each translate)\r
+ //Flip the sense of the Y coordinate from GL to normal (Y+ up/down)\r
+ og2d.drawRenderedImage(\r
+ cpCaretRaster,\r
+ AffineTransform.getTranslateInstance((pCP.x - d),\r
+ canvas.getHeight() - (pCP.y + d)));\r
+ og2d.drawRenderedImage(\r
+ cgCaretRaster,\r
+ AffineTransform.getTranslateInstance((pCG.x - d),\r
+ canvas.getHeight() - (pCG.y + d)));\r
+ } else {\r
+ og2d.drawRenderedImage(\r
+ cgCaretRaster,\r
+ AffineTransform.getTranslateInstance((pCG.x - d),\r
+ canvas.getHeight() - (pCG.y + d)));\r
+ og2d.drawRenderedImage(\r
+ cpCaretRaster,\r
+ AffineTransform.getTranslateInstance((pCP.x - d),\r
+ canvas.getHeight() - (pCP.y + d)));\r
+ }\r
+ og2d.dispose();\r
+ \r
+ gl.glEnable(GL.GL_BLEND);\r
+ caretOverlay.drawAll();\r
+ gl.glDisable(GL.GL_BLEND);\r
+ }\r
+ \r
+ /**\r
+ * Draw the extras overlay to the gl canvas.\r
+ * Re-blits the overlay every frame. Only re-renders the overlay\r
+ * when needed.\r
+ */\r
+ private void drawExtras(GL2 gl, GLU glu){\r
+ //Only re-render if needed\r
+ // redrawExtras: Some external change (new simulation data) means\r
+ // the data is out of date.\r
+ // extrasOverlay.contentsLost(): For some reason the buffer with this\r
+ // data is lost.\r
+ if ( redrawExtras || extrasOverlay.contentsLost() ){\r
+ log.debug("Redrawing Overlay");\r
+ \r
+ final Graphics2D og2d = extrasOverlay.createGraphics(); \r
+ setRenderingHints(og2d);\r
+\r
+ og2d.setBackground(new Color(0, 0, 0, 0));\r
+ og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());\r
+ extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());\r
+ \r
+ for (FigureElement e : relativeExtra) {\r
+ e.paint(og2d, 1);\r
+ }\r
+ Rectangle rect = this.getVisibleRect();\r
+ \r
+ for (FigureElement e : absoluteExtra) {\r
+ e.paint(og2d, 1.0, rect);\r
+ }\r
+ og2d.dispose();\r
+ \r
+ redrawExtras = false;\r
+ }\r
+\r
+ //Re-blit to gl canvas every time\r
+ gl.glEnable(GL.GL_BLEND);\r
+ extrasOverlay.drawAll();\r
+ gl.glDisable(GL.GL_BLEND);\r
+ }\r
+\r
+ @Override\r
+ public void dispose(GLAutoDrawable drawable) {\r
+ }\r
+\r
+ @Override\r
+ public void init(GLAutoDrawable drawable) {\r
+ rr.init(drawable);\r
+\r
+ GL2 gl = drawable.getGL().getGL2();\r
+ gl.glClearDepth(1.0f); // clear z-buffer to the farthest\r
+\r
+ gl.glDepthFunc(GL.GL_LEQUAL); // the type of depth test to do\r
+\r
+ float amb = 0.5f;\r
+ float dif = 1.0f;\r
+ gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT,\r
+ new float[] { amb, amb, amb, 1 }, 0);\r
+ gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE,\r
+ new float[] { dif, dif, dif, 1 }, 0);\r
+ gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR,\r
+ new float[] { dif, dif, dif, 1 }, 0);\r
+\r
+ gl.glEnable(GLLightingFunc.GL_LIGHT1);\r
+ gl.glEnable(GLLightingFunc.GL_LIGHTING);\r
+ gl.glShadeModel(GLLightingFunc.GL_SMOOTH);\r
+\r
+ gl.glEnable(GLLightingFunc.GL_NORMALIZE);\r
+\r
+ extrasOverlay = new Overlay(drawable);\r
+ caretOverlay = new Overlay(drawable);\r
+ }\r
+\r
+ @Override\r
+ public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {\r
+ GL2 gl = drawable.getGL().getGL2();\r
+ GLU glu = new GLU();\r
+\r
+ double ratio = (double) w / (double) h;\r
+ fovX = fovY * ratio;\r
+\r
+ gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);\r
+ gl.glLoadIdentity();\r
+ glu.gluPerspective(fovY, ratio, 0.05f, 100f);\r
+ gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);\r
+ \r
+ redrawExtras = true;\r
+ }\r
+\r
+ @SuppressWarnings("unused")\r
+ private static class Bounds {\r
+ double xMin, xMax, xSize;\r
+ double yMin, yMax, ySize;\r
+ double zMin, zMax, zSize;\r
+ double rMax;\r
+ }\r
+\r
+ /**\r
+ * Calculates the bounds for the current configuration\r
+ * \r
+ * @return\r
+ */\r
+ private Bounds calculateBounds() {\r
+ Bounds ret = new Bounds();\r
+ Collection<Coordinate> bounds = configuration.getBounds();\r
+ for (Coordinate c : bounds) {\r
+ ret.xMax = Math.max(ret.xMax, c.x);\r
+ ret.xMin = Math.min(ret.xMin, c.x);\r
+\r
+ ret.yMax = Math.max(ret.yMax, c.y);\r
+ ret.yMin = Math.min(ret.yMin, c.y);\r
+\r
+ ret.zMax = Math.max(ret.zMax, c.z);\r
+ ret.zMin = Math.min(ret.zMin, c.z);\r
+\r
+ double r = MathUtil.hypot(c.y, c.z);\r
+ ret.rMax = Math.max(ret.rMax, r);\r
+ }\r
+ ret.xSize = ret.xMax - ret.xMin;\r
+ ret.ySize = ret.yMax - ret.yMin;\r
+ ret.zSize = ret.zMax - ret.zMin;\r
+ return ret;\r
+ }\r
+\r
+ private void setupView(GL2 gl, GLU glu) {\r
+ gl.glLoadIdentity();\r
+\r
+ gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION,\r
+ lightPosition, 0);\r
+\r
+ // Get the bounds\r
+ Bounds b = calculateBounds();\r
+\r
+ // Calculate the distance needed to fit the bounds in both the X and Y\r
+ // direction\r
+ // Add 10% for space around it.\r
+ double dX = (b.xSize * 1.2 / 2.0)\r
+ / Math.tan(Math.toRadians(fovX / 2.0));\r
+ double dY = (b.rMax * 2.0 * 1.2 / 2.0)\r
+ / Math.tan(Math.toRadians(fovY / 2.0));\r
+\r
+ // Move back the greater of the 2 distances\r
+ glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0);\r
+\r
+ gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0);\r
+ gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0);\r
+\r
+ // Center the rocket in the view.\r
+ gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0);\r
+ \r
+ //Change to LEFT Handed coordinates\r
+ gl.glScaled(1, 1, -1);\r
+ gl.glFrontFace(GL.GL_CW);\r
+ \r
+ //Flip textures for LEFT handed coords\r
+ gl.glMatrixMode(GL.GL_TEXTURE);\r
+ gl.glLoadIdentity();\r
+ gl.glScaled(-1,1,1);\r
+ gl.glTranslated(-1,0,0);\r
+ gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);\r
+ }\r
+\r
+ /**\r
+ * Call when the rocket has changed\r
+ */\r
+ public void updateFigure() {\r
+ log.debug("3D Figure Updated");\r
+ rr.updateFigure();\r
+ internalRepaint();\r
+ }\r
+\r
+ private void internalRepaint(){\r
+ super.repaint();\r
+ if (canvas != null)\r
+ canvas.display();\r
+ }\r
+ \r
+ @Override\r
+ public void repaint() {\r
+ redrawExtras = true;\r
+ internalRepaint();\r
+ }\r
+\r
+ private Set<RocketComponent> selection = new HashSet<RocketComponent>();\r
+\r
+ public void setSelection(RocketComponent[] selection) {\r
+ this.selection.clear();\r
+ if (selection != null) {\r
+ for (RocketComponent c : selection)\r
+ this.selection.add(c);\r
+ }\r
+ internalRepaint();\r
+ }\r
+\r
+ private void setRoll(double rot) {\r
+ if (MathUtil.equals(roll, rot))\r
+ return;\r
+ this.roll = MathUtil.reduce360(rot);\r
+ internalRepaint();\r
+ }\r
+\r
+ private void setYaw(double rot) {\r
+ if (MathUtil.equals(yaw, rot))\r
+ return;\r
+ this.yaw = MathUtil.reduce360(rot);\r
+ internalRepaint();\r
+ }\r
+\r
+ // ///////////// Extra methods\r
+\r
+ private Coordinate project(Coordinate c, GL2 gl, GLU glu) {\r
+ double[] mvmatrix = new double[16];\r
+ double[] projmatrix = new double[16];\r
+ int[] viewport = new int[4];\r
+\r
+ gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\r
+ gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0);\r
+ gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0);\r
+\r
+ double out[] = new double[4];\r
+ glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0,\r
+ out, 0);\r
+\r
+ return new Coordinate(out[0], out[1], out[2]);\r
+ }\r
+\r
+ private Coordinate cp = new Coordinate(0, 0, 0);\r
+ private Coordinate cg = new Coordinate(0, 0, 0);\r
+\r
+ public void setCG(Coordinate cg) {\r
+ this.cg = cg;\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void setCP(Coordinate cp) {\r
+ this.cp = cp;\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void addRelativeExtra(FigureElement p) {\r
+ relativeExtra.add(p);\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void removeRelativeExtra(FigureElement p) {\r
+ relativeExtra.remove(p);\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void clearRelativeExtra() {\r
+ relativeExtra.clear();\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void addAbsoluteExtra(FigureElement p) {\r
+ absoluteExtra.add(p);\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void removeAbsoluteExtra(FigureElement p) {\r
+ absoluteExtra.remove(p);\r
+ redrawExtras = true;\r
+ }\r
+\r
+ public void clearAbsoluteExtra() {\r
+ absoluteExtra.clear();\r
+ redrawExtras = true;\r
+ }\r
+\r
+ private ComponentSelectionListener csl;\r
+\r
+ public static interface ComponentSelectionListener {\r
+ public void componentClicked(RocketComponent[] components, MouseEvent e);\r
+ }\r
+\r
+ public void addComponentSelectionListener(\r
+ ComponentSelectionListener newListener) {\r
+ this.csl = newListener;\r
+ }\r
+\r
+}\r