TransformExplorer.java Source code

Java tutorial

Introduction

Here is the source code for TransformExplorer.java

Source

/*
 * %Z%%M% %I% %E% %U%
 * 
 * ************************************************************** "Copyright (c)
 * 2001 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * -Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
 * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
 * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
 * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGES.
 * 
 * You acknowledge that Software is not designed,licensed or intended for use in
 * the design, construction, operation or maintenance of any nuclear facility."
 * 
 * ***************************************************************************
 */

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.text.NumberFormat;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.Vector;

import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.Font3D;
import javax.media.j3d.ImageComponent;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.Link;
import javax.media.j3d.Material;
import javax.media.j3d.OrientedShape3D;
import javax.media.j3d.Screen3D;
import javax.media.j3d.SharedGroup;
import javax.media.j3d.Switch;
import javax.media.j3d.Text3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.View;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.vecmath.AxisAngle4f;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
import com.sun.j3d.utils.geometry.Cone;
import com.sun.j3d.utils.geometry.Cylinder;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.ViewingPlatform;

/*
 *  
 */

public class TransformExplorer extends Applet implements Java3DExplorerConstants {

    SimpleUniverse u;

    boolean isApplication;

    Canvas3D canvas;

    OffScreenCanvas3D offScreenCanvas;

    View view;

    TransformGroup coneTG;

    // transformation factors for the cone
    Vector3f coneTranslation = new Vector3f(0.0f, 0.0f, 0.0f);

    float coneScale = 1.0f;

    Vector3d coneNUScale = new Vector3d(1.0f, 1.0f, 1.0f);

    Vector3f coneRotateAxis = new Vector3f(1.0f, 0.0f, 0.0f);

    Vector3f coneRotateNAxis = new Vector3f(1.0f, 0.0f, 0.0f);

    float coneRotateAngle = 0.0f;

    AxisAngle4f coneRotateAxisAngle = new AxisAngle4f(coneRotateAxis, coneRotateAngle);

    Vector3f coneRefPt = new Vector3f(0.0f, 0.0f, 0.0f);

    // this tells whether to use the compound transformation
    boolean useCompoundTransform = true;

    // These are Transforms are used for the compound transformation
    Transform3D translateTrans = new Transform3D();

    Transform3D scaleTrans = new Transform3D();

    Transform3D rotateTrans = new Transform3D();

    Transform3D refPtTrans = new Transform3D();

    Transform3D refPtInvTrans = new Transform3D();

    // this tells whether to use the uniform or non-uniform scale when
    // updating the compound transform
    boolean useUniformScale = true;

    // The size of the cone
    float coneRadius = 1.0f;

    float coneHeight = 2.0f;

    // The axis indicator, used to show the rotation axis
    RotAxis rotAxis;

    boolean showRotAxis = false;

    float rotAxisLength = 3.0f;

    // The coord sys used to show the coordinate system
    CoordSys coordSys;

    boolean showCoordSys = true;

    float coordSysLength = 5.0f;

    // GUI elements
    String rotAxisString = "Rotation Axis";

    String coordSysString = "Coord Sys";

    JCheckBox rotAxisCheckBox;

    JCheckBox coordSysCheckBox;

    String snapImageString = "Snap Image";

    String outFileBase = "transform";

    int outFileSeq = 0;

    float offScreenScale;

    JLabel coneRotateNAxisXLabel;

    JLabel coneRotateNAxisYLabel;

    JLabel coneRotateNAxisZLabel;

    // Temporaries that are reused
    Transform3D tmpTrans = new Transform3D();

    Vector3f tmpVector = new Vector3f();

    AxisAngle4f tmpAxisAngle = new AxisAngle4f();

    // geometric constant
    Point3f origin = new Point3f();

    Vector3f yAxis = new Vector3f(0.0f, 1.0f, 0.0f);

    // Returns the TransformGroup we will be editing to change the transform
    // on the cone
    TransformGroup createConeTransformGroup() {

        // create a TransformGroup for the cone, allow tranform changes,
        coneTG = new TransformGroup();
        coneTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        coneTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);

        // Set up an appearance to make the Cone with red ambient,
        // black emmissive, red diffuse and white specular coloring
        Material material = new Material(red, black, red, white, 64);
        // These are the colors used for the book figures:
        //Material material = new Material(white, black, white, black, 64);
        Appearance appearance = new Appearance();
        appearance.setMaterial(material);

        // create the cone and add it to the coneTG
        Cone cone = new Cone(coneRadius, coneHeight, appearance);
        coneTG.addChild(cone);

