/** * */ package cz.cuni.mff.peckam.java.origamist.gui.common; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.image.BufferedImage; import javax.media.j3d.Appearance; import javax.media.j3d.BranchGroup; import javax.media.j3d.Canvas3D; import javax.media.j3d.ImageComponent2D; import javax.media.j3d.PolygonAttributes; import javax.media.j3d.QuadArray; import javax.media.j3d.RenderingAttributes; import javax.media.j3d.Shape3D; import javax.media.j3d.Texture; import javax.media.j3d.Texture2D; import javax.media.j3d.TextureAttributes; import javax.media.j3d.Transform3D; import javax.media.j3d.TransformGroup; import javax.media.j3d.TransparencyAttributes; import javax.vecmath.Vector3d; import com.sun.j3d.utils.universe.SimpleUniverse; /** * An OSD panel to be displayed over Canvas3D. * * @author Martin Pecka */ public class OSDPanel { protected Canvas3D canvas; protected Coordinates coordinates; protected Rectangle bounds; protected BufferedImage textureBuffer; protected BufferedImage paintArea; protected ImageComponent2D textureImage; protected Texture texture; protected Appearance appearance; protected Shape3D shape; protected QuadArray plate; protected Transform3D transform; protected TransformGroup tGroup; protected BranchGroup root; /** * Construct a new OSD panel on the given canvas and with the given bounds. * * @param canvas The canvas this panel is attached to. * @param x Top-left corner's x coordinate in px. * @param y Top-left corner's y coordinate in px. * @param width Width of the panel in px. * @param height Height of the panel in px. * @param fromRight Whether the x coordinate is relative to the right side of canvas or not. * @param fromBottom Whether the y coordinate is relative to the bottom side of canvas or not. */ public OSDPanel(Canvas3D canvas, int x, int y, int width, int height, final boolean fromRight, final boolean fromBottom) { this.canvas = canvas; this.coordinates = new Coordinates(x, y, width, height, fromRight, fromBottom); updateBounds(); canvas.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { repaint(); updateTransform(); } }); Dimension textureSize = getTextureSize(width, height); textureBuffer = new BufferedImage(textureSize.width, textureSize.height, BufferedImage.TYPE_INT_ARGB); paintArea = textureBuffer.getSubimage(0, 0, width, height); float relWidth = (float) width / textureBuffer.getWidth(), relHeight = (float) height / textureBuffer.getHeight(); plate = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); if (fromRight || fromBottom) plate.setCapability(QuadArray.ALLOW_COORDINATE_WRITE); plate.setTextureCoordinates(0, 0, new float[] { 0, 1, relWidth, 1, relWidth, 1 - relHeight, 0, 1 - relHeight }); updatePlateCoords(); textureImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA8, textureSize.width, textureSize.height, true, false); textureImage.setCapability(ImageComponent2D.ALLOW_IMAGE_WRITE); textureImage.set(textureBuffer); texture = new Texture2D(Texture.BASE_LEVEL, Texture.RGBA, textureSize.width, textureSize.height); texture.setImage(0, textureImage); appearance = new Appearance(); appearance.setTexture(texture); appearance.setTextureAttributes(new TextureAttributes()); appearance.getTextureAttributes().setTextureMode(TextureAttributes.REPLACE); appearance.setTransparencyAttributes(new TransparencyAttributes()); appearance.getTransparencyAttributes().setTransparencyMode(TransparencyAttributes.BLENDED); appearance.setPolygonAttributes(new PolygonAttributes()); appearance.getPolygonAttributes().setCullFace(PolygonAttributes.CULL_NONE); appearance.setRenderingAttributes(new RenderingAttributes()); appearance.getRenderingAttributes().setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE); appearance.getRenderingAttributes().setDepthBufferEnable(false); appearance.getRenderingAttributes().setDepthBufferWriteEnable(false); shape = new Shape3D(plate, appearance); shape.setPickable(true); shape.setCapability(Shape3D.ENABLE_PICK_REPORTING); root = new BranchGroup(); root.setCapability(BranchGroup.ALLOW_DETACH); tGroup = new TransformGroup(); tGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); tGroup.addChild(shape); updateTransform(); root.addChild(tGroup); } protected void updateBounds() { this.bounds = coordinates.getBoundsInComponent(canvas); } protected void updateTransform() { // code inspired by j3d.org Overlay library, which is LGPL // get the field of view and then calculate the width in meters of the // screen double c_width = 2 * 2.1 * Math.tan(canvas.getView().getFieldOfView() * 0.5); // calculate the ratio between the canvas in pixels and the screen in // meters and use that to find the height of the screen in meters double scale = c_width / canvas.getWidth(); double c_height = canvas.getHeight() * scale; // The texture is upside down relative to the canvas so this has to // be flipped to be in the right place. bounds needs to have the correct // value to be used in Overlays that relay on it to know their position // like mouseovers // float flipped_x = x; //TODO these values don't look to be correct, but in fact, they are (I think there // cumulate some errors that cancel mutually). float flipped_y = canvas.getHeight() - bounds.height - 2 * bounds.y; // build the plane offset transform = new Transform3D(); Vector3d loc = new Vector3d(-c_width / 2, -c_height / 2 + flipped_y * scale, -2.1); transform.setTranslation(loc); transform.setScale(scale); tGroup.setTransform(transform); } /** * Update coordinates of plate. */ protected void updatePlateCoords() { plate.setCoordinates(0, new float[] { bounds.x, bounds.y + bounds.height, 0, bounds.x + bounds.width, bounds.y + bounds.height, 0, bounds.x + bounds.width, bounds.y, 0, bounds.x, bounds.y, 0 }); } /** * Attach this panel to the given universe. * * @param universe The universe to attach to. */ public void attachToUniverse(SimpleUniverse universe) { root.detach(); universe.getViewingPlatform().getViewPlatformTransform().addChild(root); repaint(); } /** * Detach this panel from the given universe. * * @param universe The universe to detach from. */ public void detachFromUniverse(SimpleUniverse universe) { universe.getViewingPlatform().getViewPlatformTransform().removeChild(root); } /** * Manually force a repaint of this panel. */ public void repaint() { if (coordinates.fromRight || coordinates.fromBottom) { updateBounds(); updatePlateCoords(); } paint(paintArea.createGraphics()); textureImage.set(textureBuffer); canvas.repaint(); } /** * If true, show this panel, otherwise hide it. * * @param visible If the panel should be visible. */ public void setVisible(boolean visible) { appearance.getRenderingAttributes().setVisible(visible); } /** * Paint graphics on the panel. Add all your painting code here. * * @param graphics The graphics used to paint. */ protected void paint(Graphics2D graphics) { } /** * Return the smallest possible texture size so that the whole rectangle of width and height fits into it. * * @param width The minimum width of the texture. * @param height The minimum height of the texture. * @return Size of texture (both dimensions are powers of 2). */ protected Dimension getTextureSize(int width, int height) { return new Dimension(getSmallestPower(width), getSmallestPower(height)); } /** * Return the smallest power of 2 greater than value. * * @param value The value to find the smallest non-less power of. * @return The smallest power of 2 greater than value. */ protected int getSmallestPower(int value) { int n = 1; while (n < value) n <<= 1; return n; } /** * @return The root node of this panel. */ public BranchGroup getRoot() { return root; } /** * @return The bounds. */ public Rectangle getBounds() { return bounds; } /** * @return The paintArea. */ public BufferedImage getPaintArea() { return paintArea; } /** * Utility class that makes it easy to get bounds that "stick" to a component's bottom or right edge. * * @author Martin Pecka */ protected class Coordinates { protected int x, y, width, height; boolean fromRight, fromBottom; public Coordinates(int x, int y, int width, int height, boolean fromRight, boolean fromBottom) { this.x = x; this.y = y; this.width = width; this.height = height; this.fromRight = fromRight; this.fromBottom = fromBottom; } /** * Get the bounds relative to the given component. * * @param component The component to use as container. * @return The bounds of this corrdinates in the component. */ public Rectangle getBoundsInComponent(Component component) { return new Rectangle(!fromRight ? x : component.getWidth() - x - width, !fromBottom ? y : component.getHeight() - y - height, width, height); } } }