Package org.jdesktop.wonderland.modules.affordances.client.jme

Source Code of org.jdesktop.wonderland.modules.affordances.client.jme.TranslateAffordance

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.modules.affordances.client.jme;

import com.jme.bounding.BoundingBox;
import com.jme.bounding.BoundingSphere;
import com.jme.bounding.BoundingVolume;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.GeometricUpdateListener;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Arrow;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.RenderState.StateType;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.Formatter;
import java.util.HashSet;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.jdesktop.mtgame.Entity;
import org.jdesktop.mtgame.RenderManager;
import org.jdesktop.mtgame.RenderUpdater;
import org.jdesktop.wonderland.client.input.Event;
import org.jdesktop.wonderland.client.input.EventClassListener;
import org.jdesktop.wonderland.client.jme.ClientContextJME;
import org.jdesktop.wonderland.client.jme.input.MouseButtonEvent3D;
import org.jdesktop.wonderland.client.jme.input.MouseDraggedEvent3D;
import org.jdesktop.wonderland.client.jme.input.MouseEvent3D;
import org.jdesktop.wonderland.client.jme.input.MouseEvent3D.ButtonId;

/**
* Visual affordance (manipulator) to move a cell around in the world.
*
* @author Jordan Slott <jslott@dev.java.net>
*/
public class TranslateAffordance extends Affordance {

    /* The length scaling factor for each arrow */
    private static final float LENGTH_SCALE = 1.5f;

    /* The width (thickness) for each arrow */
    private static final float THICKNESS = 0.10f;

    /* The constant amount to extent the arrows beyond the size of the object */
    private static final float LENGTH_OFFSET = 0.1f;

    /* The current scale of the affordance w.r.t the size of the cell */
    private float currentScale = LENGTH_SCALE;

    /* The original extent of the object, before it was modified */
    private float extent = 0.0f;

    /* The root of the scene graph of the cell */
    private Node sceneRoot = null;

    /** An enumeration of the axis along which to effect the drag motion */
    public enum TranslateAxis {
        X_AXIS, Y_AXIS, Z_AXIS
    }

    /* The nodes representing the double-edged arrows for each axis */
    private Node xNode = null, yNode = null, zNode = null;

    /* The endities representing the double-edged arrows for each axis */
    private Entity xEntity = null, yEntity = null, zEntity = null;

    /* Listener for changes in the transform of the cell */
    private GeometricUpdateListener updateListener = null;

    /* Listeners for drag events for each axis */
    private TranslateDragListener xListener = null, yListener = null, zListener = null;

    /**
     * Creates a new translate affordance given the cell to  which it is going
     * to be added. Also adds the movable component if not already present
     *
     * @param cell
     * @throw AffordanceException Upon error creating the affordance
     */
    public TranslateAffordance(Node sceneRoot) {
        super("Translate");

        // Figure out the bounds of the root entity of the cell and create an
        // arrow to be just a bit larger than that
        this.sceneRoot = sceneRoot;
        BoundingVolume bounds = sceneRoot.getWorldBound();
        if (bounds instanceof BoundingSphere) {
            extent = ((BoundingSphere)bounds).radius;
        }
        else if (bounds instanceof BoundingBox) {
            float xExtent = ((BoundingBox)bounds).xExtent;
            float yExtent = ((BoundingBox)bounds).yExtent;
            float zExtent = ((BoundingBox)bounds).zExtent;
            extent = Math.max(xExtent, Math.max(yExtent, zExtent));
        }
       
        // Fetch the world translation for the root node of the cell and set
        // the translation for this entity root node
        Vector3f translation = sceneRoot.getWorldTranslation();
        rootNode.setLocalTranslation(translation);
       
        // Create a red arrow in the +x direction. We arrow we get back is
        // pointed in the +y direction, so we rotate around the -z axis to
        // orient the arrow properly.
        xEntity = new Entity("Entity X");
        xNode = createArrow("Arrow X", extent + LENGTH_OFFSET, THICKNESS, ColorRGBA.red);
        Quaternion xRotation = new Quaternion().fromAngleAxis((float)Math.PI / 2, new Vector3f(0, 0, -1));
        xNode.setLocalRotation(xRotation);
        xNode.setLocalScale(new Vector3f(1.0f, LENGTH_SCALE, 1.0f));
        xNode.setRenderState(zbuf);
        addSubEntity(xEntity, xNode);
        xListener = addDragListener(xEntity, xNode, TranslateAxis.X_AXIS);

        // Create a green arrow in the +y direction. We arrow we get back is
        // pointed in the +y direction.
        yEntity = new Entity("Entity Y");
        yNode = createArrow("Arrow Y", extent + LENGTH_OFFSET, THICKNESS, ColorRGBA.green);
        yNode.setLocalScale(new Vector3f(1.0f, LENGTH_SCALE, 1.0f));
        yNode.setRenderState(zbuf);
        addSubEntity(yEntity, yNode);
        yListener = addDragListener(yEntity, yNode, TranslateAxis.Y_AXIS);

        // Create a red arrow in the +z direction. We arrow we get back is
        // pointed in the +y direction, so we rotate around the +x axis to
        // orient the arrow properly.
        zEntity = new Entity("Entity Z");
        zNode = createArrow("Arrow Z", extent + LENGTH_OFFSET, THICKNESS, ColorRGBA.blue);
        Quaternion zRotation = new Quaternion().fromAngleAxis((float)Math.PI / 2, new Vector3f(1, 0, 0));
        zNode.setLocalRotation(zRotation);
        zNode.setRenderState(zbuf);
        zNode.setLocalScale(new Vector3f(1.0f, LENGTH_SCALE, 1.0f));
        addSubEntity(zEntity, zNode);
        zListener = addDragListener(zEntity, zNode, TranslateAxis.Z_AXIS);

        // Listen for changes to the cell's translation and apply the same
        // update to the root node of the affordances. We also re-set the size
        // of the affordances: this handles the case where the bounds of the
        // scene graph has changed and we need to update the affordances
        // accordingly.
        sceneRoot.addGeometricUpdateListener(updateListener = new GeometricUpdateListener() {
            public void geometricDataChanged(final Spatial spatial) {
                // We need to perform this work inside a proper updater, to
                // make sure we are MT thread safe
                RenderUpdater u = new RenderUpdater() {
                    public void update(Object obj) {
                        Vector3f translation = spatial.getWorldTranslation();
                        rootNode.setLocalTranslation(translation);
                        setSizeInternal(currentScale);
                        ClientContextJME.getWorldManager().addToUpdateList(rootNode);
                    }
                };
                ClientContextJME.getWorldManager().addRenderUpdater(u, this);
            }
        });
    }