        return coneTG;
    }

    void setConeTranslation() {
        coneTG.getTransform(tmpTrans); // get the old transform
        tmpTrans.setTranslation(coneTranslation); // set only translation
        coneTG.setTransform(tmpTrans); // set the new transform
    }

    void setConeUScale() {
        coneTG.getTransform(tmpTrans); // get the old transform
        tmpTrans.setScale(coneScale); // set only scale
        coneTG.setTransform(tmpTrans); // set the new transform
    }

    void setConeNUScale() {
        coneTG.getTransform(tmpTrans); // get the old transform
        System.out.println("coneNUScale.x = " + coneNUScale.x);
        tmpTrans.setScale(coneNUScale);// set only scale
        coneTG.setTransform(tmpTrans); // set the new transform
    }

    void setConeRotation() {
        coneTG.getTransform(tmpTrans); // get the old transform
        tmpTrans.setRotation(coneRotateAxisAngle); // set only rotation
        coneTG.setTransform(tmpTrans); // set the new transform
    }

    void updateUsingCompoundTransform() {
        // set the component transformations
        translateTrans.set(coneTranslation);
        if (useUniformScale) {
            scaleTrans.set(coneScale);
        } else {
            scaleTrans.setIdentity();
            scaleTrans.setScale(coneNUScale);
        }
        rotateTrans.set(coneRotateAxisAngle);

        // translate from ref pt to origin
        tmpVector.sub(origin, coneRefPt); // vector from ref pt to origin
        refPtTrans.set(tmpVector);

        // translate from origin to ref pt
        tmpVector.sub(coneRefPt, origin); // vector from origin to ref pt
        refPtInvTrans.set(tmpVector);

        // now build up the transfomation
        // trans = translate * refPtInv * scale * rotate * refPt;
        tmpTrans.set(translateTrans);
        tmpTrans.mul(refPtInvTrans);
        tmpTrans.mul(scaleTrans);
        tmpTrans.mul(rotateTrans);
        tmpTrans.mul(refPtTrans);

        // Copy the transform to the TransformGroup
        coneTG.setTransform(tmpTrans);
    }

    // ensure that the cone rotation axis is a unit vector
    void normalizeConeRotateAxis() {
        // normalize, watch for length == 0, if so, then use default
        float lengthSquared = coneRotateAxis.lengthSquared();
        if (lengthSquared > 0.0001) {
            coneRotateNAxis.scale((float) (1.0 / Math.sqrt(lengthSquared)), coneRotateAxis);
        } else {
            coneRotateNAxis.set(1.0f, 0.0f, 0.0f);
        }
    }

    // copy the current axis and angle to the axis angle, convert angle
    // to radians
    void updateConeAxisAngle() {
        coneRotateAxisAngle.set(coneRotateNAxis, (float) Math.toRadians(coneRotateAngle));
    }

    void updateConeRotateNormalizedLabels() {
        nf.setMinimumFractionDigits(2);
        nf.setMaximumFractionDigits(2);
        coneRotateNAxisXLabel.setText("X: " + nf.format(coneRotateNAxis.x));
        coneRotateNAxisYLabel.setText("Y: " + nf.format(coneRotateNAxis.y));
        coneRotateNAxisZLabel.setText("Z: " + nf.format(coneRotateNAxis.z));
    }

    BranchGroup createSceneGraph() {
        // Create the root of the branch graph
        BranchGroup objRoot = new BranchGroup();

        // Create a TransformGroup to scale the scene down by 3.5x
        TransformGroup objScale = new TransformGroup();
        Transform3D scaleTrans = new Transform3D();
        scaleTrans.set(1 / 3.5f); // scale down by 3.5x
        objScale.setTransform(scaleTrans);
        objRoot.addChild(objScale);

        // Create a TransformGroup and initialize it to the
        // identity. Enable the TRANSFORM_WRITE capability so that
        // the mouse behaviors code can modify it at runtime. Add it to the
        // root of the subgraph.
        TransformGroup objTrans = new TransformGroup();
        objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        objScale.addChild(objTrans);

        // Add the primitives to the scene
        objTrans.addChild(createConeTransformGroup()); // the cone
        rotAxis = new RotAxis(rotAxisLength); // the axis
        objTrans.addChild(rotAxis);
        coordSys = new CoordSys(coordSysLength); // the coordSys
        objTrans.addChild(coordSys);

        BoundingSphere bounds = new BoundingSphere(new Point3d(), 100.0);

        // The book used a white background for the figures
        //Background bg = new Background(new Color3f(1.0f, 1.0f, 1.0f));
        //bg.setApplicationBounds(bounds);
        //objTrans.addChild(bg);

        // Set up the ambient light
        Color3f ambientColor = new Color3f(0.1f, 0.1f, 0.1f);
        AmbientLight ambientLightNode = new AmbientLight(ambientColor);
        ambientLightNode.setInfluencingBounds(bounds);
        objRoot.addChild(ambientLightNode);

        // Set up the directional lights
        Color3f light1Color = new Color3f(1.0f, 1.0f, 1.0f);
        Vector3f light1Direction = new Vector3f(0.0f, -0.2f, -1.0f);

        DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction);
        light1.setInfluencingBounds(bounds);
        objRoot.addChild(light1);

        return objRoot;
    }

    public TransformExplorer() {
        this(false, 1.0f);
    }

    public TransformExplorer(boolean isApplication, float initOffScreenScale) {
        this.isApplication = isApplication;
        this.offScreenScale = initOffScreenScale;
    }

    public void init() {

        setLayout(new BorderLayout());
        GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();

        canvas = new Canvas3D(config);
        add("Center", canvas);

        u = new SimpleUniverse(canvas);

        if (isApplication) {
            offScreenCanvas = new OffScreenCanvas3D(config, true);
            // set the size of the off-screen canvas based on a scale
            // of the on-screen size
            Screen3D sOn = canvas.getScreen3D();
            Screen3D sOff = offScreenCanvas.getScreen3D();
            Dimension dim = sOn.getSize();
            dim.width *= offScreenScale;
            dim.height *= offScreenScale;
            sOff.setSize(dim);
            sOff.setPhysicalScreenWidth(sOn.getPhysicalScreenWidth() * offScreenScale);
            sOff.setPhysicalScreenHeight(sOn.getPhysicalScreenHeight() * offScreenScale);

            // attach the offscreen canvas to the view
            u.getViewer().getView().addCanvas3D(offScreenCanvas);
        }

        // Create a simple scene and attach it to the virtual universe
        BranchGroup scene = createSceneGraph();

        // get the view
        view = u.getViewer().getView();

        // This will move the ViewPlatform back a bit so the
        // objects in the scene can be viewed.
        ViewingPlatform viewingPlatform = u.getViewingPlatform();
        viewingPlatform.setNominalViewingTransform();

        // add an orbit behavior to move the viewing platform
        OrbitBehavior orbit = new OrbitBehavior(canvas, OrbitBehavior.STOP_ZOOM);
        BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 100.0);
        orbit.setSchedulingBounds(bounds);
        viewingPlatform.setViewPlatformBehavior(orbit);

        u.addBranchGraph(scene);

        add("East", guiPanel());
    }

    // create a panel with a tabbed pane holding each of the edit panels
    JPanel guiPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        JTabbedPane tabbedPane = new JTabbedPane();
        tabbedPane.addTab("Translation", translationPanel());
        tabbedPane.addTab("Scaling", scalePanel());
        tabbedPane.addTab("Rotation", rotationPanel());
        tabbedPane.addTab("Reference Point", refPtPanel());
        panel.add("Center", tabbedPane);
        panel.add("South", configPanel());
        return panel;
    }

    Box translationPanel() {
        Box panel = new Box(BoxLayout.Y_AXIS);

        panel.add(new LeftAlignComponent(new JLabel("Translation Offset")));

        // X translation label, slider, and value label
        FloatLabelJSlider coneTranslateXSlider = new FloatLabelJSlider("X", 0.1f, -2.0f, 2.0f, coneTranslation.x);
        coneTranslateXSlider.setMajorTickSpacing(1.0f);
        coneTranslateXSlider.setPaintTicks(true);
        coneTranslateXSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneTranslation.x = e.getValue();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeTranslation();
                }
            }
        });
        panel.add(coneTranslateXSlider);

        // Y translation label, slider, and value label
        FloatLabelJSlider coneTranslateYSlider = new FloatLabelJSlider("Y", 0.1f, -2.0f, 2.0f, coneTranslation.y);
        coneTranslateYSlider.setMajorTickSpacing(1.0f);
        coneTranslateYSlider.setPaintTicks(true);
        coneTranslateYSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneTranslation.y = e.getValue();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeTranslation();
                }
            }
        });
        panel.add(coneTranslateYSlider);

        // Z translation label, slider, and value label
        FloatLabelJSlider coneTranslateZSlider = new FloatLabelJSlider("Z", 0.1f, -2.0f, 2.0f, coneTranslation.z);
        coneTranslateZSlider.setMajorTickSpacing(1.0f);
        coneTranslateZSlider.setPaintTicks(true);
        coneTranslateZSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneTranslation.z = e.getValue();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeTranslation();
                }
            }
        });
        panel.add(coneTranslateZSlider);

        return panel;
    }

    Box scalePanel() {
        Box panel = new Box(BoxLayout.Y_AXIS);

        // Uniform Scale
        JLabel uniform = new JLabel("Uniform Scale");
        panel.add(new LeftAlignComponent(uniform));

        FloatLabelJSlider coneScaleSlider = new FloatLabelJSlider("S:", 0.1f, 0.0f, 3.0f, coneScale);
        coneScaleSlider.setMajorTickSpacing(1.0f);
        coneScaleSlider.setPaintTicks(true);
        coneScaleSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneScale = e.getValue();
                useUniformScale = true;
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeUScale();
                }
            }
        });
        panel.add(coneScaleSlider);

        JLabel nonUniform = new JLabel("Non-Uniform Scale");
        panel.add(new LeftAlignComponent(nonUniform));

        // Non-Uniform Scale
        FloatLabelJSlider coneNUScaleXSlider = new FloatLabelJSlider("X: ", 0.1f, 0.0f, 3.0f,
                (float) coneNUScale.x);
        coneNUScaleXSlider.setMajorTickSpacing(1.0f);
        coneNUScaleXSlider.setPaintTicks(true);
        coneNUScaleXSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneNUScale.x = (double) e.getValue();
                useUniformScale = false;
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeNUScale();
                }
            }
        });
        panel.add(coneNUScaleXSlider);

        FloatLabelJSlider coneNUScaleYSlider = new FloatLabelJSlider("Y: ", 0.1f, 0.0f, 3.0f,
                (float) coneNUScale.y);
        coneNUScaleYSlider.setMajorTickSpacing(1.0f);
        coneNUScaleYSlider.setPaintTicks(true);
        coneNUScaleYSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneNUScale.y = (double) e.getValue();
                useUniformScale = false;
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeNUScale();
                }
            }
        });
        panel.add(coneNUScaleYSlider);

        FloatLabelJSlider coneNUScaleZSlider = new FloatLabelJSlider("Z: ", 0.1f, 0.0f, 3.0f,
                (float) coneNUScale.z);
        coneNUScaleZSlider.setMajorTickSpacing(1.0f);
        coneNUScaleZSlider.setPaintTicks(true);
        coneNUScaleZSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneNUScale.z = (double) e.getValue();
                useUniformScale = false;
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeNUScale();
                }
            }
        });
        panel.add(coneNUScaleZSlider);

        return panel;
    }

    JPanel rotationPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(0, 1));

        panel.add(new LeftAlignComponent(new JLabel("Rotation Axis")));
        FloatLabelJSlider coneRotateAxisXSlider = new FloatLabelJSlider("X: ", 0.01f, -1.0f, 1.0f,
                (float) coneRotateAxis.x);
        coneRotateAxisXSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRotateAxis.x = e.getValue();
                normalizeConeRotateAxis();
                updateConeAxisAngle();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeRotation();
                }
                rotAxis.setRotationAxis(coneRotateAxis);
                updateConeRotateNormalizedLabels();
            }
        });
        panel.add(coneRotateAxisXSlider);

        FloatLabelJSlider coneRotateAxisYSlider = new FloatLabelJSlider("Y: ", 0.01f, -1.0f, 1.0f,
                (float) coneRotateAxis.y);
        coneRotateAxisYSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRotateAxis.y = e.getValue();
                normalizeConeRotateAxis();
                updateConeAxisAngle();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeRotation();
                }
                rotAxis.setRotationAxis(coneRotateAxis);
                updateConeRotateNormalizedLabels();
            }
        });
        panel.add(coneRotateAxisYSlider);

        FloatLabelJSlider coneRotateAxisZSlider = new FloatLabelJSlider("Z: ", 0.01f, -1.0f, 1.0f,
                (float) coneRotateAxis.y);
        coneRotateAxisZSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRotateAxis.z = e.getValue();
                normalizeConeRotateAxis();
                updateConeAxisAngle();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeRotation();
                }
                rotAxis.setRotationAxis(coneRotateAxis);
                updateConeRotateNormalizedLabels();
            }
        });
        panel.add(coneRotateAxisZSlider);

        JLabel normalizedLabel = new JLabel("Normalized Rotation Axis");
        panel.add(new LeftAlignComponent(normalizedLabel));
        ;
        coneRotateNAxisXLabel = new JLabel("X: 1.000");
        panel.add(new LeftAlignComponent(coneRotateNAxisXLabel));
        coneRotateNAxisYLabel = new JLabel("Y: 0.000");
        panel.add(new LeftAlignComponent(coneRotateNAxisYLabel));
        coneRotateNAxisZLabel = new JLabel("Z: 0.000");
        panel.add(new LeftAlignComponent(coneRotateNAxisZLabel));
        normalizeConeRotateAxis();
        updateConeRotateNormalizedLabels();

        FloatLabelJSlider coneRotateAxisAngleSlider = new FloatLabelJSlider("Angle: ", 1.0f, -180.0f, 180.0f,
                (float) coneRotateAngle);
        coneRotateAxisAngleSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRotateAngle = e.getValue();
                updateConeAxisAngle();
                if (useCompoundTransform) {
                    updateUsingCompoundTransform();
                } else {
                    setConeRotation();
                }
            }
        });
        panel.add(coneRotateAxisAngleSlider);

        return panel;
    }

    Box refPtPanel() {
        Box panel = new Box(BoxLayout.Y_AXIS);

        panel.add(new LeftAlignComponent(new JLabel("Reference Point Coordinates")));

        // X Ref Pt
        FloatLabelJSlider coneRefPtXSlider = new FloatLabelJSlider("X", 0.1f, -2.0f, 2.0f, coneRefPt.x);
        coneRefPtXSlider.setMajorTickSpacing(1.0f);
        coneRefPtXSlider.setPaintTicks(true);
        coneRefPtXSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRefPt.x = e.getValue();
                useCompoundTransform = true;
                updateUsingCompoundTransform();
                rotAxis.setRefPt(coneRefPt);
            }
        });
        panel.add(coneRefPtXSlider);

        // Y Ref Pt
        FloatLabelJSlider coneRefPtYSlider = new FloatLabelJSlider("Y", 0.1f, -2.0f, 2.0f, coneRefPt.y);
        coneRefPtYSlider.setMajorTickSpacing(1.0f);
        coneRefPtYSlider.setPaintTicks(true);
        coneRefPtYSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRefPt.y = e.getValue();
                useCompoundTransform = true;
                updateUsingCompoundTransform();
                rotAxis.setRefPt(coneRefPt);
            }
        });
        panel.add(coneRefPtYSlider);

        // Z Ref Pt
        FloatLabelJSlider coneRefPtZSlider = new FloatLabelJSlider("Z", 0.1f, -2.0f, 2.0f, coneRefPt.z);
        coneRefPtZSlider.setMajorTickSpacing(1.0f);
        coneRefPtZSlider.setPaintTicks(true);
        coneRefPtZSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                coneRefPt.z = e.getValue();
                useCompoundTransform = true;
                updateUsingCompoundTransform();
                rotAxis.setRefPt(coneRefPt);
            }
        });
        panel.add(coneRefPtZSlider);

        return panel;
    }

    JPanel configPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(0, 1));
        panel.add(new JLabel("Display annotation:"));

        // create the check boxes
        rotAxisCheckBox = new JCheckBox(rotAxisString);
        rotAxisCheckBox.setSelected(showRotAxis);
        rotAxisCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Object source = e.getSource();
                showRotAxis = ((JCheckBox) source).isSelected();
                if (showRotAxis) {
                    rotAxis.setWhichChild(Switch.CHILD_ALL);
                } else {
                    rotAxis.setWhichChild(Switch.CHILD_NONE);
                }
            }
        });
        panel.add(rotAxisCheckBox);

        coordSysCheckBox = new JCheckBox(coordSysString);
        coordSysCheckBox.setSelected(showCoordSys);
        coordSysCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Object source = e.getSource();
                showCoordSys = ((JCheckBox) source).isSelected();
                if (showCoordSys) {
                    coordSys.setWhichChild(Switch.CHILD_ALL);
                } else {
                    coordSys.setWhichChild(Switch.CHILD_NONE);
                }
            }
        });
        panel.add(coordSysCheckBox);

        if (isApplication) {
            JButton snapButton = new JButton(snapImageString);
            snapButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    Point loc = canvas.getLocationOnScreen();
                    offScreenCanvas.setOffScreenLocation(loc);
                    Dimension dim = canvas.getSize();
                    dim.width *= offScreenScale;
                    dim.height *= offScreenScale;
                    nf.setMinimumIntegerDigits(3);
                    nf.setMaximumFractionDigits(0);
                    offScreenCanvas.snapImageFile(outFileBase + nf.format(outFileSeq++), dim.width, dim.height);
                    nf.setMinimumIntegerDigits(0);
                }
            });
            panel.add(snapButton);
        }

        return panel;
    }

    public void destroy() {
        u.removeAllLocales();
    }

    // The following allows TransformExplorer to be run as an application
    // as well as an applet
    //
    public static void main(String[] args) {
        float initOffScreenScale = 2.5f;
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-s")) {
                if (args.length >= (i + 1)) {
                    initOffScreenScale = Float.parseFloat(args[i + 1]);
                    i++;
                }
            }
        }
        new MainFrame(new TransformExplorer(true, initOffScreenScale), 950, 600);
    }
}

