/** * */ package cz.cuni.mff.peckam.java.origamist.gui.common; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.InputMethodListener; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.Callable; import javax.media.j3d.Appearance; import javax.media.j3d.Behavior; import javax.media.j3d.BoundingSphere; import javax.media.j3d.BranchGroup; import javax.media.j3d.Canvas3D; import javax.media.j3d.ColoringAttributes; import javax.media.j3d.Font3D; import javax.media.j3d.FontExtrusion; import javax.media.j3d.Geometry; import javax.media.j3d.Group; import javax.media.j3d.ImageComponent2D; import javax.media.j3d.LineArray; import javax.media.j3d.LineAttributes; import javax.media.j3d.Material; import javax.media.j3d.OrderedGroup; import javax.media.j3d.OrientedShape3D; import javax.media.j3d.PolygonAttributes; import javax.media.j3d.QuadArray; import javax.media.j3d.RenderingAttributes; import javax.media.j3d.Shape3D; import javax.media.j3d.Text3D; 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.media.j3d.TriangleArray; import javax.swing.AbstractAction; import javax.swing.ImageIcon; import javax.vecmath.AxisAngle4d; import javax.vecmath.Color3f; import javax.vecmath.Matrix3d; import javax.vecmath.Point3d; import javax.vecmath.Point3f; import javax.vecmath.Vector3d; import org.apache.log4j.Level; import org.apache.log4j.Logger; import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.universe.ViewInfo; import cz.cuni.mff.peckam.java.origamist.exceptions.InvalidOperationException; import cz.cuni.mff.peckam.java.origamist.exceptions.PaperStructureException; import cz.cuni.mff.peckam.java.origamist.math.Line3d; import cz.cuni.mff.peckam.java.origamist.math.MathHelper; import cz.cuni.mff.peckam.java.origamist.math.Segment2d; import cz.cuni.mff.peckam.java.origamist.math.Segment3d; import cz.cuni.mff.peckam.java.origamist.model.DoubleDimension; import cz.cuni.mff.peckam.java.origamist.model.Operation; import cz.cuni.mff.peckam.java.origamist.model.OperationContainer; import cz.cuni.mff.peckam.java.origamist.model.Origami; import cz.cuni.mff.peckam.java.origamist.model.Step; import cz.cuni.mff.peckam.java.origamist.model.jaxb.Unit; import cz.cuni.mff.peckam.java.origamist.model.jaxb.UnitDimension; import cz.cuni.mff.peckam.java.origamist.modelstate.Direction; import cz.cuni.mff.peckam.java.origamist.modelstate.MarkerRenderData; import cz.cuni.mff.peckam.java.origamist.modelstate.ModelSegment; import cz.cuni.mff.peckam.java.origamist.modelstate.ModelState; import cz.cuni.mff.peckam.java.origamist.utils.ParametrizedCallable; /** * A controller that performs step rendering onto a given Canvas3D. * <p> * Provided properties: * <ul> * <li>zoom</li> * <li>step (will be called when the setting thread finishes; the old value will always be null).</li> * </ul> * * @author Martin Pecka */ public class StepViewingCanvasController { /** The zoom property. */ public static final String ZOOM_PROPERTY = "zoom"; /** The step property. Will be called when the setting thread finishes; the old value will always be null. */ public static final String STEP_PROPERTY = "step"; /** * The origami diagram we are rendering. */ protected Origami origami = null; /** * The step this renderer is rendering. */ protected Step step = null; /** The offscreen canvas used for drawing. */ protected Canvas3D canvas; /** The universe we use. */ protected SimpleUniverse universe; /** The transform computed from the step. */ protected Transform3D baseTransform = new Transform3D(); /** The main transform used to display the step. */ protected Transform3D transform = new Transform3D(); /** The transform group containing the whole step. */ protected TransformGroup tGroup; /** The group containing the whole model. */ protected Group model; /** The branch graph to be added to the scene. */ protected BranchGroup branchGraph = null; /** The zoom of the step renderer. */ protected double zoom = 100d; /** The font to use for drawing markers. */ protected Font markerFont = new Font("Arial", Font.BOLD, 12); /** The size of the surface texture. */ protected final static int TEXTURE_SIZE = 512; /** The factory that handles different strokes. */ protected StrokeFactory strokeFactory = new StrokeFactory(); /** Cached textures for top and bottom side of the paper. */ protected Texture topTexture, bottomTexture; /** The maximum level of anisotropic filter that is supported by the current HW. */ protected final float maxAnisotropyLevel; /** The manager of {@link StepRenderer}'s colors. */ protected ColorManager colorManager = createColorManager(null, null); /** The manager for changing line appearances. */ protected LineAppearanceManager lineAppearanceManager = createLineAppearanceManager(); /** * These callbacks will handle removing unnecessary callbacks. A callback returning true will be removed from this * list, too, after being called. */ protected List<Callable<Boolean>> removeListenersCallbacks = new LinkedList<Callable<Boolean>>(); /** The transform used for initial zooming when a new origami is set. */ protected Transform3D initialViewTransform = null; protected PropertyChangeSupport support = new PropertyChangeSupport(this); /** The OSD panel displaying image operations. */ protected OSDPanel imageOverlayPanel = null; /** * @param canvas */ public StepViewingCanvasController(Canvas3D canvas) { this.canvas = canvas; // The following resize listener performs scaling of the view as if the offscreen canvas were a square if its // width is longer than its height. // Why is this needed? The canvas adjusts the scale of view according to the canvas' width, but doesn't handle // the canvas' height, so the scale of the rendered image is totally dependent on the canvas' width. This // listener makes it depend on both width and height. canvas.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { adjustSize(); } }); universe = new SimpleUniverse(canvas); MouseListener listener = new MouseListener(); addMouseWheelListener(listener); addMouseMotionListener(listener); addMouseListener(listener); maxAnisotropyLevel = (Float) canvas.queryProperties().get("textureAnisotropicFilterDegreeMax"); } /** * @param canvas * @param origami * @param step */ public StepViewingCanvasController(Canvas3D canvas, Origami origami, Step step) { this(canvas); setOrigami(origami); setStep(step); } /** * @return the origami */ public Origami getOrigami() { return origami; } /** * @param origami the origami to set */ public void setOrigami(Origami origami) { this.origami = origami; initialViewTransform = null; if (origami != null) { createColorManager(origami.getModel().getPaper().getBackgroundColor(), origami.getModel().getPaper() .getForegroundColor()); setZoom(100); } } /** * @return the step */ public Step getStep() { return step; } /** * @param step the step to set */ public void setStep(final Step step) { this.setStep(step, null); } /** * @param step the step to set * @param afterSetCallback The callback to call after the step is changed. Will be run outside EDT. */ public void setStep(final Step step, final Runnable afterSetCallback) { setStep(step, afterSetCallback, null); } /** * @param step the step to set * @param afterSetCallback The callback to call after the step is changed. Will be run outside EDT. * @param exceptionCallback The callback to call if the setting thread encounters an * {@link InvalidOperationException} or {@link PaperStructureException}. Will be run outside EDT. */ public void setStep(final Step step, final Runnable afterSetCallback, final ParametrizedCallable<?, ? super Exception> exceptionCallback) { this.step = step; if (step != null && step.getAttachedTo() == null) { return; } new Thread(new Runnable() { @Override public void run() { Exception exception = null; synchronized (StepViewingCanvasController.this) { try { if (step != null) setupUniverse(); } catch (InvalidOperationException e) { Logger.getLogger("application").l7dlog( Level.ERROR, "StepRenderer.InvalidOperationException", new Object[] { StepViewingCanvasController.this.step.getId(), e.getOperation().toString() }, e); exception = e; } catch (PaperStructureException e) { Logger.getLogger("application").error(e.getMessage(), e); exception = e; } finally { topTexture = null; bottomTexture = null; } } support.firePropertyChange(STEP_PROPERTY, null, step); if (exception == null) { afterSetStep(); if (afterSetCallback != null) afterSetCallback.run(); } else if (exceptionCallback != null) { exceptionCallback.call(exception); } } }).start(); } /** * This method is called after the thread run by a {@link #setStep(Step)} call is about to finish and it didn't end * due to an exception. */ protected void afterSetStep() { adjustSize(); } /** * This method makes sure the scale of the rendered image is appropriate with respect to the renderer's size. */ public void adjustSize() { Dimension size = canvas.getSize(); if (size.width > size.height) { Transform3D trans = computeInitialViewTransform(origami); Transform3D scale = new Transform3D(); scale.set((double) size.width / size.height); trans.mul(scale, trans); universe.getViewingPlatform().getViewPlatformTransform().setTransform(trans); } } /** * @return The model state of the current step. */ protected ModelState getModelState() { return step != null ? step.getModelState(false) : null; } /** * @return The common attributes of polygons to use for rendering. */ protected PolygonAttributes createPolygonAttributes() { PolygonAttributes polyAttribs = new PolygonAttributes(); polyAttribs.setCullFace(PolygonAttributes.CULL_BACK); // DEBUG IMPORTANT: The next line allows switching between wireframe and full filling mode polyAttribs.setPolygonMode(PolygonAttributes.POLYGON_FILL); return polyAttribs; } /** * @return The buffered image used for drawing textures. */ protected BufferedImage createTextureBuffer() { return new BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_RGB); } /** * Return the rectangle the current origami will really ocuppy on the given buffer (assumed you want to scale it to * be the largest possible). * * @param buffer The buffer the origami will be drawn on. * @return The rectangle the origami will ocuppy on the buffer. */ protected Rectangle getUsedBufferPart(BufferedImage buffer) { DoubleDimension paperDim = origami.getModel().getPaper().getRelativeDimensions(); double horizRatio = buffer.getWidth() / paperDim.getWidth(); double vertRatio = buffer.getHeight() / paperDim.getHeight(); double ratio = Math.min(horizRatio, vertRatio); int usedWidth = (int) (ratio * paperDim.getWidth()); int usedHeight = (int) (ratio * paperDim.getHeight()); return new Rectangle(0, 0, usedWidth, usedHeight); } /** * Initialize the texture graphics to be able to draw fold lines on it after this method finishes. * * @param buffer The buffer to create the graphics from. * @param bgColor The background color of the graphics. * * @return The initialized graphics object. */ protected Graphics2D initTextureGraphics(BufferedImage buffer, Color bgColor) { Graphics2D graphics = buffer.createGraphics(); Rectangle usedPart = getUsedBufferPart(buffer); graphics.setColor(bgColor); graphics.setBackground(bgColor); graphics.clearRect(usedPart.x, usedPart.y, usedPart.width, usedPart.height); graphics.setColor(Color.BLACK); graphics.setStroke(new BasicStroke(0.5f)); return graphics; } /** * Create a texture with image taken from the given buffer. Also set some desired texture attributes. * * @param buffer The buffer to take the image from. * @return A texture. */ protected Texture createTextureFromBuffer(BufferedImage buffer) { Texture texture = new Texture2D(Texture2D.BASE_LEVEL, Texture2D.RGB, buffer.getWidth(), buffer.getHeight()); ImageComponent2D image = new ImageComponent2D(ImageComponent2D.FORMAT_RGB, buffer); texture.setMagFilter(Texture.NICEST); texture.setMinFilter(Texture.NICEST); texture.setAnisotropicFilterMode(Texture.ANISOTROPIC_SINGLE_VALUE); texture.setAnisotropicFilterDegree(Math.min(4f, maxAnisotropyLevel)); texture.setImage(0, image); return texture; } /** * @return Return (and generate if it doesn't exist) the texture for the top side of the paper. */ protected Texture getTopTexture() { if (topTexture == null) { BufferedImage buffer = createTextureBuffer(); drawTopTextureToBuffer(buffer); topTexture = createTextureFromBuffer(buffer); } return topTexture; } /** * Draw the top texture onto the specified buffer. * * @param buffer The buffer to draw to. */ protected void drawTopTextureToBuffer(BufferedImage buffer) { Graphics2D graphics = initTextureGraphics(buffer, getColorManager().getForeground()); Rectangle usedPart = getUsedBufferPart(buffer); int x = usedPart.x, y = usedPart.y; int w = usedPart.width, h = usedPart.height; // usedPart contains the really used part of buffer, but we need to compensate that for the shorter side, its // most distant point isn't generally 1, but something less; so we take the inverse ratio and multiply it with // the shorter dimension to compensate this effect DoubleDimension paperDim = origami.getModel().getPaper().getRelativeDimensions(); if (paperDim.getWidth() >= paperDim.getHeight()) { h = (int) (h * paperDim.getWidth() / paperDim.getHeight()); } else { w = (int) (w * paperDim.getHeight() / paperDim.getWidth()); } ModelSegment segment; for (LineArray array : getModelState().getLineArrays()) { segment = (ModelSegment) array.getUserData(); graphics.setStroke(strokeFactory.getForDirection(segment.getDirection(), step.getId() - segment.getOriginatingStepId())); Segment2d seg = segment.getOriginal(); graphics.drawLine(x + (int) (seg.getP1().x * w), y + (int) (h - seg.getP1().y * h), x + (int) (seg.getP2().x * w), y + (int) (h - seg.getP2().y * h)); } } /** * @return Return (and generate if it doesn't exist) the texture for the bottom side of the paper. */ protected Texture getBottomTexture() { if (bottomTexture == null) { BufferedImage buffer = createTextureBuffer(); drawBottomTextureToBuffer(buffer); bottomTexture = createTextureFromBuffer(buffer); } return bottomTexture; } /** * Draw the bottom texture onto the specified buffer. * * @param buffer The buffer to draw to. */ protected void drawBottomTextureToBuffer(BufferedImage buffer) { Graphics2D graphics = initTextureGraphics(buffer, getColorManager().getBackground()); Rectangle usedPart = getUsedBufferPart(buffer); int x = usedPart.x, y = usedPart.y; int w = usedPart.width, h = usedPart.height; // usedPart contains the really used part of buffer, but we need to compensate that for the shorter side, its // most distant point isn't generally 1, but something less; so we take the inverse ratio and multiply it with // the shorter dimension to compensate this effect DoubleDimension paperDim = origami.getModel().getPaper().getRelativeDimensions(); if (paperDim.getWidth() >= paperDim.getHeight()) { h = (int) (h * paperDim.getWidth() / paperDim.getHeight()); } else { w = (int) (w * paperDim.getHeight() / paperDim.getWidth()); } ModelSegment segment; for (LineArray array : getModelState().getLineArrays()) { segment = (ModelSegment) array.getUserData(); graphics.setStroke(strokeFactory.getForDirection(segment.getDirection() == null ? null : segment .getDirection().getOpposite(), step.getId() - segment.getOriginatingStepId())); Segment2d seg = segment.getOriginal(); graphics.drawLine(x + (int) (seg.getP1().x * w), y + (int) (h - seg.getP1().y * h), x + (int) (seg.getP2().x * w), y + (int) (h - seg.getP2().y * h)); } } /** * @return The appearance of triangles that is common for both sides of the paper. */ protected Appearance createBaseTrianglesAppearance() { Appearance appearance = new Appearance(); appearance.setPolygonAttributes(createPolygonAttributes()); ColoringAttributes colAttrs = new ColoringAttributes(); colAttrs.setShadeModel(ColoringAttributes.NICEST); appearance.setColoringAttributes(colAttrs); appearance.setTextureAttributes(new TextureAttributes()); appearance.getTextureAttributes().setPerspectiveCorrectionMode(TextureAttributes.NICEST); appearance.getTextureAttributes().setTextureMode(TextureAttributes.COMBINE); appearance.setRenderingAttributes(new RenderingAttributes()); appearance.setTransparencyAttributes(new TransparencyAttributes()); appearance.getTransparencyAttributes().setTransparencyMode(TransparencyAttributes.NICEST); return appearance; } /** * @return The appearance of triangles that represent the foreground of the paper. */ protected Appearance createNormalTrianglesAppearance() { Appearance appearance = createBaseTrianglesAppearance(); appearance.getColoringAttributes().setColor(getColorManager().getForeground3f()); appearance.setTexture(getTopTexture()); return appearance; } /** * @return The appearance of triangles that represent the background of the paper. */ protected Appearance createInverseTrianglesAppearance() { Appearance appearance = createBaseTrianglesAppearance(); appearance.getPolygonAttributes().setCullFace(PolygonAttributes.CULL_FRONT); appearance.getColoringAttributes().setColor(getColorManager().getBackground3f()); appearance.setTexture(getBottomTexture()); return appearance; } /** * @return The basic appearance of lines representing folds. */ protected Appearance createBasicLinesAppearance() { Appearance appearance = new Appearance(); ColoringAttributes colAttrs = new ColoringAttributes(getColorManager().getLine3f(), ColoringAttributes.NICEST); appearance.setColoringAttributes(colAttrs); appearance.setTransparencyAttributes(new TransparencyAttributes()); appearance.setRenderingAttributes(new RenderingAttributes()); final LineAttributes lineAttrs = new LineAttributes(); lineAttrs.setLineAntialiasingEnable(true); lineAttrs.setCapability(LineAttributes.ALLOW_WIDTH_READ); lineAttrs.setCapability(LineAttributes.ALLOW_WIDTH_WRITE); appearance.setLineAttributes(lineAttrs); addPropertyChangeListener(ZOOM_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { double oldZoom = (Double) evt.getOldValue(); double newZoom = (Double) evt.getNewValue(); lineAttrs.setLineWidth(lineAttrs.getLineWidth() * (float) (newZoom / oldZoom)); } }); return appearance; } /** * @return The current normal of the screen in vworld coordinates. */ public Vector3d getCurrentScreenNormal() { ViewInfo vi = new ViewInfo(canvas.getView()); Transform3D imagePlateToVworld = new Transform3D(); vi.getImagePlateToVworld(canvas, imagePlateToVworld, null); Point3d eyePos = new Point3d(); canvas.getCenterEyeInImagePlate(eyePos); imagePlateToVworld.transform(eyePos); Point3d centerPos = new Point3d(); canvas.getCenterEyeInImagePlate(centerPos); centerPos.z = 0; imagePlateToVworld.transform(centerPos); Vector3d screenNormal = new Vector3d(eyePos); screenNormal.sub(centerPos); screenNormal.normalize(); return screenNormal; } /** * Set this.transform to a new value. * * @return The transform used for the step just after initialization. * * @throws InvalidOperationException If the model state cannot be gotten due to invalid operations. */ protected Transform3D setupTransform() throws InvalidOperationException { ModelState state = getModelState(); ViewInfo vi = new ViewInfo(canvas.getView()); Transform3D imagePlateToVworld = new Transform3D(); vi.getImagePlateToVworld(canvas, imagePlateToVworld, null); Point3d centerPos = new Point3d(); canvas.getCenterEyeInImagePlate(centerPos); centerPos.z = 0; imagePlateToVworld.transform(centerPos); Point3d screenLeftCenter = new Point3d(); canvas.getCenterEyeInImagePlate(screenLeftCenter); screenLeftCenter.x = 0; screenLeftCenter.z = 0; imagePlateToVworld.transform(screenLeftCenter); Vector3d screenDirection = new Vector3d(screenLeftCenter); screenDirection.sub(centerPos); Point3d axis = new Point3d(screenDirection); Transform3D viewingAngleRotation = new Transform3D(); viewingAngleRotation.set(new AxisAngle4d(new Vector3d(axis), state.getViewingAngle() - Math.PI / 2.0)); transform.set(viewingAngleRotation); Vector3d screenNormal = getCurrentScreenNormal(); Transform3D rotation = new Transform3D(); rotation.set(new AxisAngle4d(screenNormal, state.getRotation())); transform.mul(rotation); Transform3D scale = new Transform3D(); scale.setScale(getCompositeZoom() / 100d); transform.mul(scale); if (model != null && model.getBounds() != null) { Point3d modelCenter = new Point3d(); ((BoundingSphere) model.getBounds()).getCenter(modelCenter); modelCenter.negate(); Transform3D translation = new Transform3D(); translation.setTranslation(new Vector3d(modelCenter)); transform.mul(translation); } baseTransform = new Transform3D(transform); return transform; } /** * @return The transform groups containing nodes for displaying markers. */ protected Group getMarkerGroups() { ModelState state = getModelState(); final BranchGroup result = new BranchGroup(); double oneRelInMeters = origami.getModel().getPaper().getOneRelInMeters(); Font3D font = new Font3D(markerFont, new FontExtrusion()); // scale of the 3D font; this should make 12pt font be 1/10 of the side of the paper large double scale = 1d / 12d * oneRelInMeters * 1d / 10d; Appearance textAp = new Appearance(); Material m = new Material(); textAp.setMaterial(m); textAp.setColoringAttributes(new ColoringAttributes(getColorManager().getMarker3f(), ColoringAttributes.FASTEST)); if (textAp.getRenderingAttributes() == null) textAp.setRenderingAttributes(new RenderingAttributes()); // draw markers always on the top textAp.getRenderingAttributes().setDepthTestFunction(RenderingAttributes.ALWAYS); for (MarkerRenderData marker : state.getMarkerRenderData()) { // we use Text3D here bacause Text2D looks very, very blurry (even if it isn't zoomed) Text3D textGeom = new Text3D(font, marker.getText()); Transform3D transform = new Transform3D(); transform.setScale(scale); Vector3d translation = new Vector3d(marker.getPoint3d()); translation.scale(oneRelInMeters); transform.setTranslation(translation); // TODO add a behavior for fine-positioning colliding markers result.addChild(createBillboard(textGeom, textAp, transform)); } return result; } /** * @return The node containing all signs of current step's operations. */ protected Group getOperationSignsGroup() { final BranchGroup result = new BranchGroup(); final Matrix3d rot = new Matrix3d(); baseTransform.get(rot, new Vector3d()); rot.invert(); // inverted rotation component of baseTransform Transform3D baseRotInv = new Transform3D(rot, new Vector3d(), 1); double oneRelInMeters = origami.getModel().getPaper().getOneRelInMeters(); // width and height of the shape double width = 0.2, height = 0.2; // used for auto-placing signs with no explicit location int usedCorners = 0; Queue<Operation> operations = new LinkedList<Operation>(getStep().getOperations()); while (!operations.isEmpty()) { Operation o = operations.poll(); // do not add signs for operations hidden in the step by a repeat operation if (o instanceof OperationContainer) { OperationContainer oc = (OperationContainer) o; if (oc.areContentsVisible()) { operations.addAll(oc.getOperations()); } } ImageIcon image = o.getIcon(); if (image == null) continue; Point3d startPoint; Segment3d markerSegment = o.getMarkerPosition(); if (markerSegment != null) { startPoint = new Point3d(markerSegment.getP1()); } else { double x, y; int mod = usedCorners % 4; double radius = ((BoundingSphere) model.getBounds()).getRadius() * Math.sqrt(2) / oneRelInMeters; x = (mod == 0 || mod == 3 ? -width / 2 : radius + width / 2) + ((usedCorners / 4) * width * (mod == 0 || mod == 3 ? -1 : 1)); y = (mod > 1 ? radius + height / 2 : -height / 2) + ((usedCorners / 4) * height * (mod > 1 ? 1 : -1)); startPoint = new Point3d(x, y, 0); usedCorners++; } float w = image.getIconWidth(), h = image.getIconHeight(); float hRatio = 1, vRatio = 1; // aspect ratio of the image to draw if (w < h) hRatio = w / h; else vRatio = h / w; // textures need power-of-2 long sides Dimension textureSize = getTextureSize((int) w, (int) h); // horizontal and vertical scale of the real image in the texture float hScale = w / textureSize.width, vScale = h / textureSize.height; // the quadrilateral for displaying the image QuadArray geom = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); geom.setCoordinates(0, new double[] { -width / 2 * hRatio, -height / 2 * vRatio, 0, width / 2 * hRatio, -height / 2 * vRatio, 0, width / 2 * hRatio, height / 2 * vRatio, 0, -width / 2 * hRatio, height / 2 * vRatio, 0 }); geom.setTextureCoordinates(0, 0, new float[] { 0, 1 - vScale, hScale, 1 - vScale, hScale, 1, 0, 1 }); // setup the texture and other appearance BufferedImage buffer = new BufferedImage(textureSize.width, textureSize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = buffer.createGraphics(); g.setBackground(new Color(0, 0, 0, 0)); g.clearRect(0, 0, textureSize.width, textureSize.height); g.drawImage(image.getImage(), 0, 0, null); ImageComponent2D tImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA8, buffer); Texture2D texture = new Texture2D(Texture2D.BASE_LEVEL, Texture2D.RGBA, textureSize.width, textureSize.height); texture.setImage(0, tImage); texture.setMinFilter(Texture.NICEST); texture.setMagFilter(Texture.NICEST); Appearance app = new Appearance(); app.setRenderingAttributes(new RenderingAttributes()); app.getRenderingAttributes().setDepthTestFunction(RenderingAttributes.ALWAYS); app.getRenderingAttributes().setDepthBufferEnable(false); app.getRenderingAttributes().setDepthBufferWriteEnable(false); app.setPolygonAttributes(new PolygonAttributes()); app.getPolygonAttributes().setCullFace(PolygonAttributes.CULL_NONE); app.setTextureAttributes(new TextureAttributes()); app.setColoringAttributes(new ColoringAttributes(new Color3f(Color.yellow), ColoringAttributes.NICEST)); app.getTextureAttributes().setPerspectiveCorrectionMode(TextureAttributes.NICEST); app.getTextureAttributes().setTextureMode(TextureAttributes.REPLACE); app.setTransparencyAttributes(new TransparencyAttributes()); app.getTransparencyAttributes().setTransparencyMode(TransparencyAttributes.BLENDED); app.setTexture(texture); // the transform of our shape Transform3D transform = new Transform3D(); if (markerSegment != null) { // now we want to align the shape's initial x axis with the line we have found for this operation Vector3d dir = new Vector3d(1, 0, 0); baseRotInv.transform(dir); // if the x axis isn't parallel to the found line, create a rotation matrix between them Double quotient = MathHelper.vectorQuotient3d(markerSegment.getVector(), dir); if (quotient == null) { // take the cross product of the two vectors and compute the angle between them - then we can // construct an AxisAngle4d Vector3d cross = new Vector3d(); cross.cross(markerSegment.getVector(), dir); double angle = markerSegment.getVector().angle(dir); // angle is cropped to [0,PI], but it can be larger, so detect if we need the larger angle Vector3d v = new Vector3d(markerSegment.getVector()); v.normalize(); Point3d p1 = new Point3d(markerSegment.getP2()); p1.add(v); Point3d p2 = new Point3d(markerSegment.getP2()); p2.add(dir); if (!MathHelper.rotate(p1, new Line3d(markerSegment.getP1(), cross), angle).epsilonEquals(p2, 1000 * MathHelper.EPSILON)) { angle = -angle; } // let the shape's normal point to the screen Vector3d screenNormal = step.getModelState(false).getScreenNormal(); transform.set(new AxisAngle4d(cross, angle)); Vector3d shapeNormal = new Vector3d(0, 0, 1); baseRotInv.transform(shapeNormal); transform.transform(shapeNormal); transform.transform(dir); Transform3D additionalTransform = new Transform3D(); additionalTransform.set(new AxisAngle4d(dir, shapeNormal.angle(screenNormal))); transform.mul(additionalTransform, transform); } else if (quotient < 0) { Vector3d axis = new Vector3d(0, 0, 1); baseRotInv.transform(axis); transform.set(new AxisAngle4d(axis, Math.PI)); } } startPoint.scale(oneRelInMeters); // scale of the shape - if we have found a line for this operation, scale this shape to stretch along whole // line double scale; if (markerSegment != null) scale = 2 * markerSegment.getLength() / (width * hRatio) * oneRelInMeters; else scale = oneRelInMeters; transform.setScale(transform.getScale() * scale); transform.setTranslation(new Vector3d(startPoint)); // cancel the rotation imposed by baseTransform transform.mul(baseRotInv); // let the upper-left corner match the start of the found segment Transform3D initialTranslation = new Transform3D(); initialTranslation.setTranslation(new Vector3d(width / 2 * hRatio, height / 2 * vRatio, 0)); transform.mul(initialTranslation); // create the necessary nodes TransformGroup group = new TransformGroup(); Shape3D shape = new Shape3D(geom, app); group.addChild(shape); group.setTransform(transform); result.addChild(group); } return result; } /** * Create a billboard node from the given geometry, appearance and transform. The billboard will rotate about point * (0,0,0) in the local coordiantes given by transform. * * @param geometry Geometry of the billboard. * @param appearance Appearance of the billboard. * @param transform Transform of the billboard. * @return The node representing the billboard. */ protected Group createBillboard(Geometry geometry, Appearance appearance, Transform3D transform) { TransformGroup group = new TransformGroup(); group.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); group.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); OrientedShape3D billboard = new OrientedShape3D(geometry, appearance, OrientedShape3D.ROTATE_ABOUT_POINT, new Point3f()); group.addChild(billboard); group.setTransform(transform); return group; } /** * Set this.tGroup to a new value. * * @return The transform group that contains all nodes. * * @throws InvalidOperationException If the model state cannot be gotten due to invalid operations. */ protected TransformGroup setupTGroup() throws InvalidOperationException { try { ModelState state = getModelState(); tGroup = new TransformGroup(); tGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); tGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); OrderedGroup og = new OrderedGroup(); model = new TransformGroup(); TriangleArray[] triangleArrays = state.getTrianglesArrays(); Shape3D top, bottom; Appearance appearance; Appearance appearance2; for (TriangleArray triangleArray : triangleArrays) { appearance = createNormalTrianglesAppearance(); appearance2 = createInverseTrianglesAppearance(); top = new Shape3D(triangleArray, appearance); bottom = new Shape3D(triangleArray, appearance2); model.addChild(top); model.addChild(bottom); } LineArray[] lineArrays = state.getLineArrays(); Group lines = new TransformGroup(); Appearance appearance3; Shape3D shape; for (LineArray lineArray : lineArrays) { ModelSegment segment = (ModelSegment) lineArray.getUserData(); appearance3 = createBasicLinesAppearance(); getLineAppearanceManager().alterBasicAppearance(appearance3, segment.getDirection(), step.getId() - segment.getOriginatingStepId()); shape = new Shape3D(lineArray, appearance3); lines.addChild(shape); } model.addChild(lines); og.addChild(model); setupTransform(); tGroup.setTransform(transform); og.addChild(getOperationSignsGroup()); og.addChild(getMarkerGroups()); tGroup.addChild(og); return tGroup; } catch (InvalidOperationException e) { tGroup = new TransformGroup(); tGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); // TODO create an ErrorTransformGroup that would signalize to the user that an operation is invalid throw e; } } /** * Return the compiled BranchGroup containing this.tGroup and all defined top-level behaviors. Also save the group * to this.branchGraph. * * @return The compiled BranchGroup containing this.tGroup and all defined top-level behaviors. * * @throws InvalidOperationException If the branch graph cannot be created due to an invalid operation in the model. */ protected BranchGroup createBranchGraph() throws InvalidOperationException { try { branchGraph = new BranchGroup(); branchGraph.setCapability(BranchGroup.ALLOW_DETACH); createAndAddBranchGraphChildren(); if (imageOverlayPanel != null) { imageOverlayPanel.detachFromUniverse(universe); imageOverlayPanel = null; } if (getModelState().getOverlayImage() != null) { imageOverlayPanel = new OSDPanel(canvas, 0, 0, canvas.getWidth(), canvas.getHeight(), false, false) { @Override protected void paint(Graphics2D graphics) { BufferedImage image = getModelState().getOverlayImage(); double h, v; h = (double) canvas.getWidth() / image.getWidth(); v = (double) canvas.getHeight() / image.getHeight(); double ratio = Math.min(h, v); int width = (int) (image.getWidth() * ratio); int height = (int) (image.getHeight() * ratio); graphics.drawImage(image, (canvas.getWidth() - width) / 2, (canvas.getHeight() - height) / 2, width, height, null); } }; imageOverlayPanel.attachToUniverse(universe); branchGraph.removeChild(tGroup); } branchGraph.compile(); // may cause unexpected problems - any consequent change of contents // (or even reading of them) will produce an error if you don't set the proper capability return branchGraph; } catch (InvalidOperationException e) { branchGraph = new BranchGroup(); branchGraph.setCapability(BranchGroup.ALLOW_DETACH); branchGraph.addChild(tGroup); branchGraph.compile(); // may cause unexpected problems - any consequent change of contents // (or even reading of them) will produce an error if you don't set the proper capability throw e; } } /** * Create and attach all desired branchGraph children. * * @throws InvalidOperationException If a child cannot be created due to an invalid operation in the model. */ protected void createAndAddBranchGraphChildren() throws InvalidOperationException { setupTGroup(); branchGraph.addChild(tGroup); Behavior rotate = new CenteredMouseRotate(tGroup) { @Override public void transformChanged(Transform3D transform) { StepViewingCanvasController.this.transform = transform; } }; rotate.setSchedulingBounds(new BoundingSphere(new Point3d(), 1000000d)); branchGraph.addChild(rotate); } /** * Set a new universe corresponding to the current step to this.universe. * * @throws InvalidOperationException If the universe cannot be setup due to an invalid operation in the model. */ protected void setupUniverse() throws InvalidOperationException { if (universe == null) { universe = new SimpleUniverse(canvas); canvas.getView().setFrontClipDistance(0); } BranchGroup oldBranchGraph = branchGraph; try { createBranchGraph(); } finally { if (oldBranchGraph != null) universe.getLocale().removeBranchGraph(oldBranchGraph); universe.addBranchGraph(branchGraph); if (initialViewTransform == null && origami != null) { initialViewTransform = computeInitialViewTransform(origami); universe.getViewingPlatform().getViewPlatformTransform().setTransform(initialViewTransform); } } } /** * Compute the transform to be set to view platform in order the given origami's paper to be well zoomed. * * @param origami The origami to compute transform for. * @return The transform for viewing platform. */ protected Transform3D computeInitialViewTransform(Origami origami) { Transform3D result = new Transform3D(); if (origami == null) return result; UnitDimension dimension = origami.getModel().getPaper().getSize().convertTo(Unit.M); double length = Math.sqrt(Math.pow(dimension.getWidth(), 2) + Math.pow(dimension.getHeight(), 2)); // inspired in ViewingPlatform#setNominalViewingTransform() // length specifies (in vworld meters) half of the visible part of the origami result.setTranslation(new Vector3d(0, 0, (length /* / 1d */) / Math.tan(Math.PI / 8d))); return result; } /** * Create a new color manager for the given colors. * * @param background The background color (if null, WHITE is used). * @param foreground The foreground color (if null, WHITE is used). * @return The created color manager. */ protected ColorManager createColorManager(Color background, Color foreground) { return colorManager = new ColorManager(background == null ? Color.WHITE : background, foreground == null ? Color.WHITE : foreground); } /** * @return The current color manager. */ protected ColorManager getColorManager() { if (colorManager == null) createColorManager(null, null); return colorManager; } /** * Create a new line appearance manager. * * @return The created line appearance manager. */ protected LineAppearanceManager createLineAppearanceManager() { return lineAppearanceManager = new LineAppearanceManager(); } /** * @return The current line appearance manager. */ protected LineAppearanceManager getLineAppearanceManager() { if (lineAppearanceManager == null) createLineAppearanceManager(); return lineAppearanceManager; } /** * @return The overall zoom of the displayed object (as percentage - 0 to 100). */ public double getCompositeZoom() { if (step != null) return step.getZoom() * zoom / 100d; else return zoom; } /** * @return the zoom */ public double getZoom() { return zoom; } /** * @param zoom the zoom to set */ public void setZoom(double zoom) { if (zoom < 25d) return; double oldZoom = this.zoom; this.zoom = zoom; support.firePropertyChange("zoom", oldZoom, zoom); if (step != null && getModelState() != null && model != null) { Transform3D additional = new Transform3D(transform); Transform3D baseInverted = new Transform3D(baseTransform); baseInverted.invert(); additional.mul(baseInverted); setupTransform(); transform.mul(additional, transform); if (tGroup != null) tGroup.setTransform(transform); } } /** * Increase zoom by 10%. */ public void incZoom() { setZoom(getZoom() + 10d); } /** * Decrease zoom by 10%. */ public void decZoom() { setZoom(getZoom() - 10d); } /** * @return The font to use for drawing markers. */ Font getMarkerFont() { return markerFont; } /** * @param markerFont The font to use for drawing markers. */ void setMarkerFont(Font markerFont) { this.markerFont = markerFont; } /** * Call all removeListenersCallbacks and remove those that have succeeded. */ protected void removeUnnecessaryListeners() { for (Iterator<Callable<Boolean>> it = removeListenersCallbacks.iterator(); it.hasNext();) { try { if (it.next().call()) it.remove(); } catch (Exception e) { Logger.getLogger(getClass()).error("Listener removal callback threw exception.", e); } } } /** * @return The canvas. */ public Canvas3D getCanvas() { return canvas; } /** * 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; } /** * @param l * @see java.awt.Component#addKeyListener(java.awt.event.KeyListener) */ public void addKeyListener(KeyListener l) { canvas.addKeyListener(l); } /** * @param l * @see java.awt.Component#addMouseListener(java.awt.event.MouseListener) */ public void addMouseListener(java.awt.event.MouseListener l) { canvas.addMouseListener(l); } /** * @param l * @see java.awt.Component#addMouseMotionListener(java.awt.event.MouseMotionListener) */ public void addMouseMotionListener(MouseMotionListener l) { canvas.addMouseMotionListener(l); } /** * @param l * @see java.awt.Component#addMouseWheelListener(java.awt.event.MouseWheelListener) */ public void addMouseWheelListener(MouseWheelListener l) { canvas.addMouseWheelListener(l); } /** * @param l * @see java.awt.Component#addInputMethodListener(java.awt.event.InputMethodListener) */ public void addInputMethodListener(InputMethodListener l) { canvas.addInputMethodListener(l); } /** * @param listener * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener) */ public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); } /** * @param listener * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener) */ public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } /** * @param propertyName * @param listener * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.lang.String, * java.beans.PropertyChangeListener) */ public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.addPropertyChangeListener(propertyName, listener); } /** * @param propertyName * @param listener * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.lang.String, * java.beans.PropertyChangeListener) */ public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.removePropertyChangeListener(propertyName, listener); } /** * @param propertyName * @return * @see java.beans.PropertyChangeSupport#hasListeners(java.lang.String) */ public boolean hasListeners(String propertyName) { return support.hasListeners(propertyName); } /** * Mouse event and picking handling. * * @author Martin Pecka */ protected class MouseListener extends MouseAdapter { @Override public void mouseWheelMoved(MouseWheelEvent e) { e.consume(); int steps = e.getWheelRotation(); if (steps == 0) return; if (e.isControlDown() || (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) > 0) { if (steps > 0) { for (int i = 0; i < steps; i++) incZoom(); } else { for (int i = steps; i < 0; i++) decZoom(); } } } } protected class ZoomInAction extends AbstractAction { /** */ private static final long serialVersionUID = 313512643556762110L; @Override public void actionPerformed(ActionEvent e) { incZoom(); } } protected class ZoomOutAction extends AbstractAction { /** */ private static final long serialVersionUID = -5340289900894828612L; @Override public void actionPerformed(ActionEvent e) { decZoom(); } } /** * Manager of all colors used in this {@link StepRenderer}. * * @author Martin Pecka */ protected class ColorManager { /** Color of textual markers' text. */ protected Color marker = Color.BLACK; /** Paper background color. */ protected Color background; /** Paper foreground color. */ protected Color foreground; /** Color of a fold line. */ protected Color line = Color.BLACK; /** * @param background Paper background color. * @param foreground Paper foreground color. */ public ColorManager(Color background, Color foreground) { this.background = background; this.foreground = foreground; } /** * @return Color of textual markers' text. */ public Color getMarker() { return marker; } /** * @return Color of textual markers' text. */ public Color3f getMarker3f() { return new Color3f(marker); } /** * @param marker Color of textual markers' text. */ public void setMarker(Color marker) { this.marker = marker; } /** * @return Paper background color. */ public Color getBackground() { return background; } /** * @return Paper background color. */ public Color3f getBackground3f() { return new Color3f(background); } /** * @param background Paper background color. */ public void setBackground(Color background) { this.background = background; } /** * @return Paper foreground color. */ public Color getForeground() { return foreground; } /** * @return Paper foreground color. */ public Color3f getForeground3f() { return new Color3f(foreground); } /** * @param foreground Paper foreground color. */ public void setForeground(Color foreground) { this.foreground = foreground; } /** * @return Color of a fold line. */ public Color getLine() { return line; } /** * @return Color of a fold line. */ public Color3f getLine3f() { return new Color3f(line); } /** * @param line Color of a fold line. */ public void setLine(Color line) { this.line = line; } } /** * A factory that handles different {@link Stroke}s. * * @author Martin Pecka */ protected class StrokeFactory { private final float[] mountainStroke = new float[] { 20f, 5f, 3f, 5f }; private final float[] valleyStroke = new float[] { 20f, 10f }; /** Array of strokes - first is fold type, then fold age. */ protected Stroke[][] textureStrokes; public StrokeFactory() { textureStrokes = new Stroke[Direction.values().length + 1][]; textureStrokes[getIndex(null)] = new Stroke[] { new BasicStroke(3f) }; textureStrokes[getIndex(Direction.MOUNTAIN)] = new Stroke[] { new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, mountainStroke, 0f), new BasicStroke(2f), new BasicStroke(1f), new BasicStroke(0.5f) }; textureStrokes[getIndex(Direction.VALLEY)] = new Stroke[] { new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, valleyStroke, 0f), new BasicStroke(2f), new BasicStroke(1f), new BasicStroke(0.5f) }; } protected int getIndex(Direction dir) { if (dir != null) return dir.ordinal(); else return Direction.values().length; } /** * Get the stroke to paint a fold line with. * * @param direction The direction of the fold line. * @param age The age of the fold line (in steps, 0 means this step). * @return The stroke to paint a fold line with. */ public Stroke getForDirection(Direction direction, int age) { Stroke[] byAge = textureStrokes[getIndex(direction)]; if (age < byAge.length) return byAge[age]; else return byAge[byAge.length - 1]; } } /** * A manager for changing line appearance. * * @author Martin Pecka */ protected class LineAppearanceManager { /** * Take a basic appearance and set it up according to the direction and age of the fold it represents. * * @param app The appearance to setup. * @param dir The direction of the fold. * @param age The age of the fold (in steps). */ public void alterBasicAppearance(Appearance app, Direction dir, int age) { if (app == null) throw new NullPointerException(); app.getLineAttributes().setLineWidth(getLineWidth(dir, age)); if (dir == null) { return; } if (age == 0) { app.getLineAttributes().setLinePattern(LineAttributes.PATTERN_DASH); app.getRenderingAttributes().setVisible(false); } } /** * Get the width of a line representing a fold. * * @param dir The direction of the fold. * @param age The age of the fold (in steps). */ protected float getLineWidth(Direction dir, int age) { if (dir == null) { return 2f; } switch (age) { case 0: return 1.25f; case 1: return 1.25f; case 2: return 0.85f; default: return 0.5f; } } } }