    /**
     * @inheritDoc()
     */
    public void setSize(final float size) {
        // Sets the size of the affordance in a thread-safe manner
        RenderUpdater u = new RenderUpdater() {
            public void update(Object obj) {
                setSizeInternal(size);
            }
        };
        ClientContextJME.getWorldManager().addRenderUpdater(u, this);
    }

    /**
     * Sets the size of the translate affordance, based upon the bounds of the
     * screen graph of the Cell. Calls of this method should make sure it is
     * invoked properly in MT Game to be thread safe.
     */
    private void setSizeInternal(float size) {
        // To set the scale properly, we need to compute the scale w.r.t the
        // current size of the object as a ratio of the original size of the
        // object (in case the size of the object has changed).
        currentScale = size;
        float xScale = 0.0f, yScale = 0.0f, zScale = 0.0f;
        BoundingVolume bounds = sceneRoot.getWorldBound();
        if (bounds instanceof BoundingSphere) {
            float newExtent = ((BoundingSphere)bounds).radius;
            xScale = yScale = zScale = (newExtent / extent) * currentScale;
        }
        else if (bounds instanceof BoundingBox) {
            float newXExtent = ((BoundingBox)bounds).xExtent;
            float newYExtent = ((BoundingBox)bounds).yExtent;
            float newZExtent = ((BoundingBox)bounds).zExtent;
            float newExtent = Math.max(newXExtent, Math.max(newYExtent, newZExtent));
            xScale = yScale = zScale = (newExtent / extent) * currentScale;
        }

        // In order to set the size of the arrows, we just set the scaling. Note
        // that we set the scaling along the +y axis since all arrows are
        // created facing that direction.
        xNode.setLocalScale(new Vector3f(1.0f, xScale, 1.0f));
        yNode.setLocalScale(new Vector3f(1.0f, yScale, 1.0f));
        zNode.setLocalScale(new Vector3f(1.0f, zScale, 1.0f));
        ClientContextJME.getWorldManager().addToUpdateList(xNode);
        ClientContextJME.getWorldManager().addToUpdateList(yNode);
        ClientContextJME.getWorldManager().addToUpdateList(zNode);
    }

    /**
     * @inheritDoc()
     */
    @Override
    public void dispose() {
        // Call the superclass dispose() first, to make sure the affordance
        // is no longer visible
        super.dispose();

        // Clean up all of the listeners so this class gets properly garbage
        // collected.
        sceneRoot.removeGeometricUpdateListener(updateListener);
        xListener.removeFromEntity(xEntity);
        yListener.removeFromEntity(yEntity);
        zListener.removeFromEntity(zEntity);
        xListener = yListener = zListener = null;
        xEntity = yEntity = zEntity = null;
        updateListener = null;
        listenerSet.clear();
    }