interface Java3DExplorerConstants {

    // colors
    static Color3f black = new Color3f(0.0f, 0.0f, 0.0f);

    static Color3f red = new Color3f(1.0f, 0.0f, 0.0f);

    static Color3f green = new Color3f(0.0f, 1.0f, 0.0f);

    static Color3f blue = new Color3f(0.0f, 0.0f, 1.0f);

    static Color3f skyBlue = new Color3f(0.6f, 0.7f, 0.9f);

    static Color3f cyan = new Color3f(0.0f, 1.0f, 1.0f);

    static Color3f magenta = new Color3f(1.0f, 0.0f, 1.0f);

    static Color3f yellow = new Color3f(1.0f, 1.0f, 0.0f);

    static Color3f brightWhite = new Color3f(1.0f, 1.5f, 1.5f);

    static Color3f white = new Color3f(1.0f, 1.0f, 1.0f);

    static Color3f darkGrey = new Color3f(0.15f, 0.15f, 0.15f);

    static Color3f medGrey = new Color3f(0.3f, 0.3f, 0.3f);

    static Color3f grey = new Color3f(0.5f, 0.5f, 0.5f);

    static Color3f lightGrey = new Color3f(0.75f, 0.75f, 0.75f);

    // infinite bounding region, used to make env nodes active everywhere
    BoundingSphere infiniteBounds = new BoundingSphere(new Point3d(), Double.MAX_VALUE);

    // common values
    static final String nicestString = "NICEST";

    static final String fastestString = "FASTEST";

    static final String antiAliasString = "Anti-Aliasing";

    static final String noneString = "NONE";

    // light type constants
    static int LIGHT_AMBIENT = 1;

    static int LIGHT_DIRECTIONAL = 2;

    static int LIGHT_POSITIONAL = 3;

    static int LIGHT_SPOT = 4;

    // screen capture constants
    static final int USE_COLOR = 1;

    static final int USE_BLACK_AND_WHITE = 2;

    // number formatter
    NumberFormat nf = NumberFormat.getInstance();

}

class OffScreenCanvas3D extends Canvas3D {

    OffScreenCanvas3D(GraphicsConfiguration graphicsConfiguration, boolean offScreen) {

        super(graphicsConfiguration, offScreen);
    }

    private BufferedImage doRender(int width, int height) {

        BufferedImage bImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        ImageComponent2D buffer = new ImageComponent2D(ImageComponent.FORMAT_RGB, bImage);
        //buffer.setYUp(true);

        setOffScreenBuffer(buffer);
        renderOffScreenBuffer();
        waitForOffScreenRendering();
        bImage = getOffScreenBuffer().getImage();
        return bImage;
    }

    void snapImageFile(String filename, int width, int height) {
        BufferedImage bImage = doRender(width, height);

        /*
         * JAI: RenderedImage fImage = JAI.create("format", bImage,
         * DataBuffer.TYPE_BYTE); JAI.create("filestore", fImage, filename +
         * ".tif", "tiff", null);
         */

        /* No JAI: */
        try {
            FileOutputStream fos = new FileOutputStream(filename + ".jpg");
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            JPEGImageEncoder jie = JPEGCodec.createJPEGEncoder(bos);
            JPEGEncodeParam param = jie.getDefaultJPEGEncodeParam(bImage);
            param.setQuality(1.0f, true);
            jie.setJPEGEncodeParam(param);
            jie.encode(bImage);

            bos.flush();
            fos.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

class FloatLabelJSlider extends JPanel implements ChangeListener, Java3DExplorerConstants {

    JSlider slider;

    JLabel valueLabel;

    Vector listeners = new Vector();

    float min, max, resolution, current, scale;

    int minInt, maxInt, curInt;;

    int intDigits, fractDigits;

    float minResolution = 0.001f;

    // default slider with name, resolution = 0.1, min = 0.0, max = 1.0 inital
    // 0.5
    FloatLabelJSlider(String name) {
        this(name, 0.1f, 0.0f, 1.0f, 0.5f);
    }

    FloatLabelJSlider(String name, float resolution, float min, float max, float current) {

        this.resolution = resolution;
        this.min = min;
        this.max = max;
        this.current = current;

        if (resolution < minResolution) {
            resolution = minResolution;
        }

        // round scale to nearest integer fraction. i.e. 0.3 => 1/3 = 0.33
        scale = (float) Math.round(1.0f / resolution);
        resolution = 1.0f / scale;

        // get the integer versions of max, min, current
        minInt = Math.round(min * scale);
        maxInt = Math.round(max * scale);
        curInt = Math.round(current * scale);

        // sliders use integers, so scale our floating point value by "scale"
        // to make each slider "notch" be "resolution". We will scale the
        // value down by "scale" when we get the event.
        slider = new JSlider(JSlider.HORIZONTAL, minInt, maxInt, curInt);
        slider.addChangeListener(this);

        valueLabel = new JLabel(" ");

        // set the initial value label
        setLabelString();

        // add min and max labels to the slider
        Hashtable labelTable = new Hashtable();
        labelTable.put(new Integer(minInt), new JLabel(nf.format(min)));
        labelTable.put(new Integer(maxInt), new JLabel(nf.format(max)));
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        /* layout to align left */
        setLayout(new BorderLayout());
        Box box = new Box(BoxLayout.X_AXIS);
        add(box, BorderLayout.WEST);

        box.add(new JLabel(name));
        box.add(slider);
        box.add(valueLabel);
    }

    public void setMinorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMinorTickSpacing(intSpacing);
    }

    public void setMajorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMajorTickSpacing(intSpacing);
    }

    public void setPaintTicks(boolean paint) {
        slider.setPaintTicks(paint);
    }

    public void addFloatListener(FloatListener listener) {
        listeners.add(listener);
    }

    public void removeFloatListener(FloatListener listener) {
        listeners.remove(listener);
    }

    public void stateChanged(ChangeEvent e) {
        JSlider source = (JSlider) e.getSource();
        // get the event type, set the corresponding value.
        // Sliders use integers, handle floating point values by scaling the
        // values by "scale" to allow settings at "resolution" intervals.
        // Divide by "scale" to get back to the real value.
        curInt = source.getValue();
        current = curInt / scale;

        valueChanged();
    }

    public void setValue(float newValue) {
        boolean changed = (newValue != current);
        current = newValue;
        if (changed) {
            valueChanged();
        }
    }

    private void valueChanged() {
        // update the label
        setLabelString();

        // notify the listeners
        FloatEvent event = new FloatEvent(this, current);
        for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
            FloatListener listener = (FloatListener) e.nextElement();
            listener.floatChanged(event);
        }
    }