    /**
     * Adds a drag listener for the Entity and the node, given the axis along
     * which the drag should take place.
     */
    private TranslateDragListener addDragListener(Entity entity, Node node,
            TranslateAxis direction) {
       
        makeEntityPickable(entity, node);
        TranslateDragListener l = new TranslateDragListener(direction);
        l.addToEntity(entity);
        return l;
    }

    /**
     * Creates a double-ended arrow, given its half-length, thickness and color.
     * Returns a Node representing the new geometry. Fills in the affordance
     * arrow object with each jME arrow object.
     */
    private Node createArrow(String name, float length, float width, ColorRGBA color) {

        // Create the two arrows with the proper name, length, thickness, and
        // color.
        Arrow a1 = new Arrow(name + " 1", length, width);
        a1.setSolidColor(color);
        Arrow a2 = new Arrow(name + " 2", length, width);
        a2.setSolidColor(color);

        // Create the main node and set the material state on the node so the
        // color shows up
        Node n = new Node();
        RenderManager rm = ClientContextJME.getWorldManager().getRenderManager();
        MaterialState matState3 = (MaterialState)rm.createRendererState(StateType.Material);
        matState3.setDiffuse(color);
        n.setRenderState(matState3);

        // Create a sub-node to hold the first arrow. We must translate it up,
        // so that the end is at (0, 0, 0) in the local coordinate space of
        // the node we return
        Node subNode1 = new Node();
        subNode1.setLocalTranslation(0, length / 2, 0);
        subNode1.attachChild(a1);

        // Create a sub-node to hold the second arrow. We must rotate it 180
        // degrees (about the +y axis since arrows by default point up). We
        // also must translate it down. Attach the second arrow to this node.
        Node subNode2 = new Node();
        Quaternion q = new Quaternion().fromAngleAxis((float)Math.PI, new Vector3f(0, 0, 1));
        subNode2.setLocalRotation(q);
        subNode2.setLocalTranslation(0, -length / 2, 0);
        subNode2.attachChild(a2);

        // Attach the first arrow and the subnode to the main node
        n.attachChild(subNode1);
        n.attachChild(subNode2);

        // Set the bounds on the arrows and update them
        a1.setModelBound(new BoundingSphere());
        a1.updateModelBound();
        a2.setModelBound(new BoundingSphere());
        a2.updateModelBound();

        return n;
    }

    private Set<TranslationListener> listenerSet = new HashSet();

    /**
     * Adds a listener for translation events. If the listener has already
     * been added, this method does nothing.
     *
     * @param listener The listener to add
     */
    public void addTranslationListener(TranslationListener listener) {
        synchronized (listenerSet) {
            listenerSet.add(listener);
        }
    }

    /**
     * Removes a listener for translation events. If the listener does not
     * exist, this method does nothing.
     *
     * @param listener The listener to remove
     */
    public void removeTranslationListener(TranslationListener listener) {
        synchronized (listenerSet) {
            listenerSet.remove(listener);
        }
    }

    /**
     * Informs all of the listeners that a translation has begun
     */
    private void fireTranslationStarted() {
        synchronized (listenerSet) {
            for (TranslationListener listener : listenerSet) {
                listener.translationStarted();
            }
        }
    }

    /**
     * Informs all of the listeners of the new translation
     */
    private void fireTranslationChanged(Vector3f translation) {
        synchronized (listenerSet) {
            for (TranslationListener listener : listenerSet) {
                listener.translationPerformed(translation);
            }
        }
    }
   
    /**
     * Listener for translation events.
     */
    public interface TranslationListener {
        /**
         * Indicates that the translation has begun using the affordance.
         */
        public void translationStarted();
       
        /**
         * Indicates that the translation affordance has been moved by a certain
         * amount, giving a Vector3f.
         *
         * @param translation The translation amount as a 3D vector
         */
        public void translationPerformed(Vector3f translation);
    }

    /**
     * Inner class that handles the dragging movement and updates the position
     * of the cell accordingly
     */
    private class TranslateDragListener extends EventClassListener {

        // The axis along which to effect the translation
        private TranslateAxis direction;

        // The intersection point on the entity over which the button was
        // pressed, in world coordinates.
        private Vector3f dragStartWorld;

        // The screen coordinates of the button press event.
        private Point dragStartScreen;

        // The label (and frame) to display the current drag amount
        private JFrame labelFrame = null;
        private JLabel positionLabel = null;

        public TranslateDragListener(TranslateAxis direction) {
            this.direction = direction;

            // Create a label to display the current drag amount. Since this
            // is not visible yet, we do not need to do it in the AWT Event
            // Thread.
            labelFrame = new JFrame();
            labelFrame.setResizable(false);
            labelFrame.setUndecorated(true);
            labelFrame.getContentPane().setLayout(new GridLayout(1, 1));
            JPanel labelPanel = new JPanel();
            labelPanel.setBackground(Color.WHITE);
            labelPanel.setOpaque(true);
            labelFrame.getContentPane().add(labelPanel);
            labelPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
            labelPanel.setLayout(new GridLayout());
            positionLabel = new JLabel("0.00");
            labelPanel.add(positionLabel);
            labelPanel.invalidate();
            labelFrame.pack();
        }

        @Override
        public Class[] eventClassesToConsume() {
            return new Class[] { MouseEvent3D.class };
        }

        @Override
        public void commitEvent(Event event) {
            // Fetch and cast some event objects
            MouseEvent3D mouseEvent = (MouseEvent3D)event;
            MouseEvent awtMouseEvent = (MouseEvent)mouseEvent.getAwtEvent();

            // Figure out where the initial mouse button press happened and
            // store the initial position
            if (event instanceof MouseButtonEvent3D) {
                MouseButtonEvent3D buttonEvent = (MouseButtonEvent3D) event;
                if (buttonEvent.isPressed() &&
                        buttonEvent.getButton() == ButtonId.BUTTON1) {
                   
                    // Fetch the initial location of the mouse drag event and
                    // store away the necessary information
                    MouseEvent awtButtonEvent = (MouseEvent) buttonEvent.getAwtEvent();
                    dragStartScreen = new Point(awtButtonEvent.getX(), awtButtonEvent.getY());
                    dragStartWorld = buttonEvent.getIntersectionPointWorld();
                   
                    // Show the position label, make sure we do this in an
                    // AWT Event Thread
                    showPositionLabel(awtMouseEvent);

                    // Tell the listeners that a translation has started.
                    fireTranslationStarted();
                }
                else if (buttonEvent.isReleased() == true) {
                    // Hide the position label, make sure we do this in an
                    // AWT Event Thread
                    hidePositionLabel();
                }
                return;
            }

            // If not a drag motion, just return, we don't care about the event
            if (!(event instanceof MouseDraggedEvent3D)) {
                return;
            }

            // Get the vector of the drag motion from the initial starting
            // point in world coordinates.
            MouseDraggedEvent3D dragEvent = (MouseDraggedEvent3D) event;
            Vector3f dragVector = dragEvent.getDragVectorWorld(dragStartWorld,
                    dragStartScreen, new Vector3f());

            // Figure out how to translate based upon the axis of the affordance
            Vector3f addVector;
            float moved = 0.0f;
            switch (direction) {
                case X_AXIS:
                    addVector = new Vector3f(dragVector.x, 0, 0);
                    moved = dragVector.x;
                    break;

                case Y_AXIS:
                    addVector = new Vector3f(0, dragVector.y, 0);
                    moved = dragVector.y;
                    break;

                case Z_AXIS:
                    addVector = new Vector3f(0, 0, dragVector.z);
                    moved = dragVector.z;
                    break;

                default:
                    addVector = new Vector3f();
                    break;
            }

            // Update the position label, make sure we do this in an AWT Event
            // Thread
            updatePositionLabel(moved, awtMouseEvent);

            // Move the cell via the moveable comopnent
            fireTranslationChanged(addVector);
        }

        /**
         * Sets the location of the frame holding the label given the current
         * mouse event, using its location.
         *
         * NOTE: This method assumes it is being called within the AWT Event
         * Thread.
         */
        private void setLabelPosition(MouseEvent mouseEvent) {
            Component component = mouseEvent.getComponent();
            Point parentPoint = new Point(component.getLocationOnScreen());
            parentPoint.translate(mouseEvent.getX() + 10, mouseEvent.getY() - 15);
            labelFrame.setLocation(parentPoint);
        }
       
        /**
         * Shows the position label, properly in an AWT Event THread
         */
       private void showPositionLabel(final MouseEvent mouseEvent) {
           SwingUtilities.invokeLater(new Runnable() {
               public void run() {
                   setLabelPosition(mouseEvent);
                   labelFrame.toFront();
                   labelFrame.setVisible(true);
                   labelFrame.repaint();
               }
           });
       }


        /**
         * Hides the position label, properly in an AWT Event Thread
         */
        private void hidePositionLabel() {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    labelFrame.setVisible(false);
                }
            });
        }

        /**
         * Updates the position label with the amount moved, properly in an
         * AWT Event Thread.
         */
        private void updatePositionLabel(float moved, final MouseEvent mouseEvent) {
            // Set the label with the amount that we have dragged it. We display
            // the dragged amount to two decimal points
            final StringBuilder movedString = new StringBuilder();
            Formatter formatter = new Formatter(movedString);
            formatter.format("%.2f", moved);

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    positionLabel.setText(movedString.toString());
                    labelFrame.pack();
                    setLabelPosition(mouseEvent);
                }
            });
        }
    }
}
TOP

Related Classes of org.jdesktop.wonderland.modules.affordances.client.jme.TranslateAffordance

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.