    void setLabelString() {
        // Need to muck around to try to make sure that the width of the label
        // is wide enough for the largest value. Pad the string
        // be large enough to hold the largest value.
        int pad = 5; // fudge to make up for variable width fonts
        float maxVal = Math.max(Math.abs(min), Math.abs(max));
        intDigits = Math.round((float) (Math.log(maxVal) / Math.log(10))) + pad;
        if (min < 0) {
            intDigits++; // add one for the '-'
        }
        // fractDigits is num digits of resolution for fraction. Use base 10 log
        // of scale, rounded up, + 2.
        fractDigits = (int) Math.ceil((Math.log(scale) / Math.log(10)));
        nf.setMinimumFractionDigits(fractDigits);
        nf.setMaximumFractionDigits(fractDigits);
        String value = nf.format(current);
        while (value.length() < (intDigits + fractDigits)) {
            value = value + "  ";
        }
        valueLabel.setText(value);
    }

}

class FloatEvent extends EventObject {

    float value;

    FloatEvent(Object source, float newValue) {
        super(source);
        value = newValue;
    }

    float getValue() {
        return value;
    }
}

interface FloatListener extends EventListener {
    void floatChanged(FloatEvent e);
}

class LogFloatLabelJSlider extends JPanel implements ChangeListener, Java3DExplorerConstants {

    JSlider slider;

    JLabel valueLabel;

    Vector listeners = new Vector();

    float min, max, resolution, current, scale;

    double minLog, maxLog, curLog;

    int minInt, maxInt, curInt;;

    int intDigits, fractDigits;

    NumberFormat nf = NumberFormat.getInstance();

    float minResolution = 0.001f;

    double logBase = Math.log(10);

    // default slider with name, resolution = 0.1, min = 0.0, max = 1.0 inital
    // 0.5
    LogFloatLabelJSlider(String name) {
        this(name, 0.1f, 100.0f, 10.0f);
    }

    LogFloatLabelJSlider(String name, float min, float max, float current) {

        this.resolution = resolution;
        this.min = min;
        this.max = max;
        this.current = current;

        if (resolution < minResolution) {
            resolution = minResolution;
        }

        minLog = log10(min);
        maxLog = log10(max);
        curLog = log10(current);

        // resolution is 100 steps from min to max
        scale = 100.0f;
        resolution = 1.0f / scale;

        // get the integer versions of max, min, current
        minInt = (int) Math.round(minLog * scale);
        maxInt = (int) Math.round(maxLog * scale);
        curInt = (int) Math.round(curLog * scale);

        slider = new JSlider(JSlider.HORIZONTAL, minInt, maxInt, curInt);
        slider.addChangeListener(this);

        valueLabel = new JLabel(" ");

        // Need to muck around to make sure that the width of the label
        // is wide enough for the largest value. Pad the initial string
        // be large enough to hold the largest value.
        int pad = 5; // fudge to make up for variable width fonts
        intDigits = (int) Math.ceil(maxLog) + pad;
        if (min < 0) {
            intDigits++; // add one for the '-'
        }
        if (minLog < 0) {
            fractDigits = (int) Math.ceil(-minLog);
        } else {
            fractDigits = 0;
        }
        nf.setMinimumFractionDigits(fractDigits);
        nf.setMaximumFractionDigits(fractDigits);
        String value = nf.format(current);
        while (value.length() < (intDigits + fractDigits)) {
            value = value + " ";
        }
        valueLabel.setText(value);

        // add min and max labels to the slider
        Hashtable labelTable = new Hashtable();
        labelTable.put(new Integer(minInt), new JLabel(nf.format(min)));
        labelTable.put(new Integer(maxInt), new JLabel(nf.format(max)));
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        // layout to align left
        setLayout(new BorderLayout());
        Box box = new Box(BoxLayout.X_AXIS);
        add(box, BorderLayout.WEST);

        box.add(new JLabel(name));
        box.add(slider);
        box.add(valueLabel);
    }

    public void setMinorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMinorTickSpacing(intSpacing);
    }

    public void setMajorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMajorTickSpacing(intSpacing);
    }

    public void setPaintTicks(boolean paint) {
        slider.setPaintTicks(paint);
    }

    public void addFloatListener(FloatListener listener) {
        listeners.add(listener);
    }

    public void removeFloatListener(FloatListener listener) {
        listeners.remove(listener);
    }

    public void stateChanged(ChangeEvent e) {
        JSlider source = (JSlider) e.getSource();
        curInt = source.getValue();
        curLog = curInt / scale;
        current = (float) exp10(curLog);

        valueChanged();
    }

    public void setValue(float newValue) {
        boolean changed = (newValue != current);
        current = newValue;
        if (changed) {
            valueChanged();
        }
    }

    private void valueChanged() {
        String value = nf.format(current);
        valueLabel.setText(value);

        // notify the listeners
        FloatEvent event = new FloatEvent(this, current);
        for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
            FloatListener listener = (FloatListener) e.nextElement();
            listener.floatChanged(event);
        }
    }

    double log10(double value) {
        return Math.log(value) / logBase;
    }

    double exp10(double value) {
        return Math.exp(value * logBase);
    }

}

class CoordSys extends Switch {

    // Temporaries that are reused
    Transform3D tmpTrans = new Transform3D();

    Vector3f tmpVector = new Vector3f();

    AxisAngle4f tmpAxisAngle = new AxisAngle4f();

    // colors for use in the shapes
    Color3f black = new Color3f(0.0f, 0.0f, 0.0f);

    Color3f grey = new Color3f(0.3f, 0.3f, 0.3f);

    Color3f white = new Color3f(1.0f, 1.0f, 1.0f);

    // geometric constants
    Point3f origin = new Point3f();

    Vector3f yAxis = new Vector3f(0.0f, 1.0f, 0.0f);

    CoordSys(float axisLength) {
        super(Switch.CHILD_ALL);

        float coordSysLength = axisLength;
        float labelOffset = axisLength / 20.0f;
        float axisRadius = axisLength / 500.0f;
        float arrowRadius = axisLength / 125.0f;
        float arrowHeight = axisLength / 50.0f;
        float tickRadius = axisLength / 125.0f;
        float tickHeight = axisLength / 250.0f;

        // Set the Switch to allow changes
        setCapability(Switch.ALLOW_SWITCH_READ);
        setCapability(Switch.ALLOW_SWITCH_WRITE);

        // Set up an appearance to make the Axis have
        // grey ambient, black emmissive, grey diffuse and grey specular
        // coloring.
        //Material material = new Material(grey, black, grey, white, 64);
        Material material = new Material(white, black, white, white, 64);
        Appearance appearance = new Appearance();
        appearance.setMaterial(material);

        // Create a shared group to hold one axis of the coord sys
        SharedGroup coordAxisSG = new SharedGroup();

        // create a cylinder for the central line of the axis
        Cylinder cylinder = new Cylinder(axisRadius, coordSysLength, appearance);
        // cylinder goes from -coordSysLength/2 to coordSysLength in y
        coordAxisSG.addChild(cylinder);

        // create the shared arrowhead
        Cone arrowHead = new Cone(arrowRadius, arrowHeight, appearance);
        SharedGroup arrowHeadSG = new SharedGroup();
        arrowHeadSG.addChild(arrowHead);

        // Create a TransformGroup to move the arrowhead to the top of the
        // axis
        // The arrowhead goes from -arrowHeight/2 to arrowHeight/2 in y.
        // Put it at the top of the axis, coordSysLength / 2
        tmpVector.set(0.0f, coordSysLength / 2 + arrowHeight / 2, 0.0f);
        tmpTrans.set(tmpVector);
        TransformGroup topTG = new TransformGroup();
        topTG.setTransform(tmpTrans);
        topTG.addChild(new Link(arrowHeadSG));
        coordAxisSG.addChild(topTG);

        // create the minus arrowhead
        // Create a TransformGroup to turn the cone upside down:
        // Rotate 180 degrees around Z axis
        tmpAxisAngle.set(0.0f, 0.0f, 1.0f, (float) Math.toRadians(180));
        tmpTrans.set(tmpAxisAngle);

        // Put the arrowhead at the bottom of the axis
        tmpVector.set(0.0f, -coordSysLength / 2 - arrowHeight / 2, 0.0f);
        tmpTrans.setTranslation(tmpVector);
        TransformGroup bottomTG = new TransformGroup();
        bottomTG.setTransform(tmpTrans);
        bottomTG.addChild(new Link(arrowHeadSG));
        coordAxisSG.addChild(bottomTG);

        // Now add "ticks" at 1, 2, 3, etc.

        // create a shared group for the tick
        Cylinder tick = new Cylinder(tickRadius, tickHeight, appearance);
        SharedGroup tickSG = new SharedGroup();
        tickSG.addChild(tick);

        // transform each instance and add it to the coord axis group
        int maxTick = (int) (coordSysLength / 2);
        int minTick = -maxTick;
        for (int i = minTick; i <= maxTick; i++) {
            if (i == 0)
                continue; // no tick at 0

            // use a TransformGroup to offset to the tick location
            TransformGroup tickTG = new TransformGroup();
            tmpVector.set(0.0f, (float) i, 0.0f);
            tmpTrans.set(tmpVector);
            tickTG.setTransform(tmpTrans);
            // then link to an instance of the Tick shared group
            tickTG.addChild(new Link(tickSG));
            // add the TransformGroup to the coord axis
            coordAxisSG.addChild(tickTG);
        }

        // add a Link to the axis SharedGroup to the coordSys
        addChild(new Link(coordAxisSG)); // Y axis

        // Create TransformGroups for the X and Z axes
        TransformGroup xAxisTG = new TransformGroup();
        // rotate 90 degrees around Z axis
        tmpAxisAngle.set(0.0f, 0.0f, 1.0f, (float) Math.toRadians(90));
        tmpTrans.set(tmpAxisAngle);
        xAxisTG.setTransform(tmpTrans);
        xAxisTG.addChild(new Link(coordAxisSG));
        addChild(xAxisTG); // X axis

        TransformGroup zAxisTG = new TransformGroup();
        // rotate 90 degrees around X axis
        tmpAxisAngle.set(1.0f, 0.0f, 0.0f, (float) Math.toRadians(90));
        tmpTrans.set(tmpAxisAngle);
        zAxisTG.setTransform(tmpTrans);
        zAxisTG.addChild(new Link(coordAxisSG));
        addChild(zAxisTG); // Z axis

        // Add the labels. First we need a Font3D for the Text3Ds
        // select the default font, plain style, 0.5 tall. Use null for
        // the extrusion so we get "flat" text since we will be putting it
        // into an oriented Shape3D
        Font3D f3d = new Font3D(new Font("Default", Font.PLAIN, 1), null);

        // set up the +X label
        Text3D plusXText = new Text3D(f3d, "+X", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D plusXTextShape = new OrientedShape3D(plusXText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup plusXTG = new TransformGroup();
        tmpVector.set(coordSysLength / 2 + labelOffset, 0.0f, 0.0f);
        tmpTrans.set(0.15f, tmpVector);
        plusXTG.setTransform(tmpTrans);
        plusXTG.addChild(plusXTextShape);
        addChild(plusXTG);

        // set up the -X label
        Text3D minusXText = new Text3D(f3d, "-X", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D minusXTextShape = new OrientedShape3D(minusXText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup minusXTG = new TransformGroup();
        tmpVector.set(-coordSysLength / 2 - labelOffset, 0.0f, 0.0f);
        tmpTrans.set(0.15f, tmpVector);
        minusXTG.setTransform(tmpTrans);
        minusXTG.addChild(minusXTextShape);
        addChild(minusXTG);

        // set up the +Y label
        Text3D plusYText = new Text3D(f3d, "+Y", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D plusYTextShape = new OrientedShape3D(plusYText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup plusYTG = new TransformGroup();
        tmpVector.set(0.0f, coordSysLength / 2 + labelOffset, 0.0f);
        tmpTrans.set(0.15f, tmpVector);
        plusYTG.setTransform(tmpTrans);
        plusYTG.addChild(plusYTextShape);
        addChild(plusYTG);

        // set up the -Y label
        Text3D minusYText = new Text3D(f3d, "-Y", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D minusYTextShape = new OrientedShape3D(minusYText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup minusYTG = new TransformGroup();
        tmpVector.set(0.0f, -coordSysLength / 2 - labelOffset, 0.0f);
        tmpTrans.set(0.15f, tmpVector);
        minusYTG.setTransform(tmpTrans);
        minusYTG.addChild(minusYTextShape);
        addChild(minusYTG);

        // set up the +Z label
        Text3D plusZText = new Text3D(f3d, "+Z", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D plusZTextShape = new OrientedShape3D(plusZText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup plusZTG = new TransformGroup();
        tmpVector.set(0.0f, 0.0f, coordSysLength / 2 + labelOffset);
        tmpTrans.set(0.15f, tmpVector);
        plusZTG.setTransform(tmpTrans);
        plusZTG.addChild(plusZTextShape);
        addChild(plusZTG);

        // set up the -Z label
        Text3D minusZText = new Text3D(f3d, "-Z", origin, Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT);
        // orient around the local origin
        OrientedShape3D minusZTextShape = new OrientedShape3D(minusZText, appearance,
                OrientedShape3D.ROTATE_ABOUT_POINT, origin);
        // transform to scale down to 0.15 in height, locate at end of axis
        TransformGroup minusZTG = new TransformGroup();
        tmpVector.set(0.0f, 0.0f, -coordSysLength / 2 - labelOffset);
        tmpTrans.set(0.15f, tmpVector);
        minusZTG.setTransform(tmpTrans);
        minusZTG.addChild(minusZTextShape);
        addChild(minusZTG);
    }
}

class LeftAlignComponent extends JPanel {
    LeftAlignComponent(Component c) {
        setLayout(new BorderLayout());
        add(c, BorderLayout.WEST);
    }
}

class RotAxis extends Switch implements Java3DExplorerConstants {

    // axis to align with 
    Vector3f rotAxis = new Vector3f(1.0f, 0.0f, 0.0f);
    // offset to ref point 
    Vector3f refPt = new Vector3f(0.0f, 0.0f, 0.0f);

    TransformGroup axisTG; // the transform group used to align the axis

    // Temporaries that are reused
    Transform3D tmpTrans = new Transform3D();
    Vector3f tmpVector = new Vector3f();
    AxisAngle4f tmpAxisAngle = new AxisAngle4f();

    // geometric constants
    Point3f origin = new Point3f();
    Vector3f yAxis = new Vector3f(0.0f, 1.0f, 0.0f);

    RotAxis(float axisLength) {
        super(Switch.CHILD_NONE);
        setCapability(Switch.ALLOW_SWITCH_READ);
        setCapability(Switch.ALLOW_SWITCH_WRITE);

        // set up the proportions for the arrow
        float axisRadius = axisLength / 120.0f;
        float arrowRadius = axisLength / 50.0f;
        float arrowHeight = axisLength / 30.0f;

        // create the TransformGroup which will be used to orient the axis
        axisTG = new TransformGroup();
        axisTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        axisTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        addChild(axisTG);

        // Set up an appearance to make the Axis have 
        // blue ambient, black emmissive, blue diffuse and white specular 
        // coloring.  
        Material material = new Material(blue, black, blue, white, 64);
        Appearance appearance = new Appearance();
        appearance.setMaterial(material);

        // create a cylinder for the central line of the axis
        Cylinder cylinder = new Cylinder(axisRadius, axisLength, appearance);
        // cylinder goes from -length/2 to length/2 in y
        axisTG.addChild(cylinder);

        // create a SharedGroup for the arrowHead
        Cone arrowHead = new Cone(arrowRadius, arrowHeight, appearance);
        SharedGroup arrowHeadSG = new SharedGroup();
        arrowHeadSG.addChild(arrowHead);

        // Create a TransformGroup to move the cone to the top of the 
        // cylinder
        tmpVector.set(0.0f, axisLength / 2 + arrowHeight / 2, 0.0f);
        tmpTrans.set(tmpVector);
        TransformGroup topTG = new TransformGroup();
        topTG.setTransform(tmpTrans);
        topTG.addChild(new Link(arrowHeadSG));
        axisTG.addChild(topTG);

        // create the bottom of the arrow
        // Create a TransformGroup to move the cone to the bottom of the 
        // axis so that its pushes into the bottom of the cylinder
        tmpVector.set(0.0f, -(axisLength / 2), 0.0f);
        tmpTrans.set(tmpVector);
        TransformGroup bottomTG = new TransformGroup();
        bottomTG.setTransform(tmpTrans);
        bottomTG.addChild(new Link(arrowHeadSG));
        axisTG.addChild(bottomTG);

        updateAxisTransform();
    }

    public void setRotationAxis(Vector3f setRotAxis) {
        rotAxis.set(setRotAxis);
        float magSquared = rotAxis.lengthSquared();
        if (magSquared > 0.0001) {
            rotAxis.scale((float) (1.0 / Math.sqrt(magSquared)));
        } else {
            rotAxis.set(1.0f, 0.0f, 0.0f);
        }
        updateAxisTransform();
    }

    public void setRefPt(Vector3f setRefPt) {
        refPt.set(setRefPt);
        updateAxisTransform();
    }

    // set the transform on the axis so that it aligns with the rotation
    // axis and goes through the reference point
    private void updateAxisTransform() {
        // We need to rotate the axis, which is defined along the y-axis,
        // to the direction indicated by the rotAxis.
        // We can do this using a neat trick.  To transform a vector to align
        // with another vector (assuming both vectors have unit length), take 
        // the cross product the the vectors.  The direction of the cross
        // product is the axis, and the length of the cross product is the
        // the sine of the angle, so the inverse sine of the length gives 
        // us the angle
        tmpVector.cross(yAxis, rotAxis);
        float angle = (float) Math.asin(tmpVector.length());

        tmpAxisAngle.set(tmpVector, angle);
        tmpTrans.set(tmpAxisAngle);
        tmpTrans.setTranslation(refPt);
        axisTG.setTransform(tmpTrans);
    }
}