changeset 5158:ad10ea1df81d 8.0-b109

Automated merge with ssh://jfxsrc.us.oracle.com//javafx/8.0/MASTER/jfx/rt
author jgodinez
date Tue, 24 Sep 2013 10:09:57 -0700
parents 5de952a29ea8 ee82e3236895
children a9d5fe2c0bb9 8a99b06d9c22 69bd8e6330ac
files
diffstat 316 files changed, 28661 insertions(+), 2664 deletions(-) [+]
line wrap: on
line diff
--- a/apps/experiments/3DViewer/nbproject/project.properties	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/nbproject/project.properties	Tue Sep 24 10:09:57 2013 -0700
@@ -59,7 +59,7 @@
 manifest.file=manifest.mf
 meta.inf.dir=${src.dir}/META-INF
 mkdist.disabled=false
-platform.active=JDK_1.8
+platform.active=JDK_1.8_Without_FX
 run.classpath=\
     ${javac.classpath}:\
     ${build.classes.dir}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalApp.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalApp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -83,7 +83,8 @@
         this.stage = stage;
 
         // load test image
-        testImage = new Image(Height2NormalApp.class.getResource("javafx-heightmap.jpg").toExternalForm());
+        // testImage = new Image(Height2NormalApp.class.getResource("javafx-heightmap.jpg").toExternalForm());
+        testImage = new Image(Height2NormalApp.class.getResource("/com/javafx/experiments/jfx3dviewer/blue.jpg").toExternalForm());
         heightImage.set(testImage);
 
         // create toolbar
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/Loader.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/Loader.java	Tue Sep 24 10:09:57 2013 -0700
@@ -1,1842 +1,1849 @@
-package com.javafx.experiments.importers.maya;
-
-import com.javafx.experiments.importers.SmoothingGroups;
-import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import javafx.animation.Interpolator;
-import javafx.animation.KeyFrame;
-import javafx.animation.KeyValue;
-import javafx.beans.property.DoubleProperty;
-import javafx.scene.DepthTest;
-import javafx.scene.Group;
-import javafx.scene.Node;
-import javafx.scene.image.Image;
-import javafx.scene.paint.Color;
-import javafx.scene.paint.PhongMaterial;
-import javafx.scene.shape.CullFace;
-import javafx.scene.shape.Mesh;
-import javafx.scene.shape.MeshView;
-import javafx.scene.shape.TriangleMesh;
-import javafx.scene.transform.Affine;
-import javafx.util.Duration;
-import com.javafx.experiments.importers.maya.parser.MParser;
-import com.javafx.experiments.importers.maya.values.MArray;
-import com.javafx.experiments.importers.maya.values.MBool;
-import com.javafx.experiments.importers.maya.values.MCompound;
-import com.javafx.experiments.importers.maya.values.MData;
-import com.javafx.experiments.importers.maya.values.MFloat;
-import com.javafx.experiments.importers.maya.values.MFloat2Array;
-import com.javafx.experiments.importers.maya.values.MFloat3;
-import com.javafx.experiments.importers.maya.values.MFloat3Array;
-import com.javafx.experiments.importers.maya.values.MFloatArray;
-import com.javafx.experiments.importers.maya.values.MInt;
-import com.javafx.experiments.importers.maya.values.MInt3Array;
-import com.javafx.experiments.importers.maya.values.MIntArray;
-import com.javafx.experiments.importers.maya.values.MPolyFace;
-import com.javafx.experiments.importers.maya.values.MString;
-import com.javafx.experiments.shape3d.PolygonMesh;
-import com.javafx.experiments.shape3d.PolygonMeshView;
-import com.javafx.experiments.shape3d.SkinningMesh;
-import com.sun.javafx.geom.Vec3f;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.Arrays;
-import javafx.animation.AnimationTimer;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.scene.Parent;
-import javafx.scene.Scene;
-
-/** Loader */
-class Loader {
-    public static final boolean DEBUG = false;
-    public static final boolean WARN = false;
-
-    MEnv env;
-
-    int startFrame;
-    int endFrame;
-
-    MNodeType transformType;
-    MNodeType jointType;
-    MNodeType meshType;
-    MNodeType cameraType;
-    MNodeType animCurve;
-    MNodeType animCurveTA;
-    MNodeType animCurveUA;
-    MNodeType animCurveUL;
-    MNodeType animCurveUT;
-    MNodeType animCurveUU;
-
-    MNodeType lambertType;
-    MNodeType reflectType;
-    MNodeType blinnType;
-    MNodeType phongType;
-    MNodeType fileType;
-    MNodeType skinClusterType;
-    MNodeType blendShapeType;
-    MNodeType groupPartsType;
-    MNodeType shadingEngineType;
-
-    // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
-    // Joint rootJoint; //NO_JOINTS
-    Map<MNode, Node> loaded = new HashMap<MNode, Node>();
-
-    Map<Float, List<KeyValue>> keyFrameMap = new TreeMap();
-
-    Map<Node, MNode> meshParents = new HashMap();
-
-    private MFloat3Array mVerts;
-    // Optionally force per-face per-vertex normal generation
-    private int[] edgeData;
-
-    private List<MData> uvSet;
-    private int uvChannel;
-    private MFloat3Array mPointTweaks;
-    private URL url;
-    private boolean asPolygonMesh;
-
-    //=========================================================================
-    // Loader.load
-    //-------------------------------------------------------------------------
-    // Called from MayaImporter.load
-    //=========================================================================
-    public void load(URL url, boolean asPolygonMesh) {
-        this.url = url;
-        this.asPolygonMesh = asPolygonMesh;
-        env = new MEnv();
-        MParser parser = new MParser(env);
-        try {
-            parser.parse(url);
-            loadModel();
-            for (MNode n : env.getNodes()) {
-                // System.out.println("____________________________________________________________");
-                // System.out.println("==> .......Node: " + n);
-                resolveNode(n);
-            }
-        } catch (Exception e) {
-            if (WARN) System.err.println("Error loading url: [" + url + "]");
-            throw new RuntimeException(e);
-        }
-    }
-
-    //=========================================================================
-    // Loader.loadModel
-    //=========================================================================
-    void loadModel() {
-        startFrame = (int) Math.round(env.getPlaybackStart() - 1);
-        endFrame = (int) Math.round(env.getPlaybackEnd() - 1);
-        transformType = env.findNodeType("transform");
-        jointType = env.findNodeType("joint");
-        meshType = env.findNodeType("mesh");
-        cameraType = env.findNodeType("camera");
-        animCurve = env.findNodeType("animCurve");
-        animCurveTA = env.findNodeType("animCurveTA");
-        animCurveUA = env.findNodeType("animCurveUA");
-        animCurveUL = env.findNodeType("animCurveUL");
-        animCurveUT = env.findNodeType("animCurveUT");
-        animCurveUU = env.findNodeType("animCurveUU");
-
-        lambertType = env.findNodeType("lambert");
-        reflectType = env.findNodeType("reflect");
-        blinnType = env.findNodeType("blinn");
-        phongType = env.findNodeType("phong");
-        fileType = env.findNodeType("file");
-        skinClusterType = env.findNodeType("skinCluster");
-        groupPartsType = env.findNodeType("groupParts");
-        shadingEngineType = env.findNodeType("shadingEngine");
-        blendShapeType = env.findNodeType("blendShape");
-    }
-
-    //=========================================================================
-    // Loader.resolveNode
-    //-------------------------------------------------------------------------
-    // Loader.resolveNode looks up MNode in the HashMap Map<MNode, Node> loaded
-    // and returns the Node to which this map maps the MNode.
-    // Also, if the node that its looking up hasn't been processed yet,
-    // it processes the node.
-    //=========================================================================
-    Node resolveNode(MNode n) {
-        // System.out.println("--> resolveNode: " + n);
-        // if the node hasn't already been processed, then process the node
-        if (!loaded.containsKey(n)) {
-            // System.out.println("--> containsKey: " + n);
-            processNode(n);
-            // System.out.println("    loaded.get(n) " + loaded.get(n));
-        }
-        return loaded.get(n);
-    }
-
-    //=========================================================================
-    // Loader.processNode
-    //=========================================================================
-    void processNode(MNode n) {
-        Group parentNode = null;
-        for (MNode p : n.getParentNodes()) {
-            parentNode = (Group) resolveNode(p);
-        }
-        Node result = loaded.get(n);
-        // if the result is null, then it hasn't been added to the map yet
-        // so go ahead and process it
-        if (result == null) {
-            if (n.isInstanceOf(shadingEngineType)) {
-                //                System.out.println("==> Found a node of shadingEngineType: " + n);
-            } else if (n.isInstanceOf(lambertType)) {
-                //                System.out.println("==> Found a node of lambertType: " + n);
-            } else if (n.isInstanceOf(reflectType)) {
-                //                System.out.println("==> Found a node of reflectType: " + n);
-            } else if (n.isInstanceOf(blinnType)) {
-                //                System.out.println("==> Found a node of blinnType: " + n);
-            } else if (n.isInstanceOf(phongType)) {
-                //                System.out.println("==> Found a node of phongType: " + n);
-            } else if (n.isInstanceOf(fileType)) {
-                //                System.out.println("==> Found a node of fileType: " + n);
-            } else if (n.isInstanceOf(skinClusterType)) {
-                processClusterType(n);
-            } else if (n.isInstanceOf(meshType)) {
-                processMeshType(n, parentNode);
-            } else if (n.isInstanceOf(jointType)) {
-                processJointType(n, parentNode);
-            } else if (n.isInstanceOf(transformType)) {
-                processTransformType(n, parentNode);
-            } else if (n.isInstanceOf(animCurve)) {
-                processAnimCurve(n);
-            }
-        }
-    }
-
-    protected void processClusterType(MNode n) {
-        loaded.put(n, null);
-        MArray ma = (MArray) n.getAttr("ma");
-
-        List<Joint> jointNodes = new ArrayList<Joint>();
-        Set<Parent> jointForest = new HashSet<Parent>(); // root's children that have joints in their trees
-        for (int i = 0; i < ma.getSize(); i++) {
-            // hack... ?
-            MNode c = n.getIncomingConnectionToType("ma[" + i + "]", "joint");
-            Joint jn = (Joint) resolveNode(c);
-            jointNodes.add(jn);
-            
-            Parent rootChild = jn; // root's child, which is an ancestor of joint jn
-            while (rootChild.getParent() != null) {
-                rootChild = rootChild.getParent();
-            }
-            jointForest.add(rootChild);
-        }
-        
-        MNode outputMeshMNode = resolveOutputMesh(n);
-        MNode inputMeshMNode = resolveInputMesh(n);
-        if (inputMeshMNode == null || outputMeshMNode == null) {
-            return;
-        }
-        // We must be able to find the original converter in the meshConverters map
-        MNode origOrigMesh = resolveOrigInputMesh(n);
-        //               println("ORIG ORIG={origOrigMesh}");
-        
-        // TODO: What is with this? origMesh
-        resolveNode(origOrigMesh).setVisible(false);
-
-        MArray bindPreMatrixArray = (MArray) n.getAttr("pm");
-        Affine bindGlobalMatrix = convertMatrix((MFloatArray) n.getAttr("gm"));
-
-        Affine[] bindPreMatrix = new Affine[bindPreMatrixArray.getSize()];
-        for (int i = 0; i < bindPreMatrixArray.getSize(); i++) {
-            bindPreMatrix[i] = convertMatrix((MFloatArray) bindPreMatrixArray.getData(i));
-        }
-
-        MArray mayaWeights = (MArray) n.getAttr("wl");
-        float[][] weights = new float [jointNodes.size()][mayaWeights.getSize()];
-        for (int i=0; i<mayaWeights.getSize(); i++) {
-            MFloatArray curWeights = (MFloatArray) mayaWeights.getData(i).getData("w");
-            for (int j = 0; j < jointNodes.size(); j++) {
-                weights[j][i] = j < curWeights.getSize() ? curWeights.get(j) : 0;
-            }
-        }
-        
-        Node sourceMayaMeshNode = resolveNode(inputMeshMNode);
-        Node targetMayaMeshNode = resolveNode(outputMeshMNode);
-        
-        if (sourceMayaMeshNode.getClass().equals(PolygonMeshView.class)) {
-            PolygonMeshView sourceMayaMeshView = (PolygonMeshView) sourceMayaMeshNode;
-            PolygonMeshView targetMayaMeshView = (PolygonMeshView) targetMayaMeshNode;
-            
-            PolygonMesh sourceMesh = (PolygonMesh) sourceMayaMeshView.getMesh();
-            SkinningMesh targetMesh = new SkinningMesh(sourceMesh, weights, bindPreMatrix, bindGlobalMatrix, jointNodes, new ArrayList(jointForest));
-            targetMayaMeshView.setMesh(targetMesh);
-
-            final SkinningMeshTimer skinningMeshTimer = new SkinningMeshTimer(targetMesh);
-            if (targetMayaMeshNode.getScene() != null) {
-                skinningMeshTimer.start();
-            }
-            targetMayaMeshView.sceneProperty().addListener(new ChangeListener<Scene>() {
-                @Override
-                public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
-                    if (newValue == null) {
-                        skinningMeshTimer.stop();
-                    } else {
-                        skinningMeshTimer.start();
-                    }
-                }
-            });
-        } else {
-            Logger.getLogger(MayaImporter.class.getName()).log(Level.INFO, "Mesh skinning is not supported for triangle meshes. Select the 'Load as Polygons' option to load the mesh as polygon mesh.");
-            MeshView sourceMayaMeshView = (MeshView) sourceMayaMeshNode;
-            MeshView targetMayaMeshView = (MeshView) targetMayaMeshNode;
-            TriangleMesh sourceMesh = (TriangleMesh) sourceMayaMeshView.getMesh();
-            TriangleMesh targetMesh = (TriangleMesh) targetMayaMeshView.getMesh();
-            targetMesh.getPoints().setAll(sourceMesh.getPoints());
-            targetMesh.getTexCoords().setAll(sourceMesh.getTexCoords());
-            targetMesh.getFaces().setAll(sourceMesh.getFaces());
-            targetMesh.getFaceSmoothingGroups().setAll(sourceMesh.getFaceSmoothingGroups());
-        }
-    }
-    
-    private class SkinningMeshTimer extends AnimationTimer {
-        private SkinningMesh mesh;
-        SkinningMeshTimer(SkinningMesh mesh) {
-            this.mesh = mesh;
-        }
-        @Override
-        public void handle(long l) {
-            mesh.update();
-        }
-    }
-
-    protected Image loadImageFromFtnAttr(MNode fileNode, String name) {
-        Image image = null;
-        MString fileName = (MString) fileNode.getAttr("ftn");
-        String imageFilename = (String) fileName.get();
-        try {
-            File file = new File(imageFilename);
-            String filePath;
-            if (file.exists()) {
-                filePath = file.toURI().toString();
-            } else {
-                filePath = new URL(url, imageFilename).toString();
-            }
-            image = new Image(filePath);
-            if (DEBUG) {
-                System.out.println(name + " = " + filePath);
-                System.out.println(name + " w = " + image.getWidth() + " h = " + image.getHeight());
-            }
-        } catch (MalformedURLException ex) {
-            Logger.getLogger(MayaImporter.class.getName()).log(Level.SEVERE, "Failed to load " + name + " '" + imageFilename + "'!", ex);
-        }
-        return image;
-    }
-
-    protected void processMeshType(MNode n, Group parentNode) throws RuntimeException {
-        //=============================================================
-        // When JavaFX supports polygon mesh geometry,
-        // add the polygon mesh geometry here.
-        // Until then, add a unit square as a placeholder.
-        //=============================================================
-        Node node = resolveNode(n.getParentNodes().get(0));
-        //                if (node != null) {
-        //                if (node != null && !n.getName().endsWith("Orig")) {
-        // Original approach to mesh placeholder:
-        //                     meshParents.put(node, n);
-
-        // Try to find an image or color from n (MNode)
-        if (DEBUG) { System.out.println("________________________________________"); }
-        if (DEBUG) { System.out.println("n.getName(): " + n.getName()); }
-        if (DEBUG) { System.out.println("n.getNodeType(): " + n.getNodeType()); }
-        MNode shadingGroup = n.getOutgoingConnectionToType("iog", "shadingEngine", true);
-        MNode mat;
-        MNode mFile;
-        if (DEBUG) { System.out.println("shadingGroup: " + shadingGroup); }
-
-        MFloat3 mColor;
-        Vec3f diffuseColor = null;
-        Vec3f specularColor = null;
-
-        Image diffuseImage = null;
-        Image normalImage = null;
-        Image specularImage = null;
-        Float specularPower = null;
-
-        if (shadingGroup != null) {
-            mat = shadingGroup.getIncomingConnectionToType("ss", "lambert");
-            if (mat != null) {
-                // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
-                if (DEBUG) { System.out.println("lambert mat: " + mat); }
-                mColor = (MFloat3) mat.getAttr("c");
-                float diffuseIntensity = ((MFloat) mat.getAttr("dc")).get();
-                if (mColor != null) {
-                    diffuseColor = new Vec3f(
-                            mColor.get()[0] * diffuseIntensity,
-                            mColor.get()[1] * diffuseIntensity,
-                            mColor.get()[2] * diffuseIntensity);
-                    if (DEBUG) { System.out.println("diffuseColor = " + diffuseColor); }
-                }
-
-                mFile = mat.getIncomingConnectionToType("c", "file");
-                if (mFile != null) {
-                    diffuseImage = loadImageFromFtnAttr(mFile, "diffuseImage");
-                }
-                MNode bump2d = mat.getIncomingConnectionToType("n", "bump2d");
-                if (bump2d != null) {
-                    mFile = bump2d.getIncomingConnectionToType("bv", "file");
-                    if (mFile != null) {
-                        normalImage = loadImageFromFtnAttr(mFile, "normalImage");
-                    }
-                }
-            }
-            mat = shadingGroup.getIncomingConnectionToType("ss", "phong");
-            if (mat != null) {
-                // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
-                if (DEBUG) { System.out.println("phong mat: " + mat); }
-                mColor = (MFloat3) mat.getAttr("sc");
-                if (mColor != null) {
-                    specularColor = new Vec3f(
-                            mColor.get()[0],
-                            mColor.get()[1],
-                            mColor.get()[2]);
-                    if (DEBUG) { System.out.println("specularColor = " + specularColor); }
-                }
-                mFile = mat.getIncomingConnectionToType("sc", "file");
-                if (mFile != null) {
-                    specularImage = loadImageFromFtnAttr(mFile, "specularImage");
-                }
-
-                specularPower = ((MFloat) mat.getAttr("cp")).get();
-                if (DEBUG) { System.out.println("specularPower = " + specularPower); }
-            }
-        }
-
-        PhongMaterial material = new PhongMaterial();
-
-        if (diffuseImage != null) {
-            material.setDiffuseMap(diffuseImage);
-            material.setDiffuseColor(Color.WHITE);
-        } else {
-            if (diffuseColor != null) {
-                material.setDiffuseColor(
-                        new Color(
-                                diffuseColor.x,
-                                diffuseColor.y,
-                                diffuseColor.z, 1));
-                //                            material.setDiffuseColor(new Color(
-                //                                    0.5,
-                //                                    0.5,
-                //                                    0.5, 0));
-            } else {
-                material.setDiffuseColor(Color.GRAY);
-            }
-        }
-
-        if (normalImage != null) {
-            material.setBumpMap(normalImage);
-        }
-
-        if (specularImage != null) {
-            material.setSpecularMap(specularImage);
-        } else {
-            if (specularColor != null && specularPower != null) {
-                material.setSpecularColor(
-                        new Color(
-                                specularColor.x,
-                                specularColor.y,
-                                specularColor.z, 1));
-                material.setSpecularPower(specularPower / 33);
-                //                            material.setSpecularColor(new Color(
-                //                                    0,
-                //                                    1,
-                //                                    0, 1));
-                //                            material.setSpecularPower(1);
-            } else {
-                //                            material.setSpecularColor(new Color(
-                //                                    0.2,
-                //                                    0.2,
-                //                                    0.2, 1));
-                //                            material.setSpecularPower(1);
-                material.setSpecularColor(null);
-            }
-        }
-
-        Object mesh = convertToFXMesh(n);
-
-        if (asPolygonMesh) {
-            PolygonMeshView mv = new PolygonMeshView();
-            mv.setId(n.getName());
-            mv.setMaterial(material);
-            mv.setMesh((PolygonMesh) mesh);
-//            mv.setCullFace(CullFace.NONE); //TODO
-            loaded.put(n, mv);
-            if (node != null) {
-                ((Group) node).getChildren().add(mv);
-            }
-        } else {
-            MeshView mv = new MeshView();
-            mv.setId(n.getName());
-            mv.setMaterial(material);
-
-//            // TODO HACK for [JIRA] (RT-30449) FX 8 3D: Need to handle mirror transformation (flip culling);
-//            mv.setCullFace(CullFace.FRONT);
-
-            mv.setMesh((TriangleMesh) mesh);
-
-            loaded.put(n, mv);
-            if (node != null) {
-                ((Group) node).getChildren().add(mv);
-            }
-        }
-    }
-    
-    protected void processJointType(MNode n, Group parentNode) {
-        // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
-        Node result;
-        MFloat3 t = (MFloat3) n.getAttr("t");
-        MFloat3 jo = (MFloat3) n.getAttr("jo");
-        MFloat3 r = (MFloat3) n.getAttr("r");
-        MFloat3 s = (MFloat3) n.getAttr("s");
-        String id = n.getName();
-
-        Joint j = new Joint();
-        j.setId(id);
-
-        // There's various ways to get the same thing:
-        // n.getAttr("r").get()[0]
-        // n.getAttr("r").getX()
-        // n.getAttr("rx")
-        // Up to you which you prefer
-
-        j.t.setX(t.get()[0]);
-        j.t.setY(t.get()[1]);
-        j.t.setZ(t.get()[2]);
-
-        // if ssc (Segment Scale Compensate) is false, then it is = 1, 1, 1
-        boolean ssc = ((MBool) n.getAttr("ssc")).get();
-        if (ssc) {
-            List<MNode> parents = n.getParentNodes();
-            if (parents.size() > 0) {
-                MFloat3 parent_s = (MFloat3) n.getParentNodes().get(0).getAttr("s");
-                j.is.setX(1f / parent_s.getX());
-                j.is.setY(1f / parent_s.getY());
-                j.is.setZ(1f / parent_s.getZ());
-            } else {
-                j.is.setX(1f);
-                j.is.setY(1f);
-                j.is.setZ(1f);
-            }
-        } else {
-            j.is.setX(1f);
-            j.is.setY(1f);
-            j.is.setZ(1f);
-        }
-
-        /*
-        // This code doesn't seem to work right:
-        MFloat jox = (MFloat) n.getAttr("jox");
-        MFloat joy = (MFloat) n.getAttr("joy");
-        MFloat joz = (MFloat) n.getAttr("joz");
-        j.jox.setAngle(jox.get());
-        j.joy.setAngle(joy.get());
-        j.joz.setAngle(joz.get());
-        // The following code works right:
-        */
-
-        if (jo != null) {
-            j.jox.setAngle(jo.getX());
-            j.joy.setAngle(jo.getY());
-            j.joz.setAngle(jo.getZ());
-        } else {
-            j.jox.setAngle(0f);
-            j.joy.setAngle(0f);
-            j.joz.setAngle(0f);
-        }
-
-        MFloat rx = (MFloat) n.getAttr("rx");
-        MFloat ry = (MFloat) n.getAttr("ry");
-        MFloat rz = (MFloat) n.getAttr("rz");
-        j.rx.setAngle(rx.get());
-        j.ry.setAngle(ry.get());
-        j.rz.setAngle(rz.get());
-
-        j.s.setX(s.get()[0]);
-        j.s.setY(s.get()[1]);
-        j.s.setZ(s.get()[2]);
-
-        result = j;
-        // Add the Joint to the map
-        loaded.put(n, j);
-        j.setDepthTest(DepthTest.ENABLE);
-        // Add the Joint to its JavaFX parent
-        if (parentNode != null) {
-            parentNode.getChildren().add(j);
-            if (DEBUG) System.out.println("j.getDepthTest() : " + j.getDepthTest());
-        }
-        if (parentNode == null || !(parentNode instanceof Joint)) {
-            // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
-            // rootJoint = j;
-        }
-    }
-
-    protected void processTransformType(MNode n, Group parentNode) {
-        MFloat3 t = (MFloat3) n.getAttr("t");
-        MFloat3 r = (MFloat3) n.getAttr("r");
-        MFloat3 s = (MFloat3) n.getAttr("s");
-        String id = n.getName();
-        // ignore cameras
-        if ("persp".equals(id) ||
-                "top".equals(id) ||
-                "front".equals(id) ||
-                "side".equals(id)) {
-            return;
-        }
-
-        MayaGroup mGroup = new MayaGroup();
-        mGroup.setId(n.getName());
-        // g.setBlendMode(BlendMode.SRC_OVER);
-
-        // if (DEBUG) System.out.println("t = " + t);
-        // if (DEBUG) System.out.println("r = " + r);
-        // if (DEBUG) System.out.println("s = " + s);
-
-        mGroup.t.setX(t.get()[0]);
-        mGroup.t.setY(t.get()[1]);
-        mGroup.t.setZ(t.get()[2]);
-
-        MFloat rx = (MFloat) n.getAttr("rx");
-        MFloat ry = (MFloat) n.getAttr("ry");
-        MFloat rz = (MFloat) n.getAttr("rz");
-        mGroup.rx.setAngle(rx.get());
-        mGroup.ry.setAngle(ry.get());
-        mGroup.rz.setAngle(rz.get());
-
-        mGroup.s.setX(s.get()[0]);
-        mGroup.s.setY(s.get()[1]);
-        mGroup.s.setZ(s.get()[2]);
-
-        MFloat rptx = (MFloat) n.getAttr("rptx");
-        MFloat rpty = (MFloat) n.getAttr("rpty");
-        MFloat rptz = (MFloat) n.getAttr("rptz");
-        mGroup.rpt.setX(rptx.get());
-        mGroup.rpt.setY(rpty.get());
-        mGroup.rpt.setZ(rptz.get());
-
-        MFloat rpx = (MFloat) n.getAttr("rpx");
-        MFloat rpy = (MFloat) n.getAttr("rpy");
-        MFloat rpz = (MFloat) n.getAttr("rpz");
-        mGroup.rp.setX(rpx.get());
-        mGroup.rp.setY(rpy.get());
-        mGroup.rp.setZ(rpz.get());
-
-        mGroup.rpi.setX(-rpx.get());
-        mGroup.rpi.setY(-rpy.get());
-        mGroup.rpi.setZ(-rpz.get());
-
-        MFloat spx = (MFloat) n.getAttr("spx");
-        MFloat spy = (MFloat) n.getAttr("spy");
-        MFloat spz = (MFloat) n.getAttr("spz");
-        mGroup.sp.setX(spx.get());
-        mGroup.sp.setY(spy.get());
-        mGroup.sp.setZ(spz.get());
-
-        mGroup.spi.setX(-spx.get());
-        mGroup.spi.setY(-spy.get());
-        mGroup.spi.setZ(-spz.get());
-
-        // Add the MayaGroup to the map
-        loaded.put(n, mGroup);
-        // Add the MayaGroup to its JavaFX parent
-        if (parentNode != null) {
-            parentNode.getChildren().add(mGroup);
-        }
-    }
-
-    protected void processAnimCurve(MNode n) {
-        // if (DEBUG) System.out.println("processing anim curve");
-        List<MPath> toPaths = n.getPathsConnectingFrom("o");
-        loaded.put(n, null);
-        for (MPath path : toPaths) {
-            MNode toNode = path.getTargetNode();
-            // if (DEBUG) System.out.println("toNode = "+ toNode.getNodeType());
-            if (toNode.isInstanceOf(transformType)) {
-                Node to = resolveNode(toNode);
-                if (to instanceof MayaGroup) {
-                    MayaGroup g = (MayaGroup) to;
-                    DoubleProperty ref = null;
-                    String s = path.getComponentSelector();
-                    // if (DEBUG) System.out.println("selector = " + s);
-                    if ("t[0]".equals(s)) {
-                        ref = g.t.xProperty();
-                    } else if ("t[1]".equals(s)) {
-                        ref = g.t.yProperty();
-                    } else if ("t[2]".equals(s)) {
-                        ref = g.t.zProperty();
-                    } else if ("s[0]".equals(s)) {
-                        ref = g.s.xProperty();
-                    } else if ("s[1]".equals(s)) {
-                        ref = g.s.yProperty();
-                    } else if ("s[2]".equals(s)) {
-                        ref = g.s.zProperty();
-                    } else if ("r[0]".equals(s)) {
-                        ref = g.rx.angleProperty();
-                    } else if ("r[1]".equals(s)) {
-                        ref = g.ry.angleProperty();
-                    } else if ("r[2]".equals(s)) {
-                        ref = g.rz.angleProperty();
-                    } else if ("rp[0]".equals(s)) {
-                        ref = g.rp.xProperty();
-                    } else if ("rp[1]".equals(s)) {
-                        ref = g.rp.yProperty();
-                    } else if ("rp[2]".equals(s)) {
-                        ref = g.rp.zProperty();
-                    } else if ("sp[0]".equals(s)) {
-                        ref = g.sp.xProperty();
-                    } else if ("sp[1]".equals(s)) {
-                        ref = g.sp.yProperty();
-                    } else if ("sp[2]".equals(s)) {
-                        ref = g.sp.zProperty();
-                    }
-                    // Note: may also want to consider adding rpt in addition to rp and sp
-                    if (ref != null) {
-                        convertAnimCurveRange(n, ref, true);
-                    }
-                }
-                // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
-                if (to instanceof Joint) {
-                    Joint j = (Joint) to;
-                    DoubleProperty ref = null;
-                    String s = path.getComponentSelector();
-                    // if (DEBUG) System.out.println("selector = " + s);
-                    if ("t[0]".equals(s)) {
-                        ref = j.t.xProperty();
-                    } else if ("t[1]".equals(s)) {
-                        ref = j.t.yProperty();
-                    } else if ("t[2]".equals(s)) {
-                        ref = j.t.zProperty();
-                    } else if ("s[0]".equals(s)) {
-                        ref = j.s.xProperty();
-                    } else if ("s[1]".equals(s)) {
-                        ref = j.s.yProperty();
-                    } else if ("s[2]".equals(s)) {
-                        ref = j.s.zProperty();
-                    } else if ("jo[0]".equals(s)) {
-                        ref = j.jox.angleProperty();
-                    } else if ("jo[1]".equals(s)) {
-                        ref = j.joy.angleProperty();
-                    } else if ("jo[2]".equals(s)) {
-                        ref = j.joz.angleProperty();
-                    } else if ("r[0]".equals(s)) {
-                        ref = j.rx.angleProperty();
-                    } else if ("r[1]".equals(s)) {
-                        ref = j.ry.angleProperty();
-                    } else if ("r[2]".equals(s)) {
-                        ref = j.rz.angleProperty();
-                    }
-                    if (ref != null) {
-                        convertAnimCurveRange(n, ref, true);
-                    }
-                }
-                break;
-            }
-        }
-    }
-
-    private Object convertToFXMesh(MNode n) {
-        mVerts = (MFloat3Array) n.getAttr("vt");
-        MPolyFace mPolys = (MPolyFace) n.getAttr("fc");
-        mPointTweaks = (MFloat3Array) n.getAttr("pt");
-        MInt3Array mEdges = (MInt3Array) n.getAttr("ed");
-        edgeData = mEdges.get();
-        uvSet = ((MArray) n.getAttr("uvst")).get();
-        String currentUVSet = ((MString) n.getAttr("cuvs")).get();
-        for (int i = 0; i < uvSet.size(); i++) {
-            if (((MString) uvSet.get(i).getData("uvsn")).get().equals(currentUVSet)) {
-                uvChannel = i;
-            }
-        }
-
-        if (mPolys.getFaces() == null) {
-            if (asPolygonMesh) {
-                return new PolygonMesh();
-            } else {
-                return new TriangleMesh();
-            }
-        }
-
-        MFloat3Array normals = (MFloat3Array) n.getAttr("n");
-        return buildMeshData(mPolys.getFaces(), normals);
-    }
-
-    private int edgeVert(int edgeNumber, boolean start) {
-        boolean reverse = (edgeNumber < 0);
-        if (reverse) {
-            edgeNumber = reverse(edgeNumber);
-            return edgeData[3 * edgeNumber + (start ? 1 : 0)];
-        } else {
-            return edgeData[3 * edgeNumber + (start ? 0 : 1)];
-        }
-    }
-
-    private int reverse(int edge) {
-        if (edge < 0) {
-            return -edge - 1;
-        }
-        return edge;
-    }
-
-    private boolean edgeIsSmooth(int edgeNumber) {
-        edgeNumber = reverse(edgeNumber);
-        return edgeData[3 * edgeNumber + 2] != 0;
-    }
-
-    private int edgeStart(int edgeNumber) {
-        return edgeVert(edgeNumber, true);
-    }
-
-    private int edgeEnd(int edgeNumber) {
-        return edgeVert(edgeNumber, false);
-    }
-
-    private float[] getTexCoords(int uvChannel) {
-        if (uvSet == null || uvChannel < 0 || uvChannel >= uvSet.size()) {
-            return new float[] {0,0};
-        }
-        MCompound compound = (MCompound) uvSet.get(uvChannel);
-        MFloat2Array uvs = (MFloat2Array) compound.getFieldData("uvsp");
-        if (uvs == null || uvs.get() == null) {
-            return new float[] {0,0};
-        }
-
-        float[] texCoords = new float[uvs.getSize() * 2];
-        float[] uvsData = uvs.get();
-        for (int i = 0; i < uvs.getSize(); i++) {
-            //note the 1 - v
-            texCoords[i * 2] = uvsData[2 * i];
-            texCoords[i * 2 + 1] = 1 - uvsData[2 * i + 1];
-        }
-        return texCoords;
-    }
-
-    private void getVert(int index, Vec3f vert) {
-        float[] verts = mVerts.get();
-        float[] tweaks = null;
-        if (mPointTweaks != null) {
-            tweaks = mPointTweaks.get();
-            if (tweaks != null) {
-                if ((3 * index + 2) >= tweaks.length) {
-                    tweaks = null;
-                }
-            }
-        }
-        if (tweaks == null) {
-            vert.set(verts[3 * index + 0], verts[3 * index + 1], verts[3 * index + 2]);
-        } else {
-            vert.set(
-                    verts[3 * index + 0] + tweaks[3 * index + 0],
-                    verts[3 * index + 1] + tweaks[3 * index + 1],
-                    verts[3 * index + 2] + tweaks[3 * index + 2]);
-        }
-    }
-
-    float FPS = 24.0f;
-    float TAN_FIXED = 1;
-    float TAN_LINEAR = 2;
-    float TAN_FLAT = 3;
-    float TAN_STEPPED = 5;
-    float TAN_SPLINE = 9;
-    float TAN_CLAMPED = 10;
-    float TAN_PLATEAU = 16;
-
-    // Experimentally trying to land the frames on whole frame values
-    // Duration is still double, but internally, in Animation/Timeline,
-    // the time is discrete.  6000 units per second.
-    // Without this EPSILON, the frames might not land on whole frame values.
-    // 0.000001f seems to work for now
-    // 0.0000001f was too small on a trial run
-    static final float EPSILON = 0.000001f;
-
-    static final float MAXIMUM = 10000000.0f;
-
-    // Empirically derived from playing with animation curve editor
-    float TAN_EPSILON = 0.05f;
-
-    //=========================================================================
-    // Loader.convertAnimCurveRange
-    //-------------------------------------------------------------------------
-    // This method adds to keyFrameMap which is a
-    // TreeMap Map<Float, List<KeyValue>>
-    //=========================================================================
-    void convertAnimCurveRange(
-            MNode n, final DoubleProperty property,
-            boolean convertAnglesToDegrees) {
-        Collection inputs = n.getConnectionsTo("i");
-        boolean isDrivenAnimCurve = (inputs.size() > 0);
-        boolean useTangentInterpolator = true;  // use the NEW tangent interpolator
-
-        //---------------------------------------------------------------------
-        // Tangent types we need to handle:
-        //   2 = Linear
-        //       - The in/out tangent points in the direction of the previous/next key
-        //   3 = Flat
-        //       - The in/out tangent has no y component
-        //   5 = Stepped
-        //       - If this is seen on the out tangent of the previous
-        //         frame, immediately goes to the next value
-        //   9 = Spline
-        //       - The in / out tangents around the current keyframe
-        //         match the slope defined by the previous and next
-        //         keyframes.
-        //  10 = Clamped
-        //       - Uses spline tangents unless the keyframe is very close to the next or
-        //         previous value, in which case it uses linear tangents.
-        //  16 = Plateau
-        //       - Generally speaking, if the keyframe is a local maximum or minimum,
-        //         uses flat tangents to prevent the curve from overshooting the keyframe.
-        //         Seems to use spline tangents when the keyframe is not a local extremum.
-        //         There is an epsilon factor built in when deciding whether the flattening
-        //         behavior is to be applied.
-        // Tangent types we aren't handling:
-        //   1 = Fixed
-        //  17 = StepNext
-        //---------------------------------------------------------------------
-
-        MArray ktv = (MArray) n.getAttr("ktv");
-        MInt tan = (MInt) n.getAttr("tan");
-        int len = ktv.getSize();
-
-        // Note: the kix, kiy, kox, koy from Maya
-        // are most likely unit vectors [kix, kiy] and [kox, koy]
-        // in some tricky units that Ken figured out.
-        MFloatArray kix = (MFloatArray) n.getAttr("kix");
-        MFloatArray kiy = (MFloatArray) n.getAttr("kiy");
-        MFloatArray kox = (MFloatArray) n.getAttr("kox");
-        MFloatArray koy = (MFloatArray) n.getAttr("koy");
-        MIntArray kit = (MIntArray) n.getAttr("kit");
-        MIntArray kot = (MIntArray) n.getAttr("kot");
-        boolean hasTangent = kix != null && kix.get() != null && kix.get().length > 0;
-        boolean isRotation = n.isInstanceOf(animCurveTA) || n.isInstanceOf(animCurveUA);
-        boolean keyTimesInSeconds =
-                (n.isInstanceOf(animCurveUA) || n.isInstanceOf(animCurveUL) ||
-                        n.isInstanceOf(animCurveUT) || n.isInstanceOf(animCurveUU));
-
-        List<KeyFrame> drivenKeys = new LinkedList();
-
-        // Many incoming animation curves start at keyframe 1; to
-        // correctly interpret these we need to subtract off one frame
-        // from each key time
-        boolean needsOneFrameAdjustment = false;
-
-        // For computing tangents around the current point
-        float[] keyTimes = new float[3];
-        float[] keyValues = new float[3];
-        boolean[] keysValid = new boolean[3];
-        float[] prevOutTan = new float[3];  // for orig interpolator
-        float[] curOutTan = new float[3];  // for tan interpolator
-        float[] curInTan = new float[3];  // for both interpolators
-        Collection toPaths = n.getPathsConnectingFrom("o");
-        String keyName = null;
-        String targetName = null;
-        for (Object obj : toPaths) {
-            MPath toPath = (MPath) obj;
-            keyName = toPath.getComponentSelector();
-            targetName = toPath.getTargetNode().getName();
-        }
-
-        for (int j = 0; j < len; j++) {
-            MCompound k1 = (MCompound) ktv.getData(j);
-
-            float kt = ((MFloat) k1.getData("kt")).get();
-            float kv = ((MFloat) k1.getData("kv")).get();
-            if (j == 0 && !keyTimesInSeconds) {
-                needsOneFrameAdjustment = (kt != 0.0f);
-                //                if (DEBUG) System.out.println("needsOneFrameAdjustment = " + needsOneFrameAdjustment);
-            }
-
-            //------------------------------------------------------------
-            // Find out the previous times, values, and durations,
-            // if they exist
-            // (this code is both for tan interpolator and orig interpolator)
-            // Ken's duration is now called durationPrev
-            // Ken's k0 is now called kPrev
-            //------------------------------------------------------------
-            float durationPrev = 0.0f;
-            float ktPrev = 0.0f;
-            float kvPrev = 0.0f;
-            if (j > 0) {
-                MCompound kPrev = (MCompound) ktv.getData(j - 1);
-                ktPrev = ((MFloat) kPrev.getData("kt")).get();
-                kvPrev = ((MFloat) kPrev.getData("kv")).get();  // NEW
-                durationPrev = kt - ktPrev;
-            }
-
-            //------------------------------------------------------------
-            // Find out the next times, values, and durations,
-            // if they exist
-            // (this code is specifically for TangentInterpolator)
-            //------------------------------------------------------------
-            float durationNext = 0.0f;
-            float ktNext = 0.0f;
-            float kvNext = 0.0f;
-            if ((j + 1) < len) {
-                MCompound kNext = (MCompound) ktv.getData(j + 1);
-                ktNext = ((MFloat) kNext.getData("kt")).get();
-                kvNext = ((MFloat) kNext.getData("kv")).get();  // NEW
-                durationNext = ktNext - kt;
-            }
-
-            if (!keyTimesInSeconds) {
-                // convert frames to seconds
-                kt /= FPS;
-                ktPrev /= FPS;  // NEW
-                ktNext /= FPS;  // NEW
-            } else {
-                // convert seconds to frames
-                durationPrev *= FPS;
-                durationNext *= FPS;  // NEW
-            }
-            /*
-              var ktd = kt;
-              if (range != null) {
-              if (range.start > ktd or range.end < ktd) {
-              continue;
-              }
-              }
-            */
-
-
-            // Determine the tangent types on both sides
-            int prevOutTanType = tan.get();  // for orig interpolator
-            int curInTanType = tan.get();  // for both interpolators
-            int curOutTanType = tan.get();  // for tan intepolator
-            if (j > 0 && j < kot.getSize()) {
-                int tmp = kot.get(j - 1);
-                // Will be 0 if not actually written in the file
-                if (tmp != 0) {
-                    prevOutTanType = tmp;
-                }
-            }
-            if (j < kot.getSize()) {  // NEW
-                int tmp = kot.get(j);
-                if (tmp != 0) {
-                    curOutTanType = tmp;
-                }
-            }
-            if (j < kit.getSize()) {
-                int tmp = kit.get(j);
-                if (tmp != 0) {
-                    curInTanType = tmp;
-                }
-            }
-
-            // Get previous out tangent
-            getTangent(
-                    ktv, kix, kiy, kox, koy,
-                    j - 1,
-                    prevOutTanType,
-                    false,
-                    isRotation,
-                    keyTimesInSeconds,
-                    prevOutTan,
-                    // Temporaries
-                    keyTimes, keyValues, keysValid);
-
-            // NEW
-            // for tangentInterpolator, we also need curOutTangent
-            // Get current out tangent
-            getTangent(
-                    ktv, kix, kiy, kox, koy,
-                    j,
-                    curOutTanType,
-                    false,
-                    isRotation,
-                    keyTimesInSeconds,
-                    curOutTan,
-                    // Temporaries
-                    keyTimes, keyValues, keysValid);
-
-            // Get current in tangent
-            getTangent(
-                    ktv, kix, kiy, kox, koy,
-                    j,
-                    curInTanType,
-                    true,
-                    isRotation,
-                    keyTimesInSeconds,
-                    curInTan,
-                    // Temporaries
-                    keyTimes, keyValues, keysValid);
-
-            // Create the appropriate interpolator type:
-            // [*] DISCRETE for STEPPED type for prevOutTanType
-            // [*] Interpolator.TANGENT
-            // [*] custom Maya animation curve interpolator if specified
-            Interpolator interp = Interpolator.DISCRETE;
-            if (prevOutTanType == TAN_STEPPED) {
-                // interp = DISCRETE;
-            } else {
-                if (useTangentInterpolator) {
-                    //--------------------------------------------------
-                    // TangentIntepolator
-                    double k_ix = curInTan[0];
-                    double k_iy = curInTan[1];
-                    // don't use prevOutTan for tangentInterpolator
-                    // double k_ox = prevOutTan[0];
-                    // double k_oy = prevOutTan[1];
-                    double k_ox = curOutTan[0];
-                    double k_oy = curOutTan[1];
-
-                    /*
-                      if (DEBUG) System.out.println("n.getName(): " + n.getName());
-                      if (DEBUG) System.out.println("(k_ix = " + k_ix + ", " +
-                      "k_iy = " + k_iy + ", " +
-                      "k_ox = " + k_ox + ", " +
-                      "k_oy = " + k_oy + ")"
-                      );
-                    */
-
-                    // if (DEBUG) System.out.println("FPS = " + FPS);
-
-                    double inTangent = 0.0;
-                    double outTangent = 0.0;
-
-                    // Compute the in tangent
-                    if (k_ix != 0) {
-                        inTangent = k_iy / (k_ix * FPS);
-                    }
-                    // Compute the out tangent
-                    if (k_ox != 0) {
-                        outTangent = k_oy / (k_ox * FPS);
-                    }
-
-                    // Compute 1/3 of the time interval of this keyframe
-                    double oneThirdDeltaPrev = durationPrev / 3.0f;
-                    double oneThirdDeltaNext = durationNext / 3.0f;
-
-                    // Note: for angular animation curves, the tangents encode
-                    // changes in radians rather than degrees. Now that our
-                    // animation curves also emit radians, no conversion is
-                    // necessary here.
-                    double inTangentValue = -1 * inTangent * oneThirdDeltaPrev + kv;
-                    double outTangentValue = outTangent * oneThirdDeltaNext + kv;
-                    // We need to add "+ kv", because the value for the tangent
-                    // interpolator is in "world space" and not relative to the key
-
-                    if (inTangentValue > MAXIMUM) {
-                        inTangentValue = MAXIMUM;
-                    }
-                    if (outTangentValue > MAXIMUM) {
-                        outTangentValue = MAXIMUM;
-                    }
-
-                    double timeDeltaPrev = (durationPrev / FPS) * 1000f / 3.0f;  // in ms
-                    double timeDeltaNext = (durationNext / FPS) * 1000f / 3.0f;  // in ms
-
-                    if (true) {
-                        //                        if (DEBUG) System.out.println("________________________________________");
-                        //                        if (DEBUG) System.out.println("n.getName() = " + n.getName());
-                        //                        if (DEBUG) System.out.println("kv = " + kv);
-                        //                        if (DEBUG) System.out.println("Interpolator.TANGENT(" +
-                        //                                           "Duration.valueOf(" +
-                        //                                           timeDeltaPrev + ")" + ", " +
-                        //                                           inTangentValue + ", " +
-                        //                                           "Duration.valueOf(" +
-                        //                                           timeDeltaNext + ")" + ", " +
-                        //                                           outTangentValue + ");"
-                        //                                           );
-
-                    }
-
-                    //--------------------------------------------------
-                    // Given the diagram below, where
-                    //     k = keyframe
-                    //     i = inTangent
-                    //     o = outTangent
-                    //     + = timeDelta
-                    // Its extremely important to note that
-                    // inTangent's and outTangent's values for "i" and "o"
-                    // are NOT relative to "k".  They are in "worldSpace".
-                    // However, the timeDeltaNext and timeDeltaPrev
-                    // are in fact relative to the keyframe "k",
-                    // and are always an absolute value.
-                    // So, in summary,
-                    // the Y-axis values are not relative, but
-                    // the X-axis values are relative, and always positive
-                    //--------------------------------------------------
-                    // (Y-axis worldSpace value for i)
-                    //    inTangent i
-                    //              |
-                    //              |        timeDeltaNext (relative to x)
-                    //              |         |<------->|
-                    //              +---------k---------+
-                    //              |<------->|         |
-                    //             timeDeltaPrev        |
-                    //                                  |
-                    //                                  o outTangent
-                    //                  (Y-axis worldSpace value for o)
-                    //--------------------------------------------------
-                    Duration inDuration = Duration.millis(timeDeltaPrev);
-                    if (inDuration.toMillis() == 0) {
-                        interp = Interpolator.TANGENT(Duration.millis(timeDeltaNext), outTangentValue);
-                    } else {
-                        interp = Interpolator.TANGENT(
-                                inDuration, inTangentValue,
-                                Duration.millis(timeDeltaNext), outTangentValue);
-                    }
-                } else {
-                    MayaAnimationCurveInterpolator mayaInterp =
-                            createMayaAnimationCurveInterpolator(
-                                    prevOutTan[0], prevOutTan[1],
-                                    curInTan[0], curInTan[1],
-                                    durationPrev,
-                                    true);
-                    // mayaInterp.isRotation = isRotation;  // was commented out long ago by Ken/Chris
-                    // mayaInterp.debug = targetName + "." + keyName + "@"+ kt;
-                    interp = mayaInterp;
-                }
-            }
-
-            float t = kt - EPSILON;
-            if (t < 0.0) {
-                continue; // just skipping all the negative frames
-            }
-
-            /*
-            // This was the old way of adjusting
-            // for the one frame adjustment.
-            if (needsOneFrameAdjustment) {
-                t = kt - 1.0f/FPS;
-            } else {
-                t = kt;
-            }
-            // The new way is below ...
-            // See: (needsOneFrameAdjustment && (j == 0))
-            */
-
-            // if (DEBUG) System.out.println("j = " + j);
-            //            if (DEBUG) System.out.println("t = " + t);
-            if (isRotation) {
-                // Maya angular animation curves implicitly output in radians.
-                // In order to properly process them throughout the utility node
-                // network, we have to follow this convention, and implicitly
-                // convert the inputs of transforms' rotation angles to degrees
-                // at the end.
-                if (!convertAnglesToDegrees) {
-                    kv = (float) Math.toRadians(kv);
-                }
-            }
-            // if (DEBUG) System.out.println("creating key value at: " + t + ": " + targetName + "." + keyName);
-            KeyValue keyValue = new KeyValue(property, kv, interp);  // [!] API change
-
-            // If the first frame is at frame 1,
-            // at least for now, try adding in a frame at frame 0
-            // which is a duplicate of the frame at frame 1,
-            // to counter-act some strange behavior we are seeing
-            // if there is no key at frame 0.
-            if (needsOneFrameAdjustment && (j == 0)) {
-                if (DEBUG) System.out.println("[!] ATTEMPTING FRAME ONE ADJUSTMENT [!]");
-                // [!] API change
-                // KeyValue keyValue0 = new KeyValue(property, kv, Interpolator.LINEAR);
-                KeyValue keyValue0 = new KeyValue(property, kv);
-                addKeyframe(0.0f, keyValue0);
-            }
-
-            // Add keyframe
-            addKeyframe(t, keyValue);
-
-            /*
-            // If you're at the last keyframe,
-            // at least for now, try adding in an extra frame
-            // to pad the ending
-            if (j == (len - 1)) {
-                addKeyframe((t+0.0001667f), keyValue);
-            }
-            */
-        }
-    }
-
-    //=========================================================================
-    // Loader.addKeyframe
-    //=========================================================================
-    void addKeyframe(float t, KeyValue keyValue) {
-        List<KeyValue> vals = keyFrameMap.get(t);
-        if (vals == null) {
-            vals = new LinkedList<KeyValue>();
-            keyFrameMap.put(t, vals);
-        }
-        vals.add(keyValue);
-    }
-
-    //=========================================================================
-    // Loader.createMayaAnimationCurveInterpolator
-    //=========================================================================
-    MayaAnimationCurveInterpolator createMayaAnimationCurveInterpolator(
-            float kox,
-            float koy,
-            float kix,
-            float kiy,
-            float duration,
-            boolean hasTangent) {
-        if (duration == 0.0f) {
-            return new MayaAnimationCurveInterpolator(0, 0, true);
-        } else {
-            // Compute the out tangent
-            float outTangent = koy / (kox * FPS);
-            // Compute the in tangent
-            float inTangent = kiy / (kix * FPS);
-            // Compute 1/3 of the time interval of this keyframe
-            float oneThirdDelta = duration / 3.0f;
-
-            // Note: for angular animation curves, the tangents encode
-            // changes in radians rather than degrees. Now that our
-            // animation curves also emit radians, no conversion is
-            // necessary here.
-            float p1Delta = outTangent * oneThirdDelta;
-            float p2Delta = -inTangent * oneThirdDelta;
-            return new MayaAnimationCurveInterpolator(p1Delta, p2Delta, false);
-        }
-    }
-
-    //=========================================================================
-    // Loader.getTangent
-    //=========================================================================
-    void getTangent(
-            MArray ktv,
-            MFloatArray kix,
-            MFloatArray kiy,
-            MFloatArray kox,
-            MFloatArray koy,
-            int index,
-            int tangentType,
-            boolean inTangent,
-            boolean isRotation,
-            boolean keyTimesInSeconds,
-            float[] result,
-            // Temporaries
-            float[] tmpKeyTimes,
-            float[] tmpKeyValues,
-            boolean[] tmpKeysValid) {
-        float[] output = result;
-        float[] keyTimes = tmpKeyTimes;
-        float[] keyValues = tmpKeyValues;
-        boolean[] keysValid = tmpKeysValid;
-        if (inTangent) {
-            if (index >= 0 && index < kix.getSize() && index < kiy.getSize()) {
-                output[0] = kix.get(index);
-                output[1] = kiy.get(index);
-                if (output[0] != 0.0f ||
-                        output[1] != 0.0f) {
-                    // A keyframe was specified in the file
-                    return;
-                }
-            }
-        } else {
-            if (index >= 0 && index < kox.getSize() && index < koy.getSize()) {
-                output[0] = kox.get(index);
-                output[1] = koy.get(index);
-                if (output[0] != 0.0f ||
-                        output[1] != 0.0f) {
-                    // A keyframe was specified in the file
-                    return;
-                }
-            }
-        }
-
-        // Need to compute the tangent from the surrounding key times and values
-        int i = -1;
-        while (i < 2) {
-            int cur = index + i;
-            if (cur >= 0 && cur < ktv.getSize()) {
-                MCompound k1 = (MCompound) ktv.getData(cur);
-                float kt = ((MFloat) k1.getData("kt")).get();
-                if (keyTimesInSeconds) {
-                    // Convert seconds to frames
-                    kt *= FPS;
-                }
-                float kv = ((MFloat) k1.getData("kv")).get();
-                if (isRotation) {
-                    // Maya angular animation curves implicitly output in radians -- see below
-                    kv = (float) Math.toRadians(kv);
-                }
-                keyTimes[1 + i] = kt;
-                keyValues[1 + i] = kv;
-                keysValid[1 + i] = true;
-            } else {
-                keysValid[1 + i] = false;
-            }
-            ++i;
-        }
-        computeTangent(keyTimes, keyValues, keysValid, tangentType, inTangent, result);
-    }
-
-    //=========================================================================
-    // Loader.computeTangent
-    //=========================================================================
-    void computeTangent(
-            float[] keyTimes,
-            float[] keyValues,
-            boolean[] keysValid,
-            float tangentType,
-            boolean inTangent,
-            float[] computedTangent) {
-        float[] output = computedTangent;
-        if (tangentType == TAN_LINEAR) {
-            float x0;
-            float x1;
-            float y0;
-            float y1;
-            if (inTangent) {
-                if (!keysValid[0]) {
-                    // Start of the animation curve: doesn't matter
-                    output[0] = 1.0f;
-                    output[1] = 0.0f;
-                    return;
-                }
-                x0 = keyTimes[0];
-                x1 = keyTimes[1];
-                y0 = keyValues[0];
-                y1 = keyValues[1];
-            } else {
-                if (!keysValid[2]) {
-                    // End of the animation curve: doesn't matter
-                    output[0] = 1.0f;
-                    output[1] = 0.0f;
-                    return;
-                }
-                x0 = keyTimes[1];
-                x1 = keyTimes[2];
-                y0 = keyValues[1];
-                y1 = keyValues[2];
-            }
-            float dx = x1 - x0;
-            float dy = y1 - y0;
-            output[0] = dx;
-            output[1] = dy;
-            // Fall through to perform normalization
-        } else if (tangentType == TAN_FLAT) {
-            output[0] = 1.0f;
-            output[1] = 0.0f;
-            return;
-        } else if (tangentType == TAN_STEPPED) {
-            // Doesn't matter what the tangent values are -- will use discrete type interpolator
-            return;
-        } else if (tangentType == TAN_SPLINE) {
-            // Whether we're computing the in or out tangent, if we don't have one or the other
-            // keyframe, it reduces to a simpler case
-            if (!(keysValid[0] && keysValid[2])) {
-                // Reduces to the linear case
-                computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
-                return;
-            }
-
-            // Figure out the slope between the adjacent keyframes
-            output[0] = keyTimes[2] - keyTimes[0];
-            output[1] = keyValues[2] - keyValues[0];
-        } else if (tangentType == TAN_CLAMPED) {
-            if (!(keysValid[0] && keysValid[2])) {
-                // Reduces to the linear case at the ends of the animation curve
-                computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
-                return;
-            }
-
-            float inDiff = Math.abs(keyValues[1] - keyValues[0]);
-            float outDiff = Math.abs(keyValues[2] - keyValues[1]);
-
-            if (inDiff <= TAN_EPSILON || outDiff <= TAN_EPSILON) {
-                // The Maya docs say that this reduces to the linear
-                // case. If this were true, then the apparent behavior
-                // would be to compute the linear tangent between the
-                // two keyframes which are closest together, and
-                // reflect that tangent about the current keyframe.
-                // computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, (inDiff < outDiff), computedTangent);
-
-                // However, experimentation in the curve editor
-                // clearly indicates for our test cases that flat
-                // rather than linear interpolation is used in this
-                // case. Therefore to match Maya's actual behavior
-                // more closely we do the following.
-                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
-            } else {
-                // Use spline tangents
-                computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
-            }
-
-            return;
-        } else if (tangentType == TAN_PLATEAU) {
-            if (!(keysValid[0] && keysValid[2])) {
-                // Reduces to the flat case at the ends of the animation curve
-                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
-                return;
-            }
-
-            // Otherwise, figure out whether we have any local extremum
-            if ((keyValues[1] > keyValues[0] &&
-                    keyValues[1] > keyValues[2]) ||
-                    (keyValues[1] < keyValues[0] &&
-                            keyValues[1] < keyValues[2])) {
-                // Use flat tangent
-                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
-            } else {
-                // The rule is that we use spline tangents unless
-                // doing so would cause the curve to go outside the
-                // envelope of the keyvalues. To figure this out, we
-                // have to compute both the in and out tangents as
-                // though we were using splines, and see whether the
-                // intermediate bezier control points go outside the
-                // hull.
-                //
-                // Note that it doesn't matter whether we compute the
-                // "in" or "out" tangent at the current point -- the
-                // result is the same.
-                computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
-
-                // Compute the values from the keyframe along the
-                // tangent 1/3 of the way to the previous and next
-                // keyframes
-                float tangent = computedTangent[1] / (computedTangent[0] * FPS);
-                float prev13 = keyValues[1] - tangent * ((keyTimes[1] - keyTimes[0]) / 3.0f);
-                float next13 = keyValues[1] + tangent * ((keyTimes[2] - keyTimes[1]) / 3.0f);
-
-                if (isBetween(prev13, keyValues[0], keyValues[2]) &&
-                        isBetween(next13, keyValues[0], keyValues[2])) {
-                } else {
-                    // Use flat tangent
-                    computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
-                }
-            }
-
-            return;
-        }
-
-        // Perform normalization
-        // NOTE the scaling of the X coordinate -- this is needed to match Maya's math
-        output[0] /= FPS;
-        float len = (float) Math.sqrt(
-                output[0] * output[0] +
-                        output[1] * output[1]);
-        if (len != 0.0f) {
-            output[0] /= len;
-            output[1] /= len;
-        }
-        // println("TAN LINEAR {output[0]} {output[1]}");
-    }
-
-    //=========================================================================
-    // Loader.isBetween
-    //=========================================================================
-    boolean isBetween(
-            float value,
-            float v1,
-            float v2) {
-        return ((v1 <= value && value <= v2) ||
-                (v1 >= value && value >= v2));
-    }
-
-
-    static class VertexHash {
-        private int vertexIndex;
-        private int normalIndex;
-        private int[] uvIndices;
-
-        VertexHash(
-                int vertexIndex,
-                int normalIndex,
-                int[] uvIndices) {
-            this.vertexIndex = vertexIndex;
-            this.normalIndex = normalIndex;
-            if (uvIndices != null) {
-                this.uvIndices = (int[]) uvIndices.clone();
-            }
-        }
-
-        @Override
-        public int hashCode() {
-            int code = vertexIndex;
-            code *= 17;
-            code += normalIndex;
-            if (uvIndices != null) {
-                for (int i = 0; i < uvIndices.length; i++) {
-                    code *= 17;
-                    code += uvIndices[i];
-                }
-            }
-            return code;
-        }
-
-        @Override
-        public boolean equals(Object arg) {
-            if (arg == null || !(arg instanceof VertexHash)) {
-                return false;
-            }
-
-            VertexHash other = (VertexHash) arg;
-            if (vertexIndex != other.vertexIndex) {
-                return false;
-            }
-            if (normalIndex != other.normalIndex) {
-                return false;
-            }
-            if ((uvIndices != null) != (other.uvIndices != null)) {
-                return false;
-            }
-            if (uvIndices != null) {
-                if (uvIndices.length != other.uvIndices.length) {
-                    return false;
-                }
-                for (int i = 0; i < uvIndices.length; i++) {
-                    if (uvIndices[i] != other.uvIndices[i]) {
-                        return false;
-                    }
-                }
-            }
-            return true;
-        }
-    }
-
-    private Object buildMeshData(List<MPolyFace.FaceData> faces, MFloat3Array normals) {
-        // Setup vertexes
-        float[] verts = mVerts.get();
-        float[] tweaks = null;
-        if (mPointTweaks != null) {
-            tweaks = mPointTweaks.get();
-        }
-        float[] points = new float[verts.length];
-        for (int index = 0; index < verts.length; index += 3) {
-            if (tweaks != null && tweaks.length > index + 2) {
-                points[index] = verts[index] + tweaks[index];
-                points[index + 1] = verts[index + 1] + tweaks[index + 1];
-                points[index + 2] = verts[index + 2] + tweaks[index + 2];
-            } else {
-                points[index] = verts[index];
-                points[index + 1] = verts[index + 1];
-                points[index + 2] = verts[index + 2];
-            }
-        }
-
-        // copy UV as-is (if any)
-        float[] texCoords = getTexCoords(uvChannel);
-
-        if (asPolygonMesh) {
-            List<int[]> ff = new ArrayList<int[]>();
-            for (int f = 0; f < faces.size(); f++) {
-                MPolyFace.FaceData faceData = faces.get(f);
-                int[] faceEdges = faceData.getFaceEdges();
-                int[][] uvData = faceData.getUVData();
-                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
-                if (faceEdges != null && faceEdges.length > 0) {
-                    int[] polyFace = new int[faceEdges.length * 2];
-                    for (int i = 0; i < faceEdges.length; i++) {
-                        int vIndex = edgeStart(faceEdges[i]);
-                        int uvIndex = uvIndices == null ? 0 : uvIndices[i];
-                        polyFace[i*2] = vIndex;
-                        polyFace[i*2+1] = uvIndex;
-                    }
-                    ff.add(polyFace);
-                }
-            }
-            int[][] facesArray = ff.toArray(new int[ff.size()][]);
-            
-            int[][] faceNormals = new int[facesArray.length][];
-            int normalInd = 0;
-            for (int f = 0; f < faceNormals.length; f++) {
-                faceNormals[f] = new int[facesArray[f].length/2];
-                for (int e = 0; e < faceNormals[f].length; e++) {
-                    faceNormals[f][e] = normalInd++;
-                }
-            }
-            int[] smGroups;
-            // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
-            if (normalInd == normals.getSize()) {
-                smGroups = SmoothingGroups.calcSmoothGroups(facesArray, faceNormals, normals.get());
-            } else {
-                smGroups = new int[facesArray.length];
-                Arrays.fill(smGroups, 1);
-            }
-
-            PolygonMesh mesh = new PolygonMesh();
-            mesh.getPoints().setAll(points);
-            mesh.getTexCoords().setAll(texCoords);
-            mesh.faces = facesArray;
-            mesh.getFaceSmoothingGroups().setAll(smGroups);
-            return mesh;
-        } else {
-            // Split the polygonal faces into triangle faces
-            List<Integer> ff = new ArrayList<Integer>();
-            List<Integer> nn = new ArrayList<Integer>();
-            int nIndex = 0;
-            
-            for (int f = 0; f < faces.size(); f++) {
-                MPolyFace.FaceData faceData = faces.get(f);
-                int[] faceEdges = faceData.getFaceEdges();
-                int[][] uvData = faceData.getUVData();
-                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
-                if (faceEdges != null && faceEdges.length > 0) {
-
-                    // Generate triangle fan about the first vertex
-                    int vIndex0 = edgeStart(faceEdges[0]);
-                    int uvIndex0 = uvIndices == null ? 0 : uvIndices[0];
-                    int nIndex0 = nIndex++;
-
-                    int vIndex1 = edgeStart(faceEdges[1]);
-                    int uvIndex1 = uvIndices == null ? 0 : uvIndices[1];
-                    int nIndex1 = nIndex++;
-
-                    for (int i = 2; i < faceEdges.length; i++) {
-                        int vIndex2 = edgeStart(faceEdges[i]);
-                        int uvIndex2 = uvIndices == null ? 0 : uvIndices[i];
-                        int nIndex2 = nIndex++;
-
-                        ff.add(vIndex0);
-                        ff.add(uvIndex0);
-                        ff.add(vIndex1);
-                        ff.add(uvIndex1);
-                        ff.add(vIndex2);
-                        ff.add(uvIndex2);
-                        nn.add(nIndex0);
-                        nn.add(nIndex1);
-                        nn.add(nIndex2);
-
-                        vIndex1 = vIndex2;
-                        uvIndex1 = uvIndex2;
-                    }
-                }
-            }
-            int[] fff = new int[ff.size()];
-            for (int i = 0; i < fff.length; i++) {
-                fff[i] = ff.get(i);
-            }
-            
-            int[] smGroups;
-            // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
-            if (nIndex == normals.getSize()) {
-                int[] faceNormals = new int[nn.size()];
-                for (int i = 0; i < faceNormals.length; i++) {
-                    faceNormals[i] = nn.get(i);
-                }
-                smGroups = SmoothingGroups.calcSmoothGroups(fff, faceNormals, normals.get());
-            } else {
-                smGroups = new int[fff.length];
-                Arrays.fill(smGroups, 1);
-            }
-            
-            TriangleMesh mesh = new TriangleMesh();
-            mesh.getPoints().setAll(points);
-            mesh.getTexCoords().setAll(texCoords);
-            mesh.getFaces().setAll(fff);
-            mesh.getFaceSmoothingGroups().setAll(smGroups);
-            return mesh;
-        }
-    }
-
-    MNode resolveOutputMesh(MNode n) {
-        MNode og;
-        List<MPath> ogc0 = n.getPathsConnectingFrom("og[0]");
-        if (ogc0.size() > 0) {
-            og = ogc0.get(0).getTargetNode();
-        } else {
-            ogc0 = n.getPathsConnectingFrom("og");
-            if (ogc0.size() > 0) {
-                og = ogc0.get(0).getTargetNode();
-            } else {
-                return null;
-            }
-        }
-        if (og.isInstanceOf(meshType)) {
-            return og;
-        }
-        // println("r.OG={og}");
-        while (og.isInstanceOf(groupPartsType)) {
-            og = og.getPathsConnectingFrom("og").get(0).getTargetNode();
-        }
-        if (og.isInstanceOf(meshType)) {
-            return og;
-        }
-        // println("r1.OG={og}");
-        if (og == null) {
-            return null;
-        }
-        return resolveOutputMesh(og);
-    }
-
-    MNode resolveInputMesh(MNode n) {
-        return resolveInputMesh(n, true);
-    }
-
-    MNode resolveInputMesh(MNode n, boolean followBlend) {
-        MNode groupParts;
-        if (!n.isInstanceOf(groupPartsType)) {
-            groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
-        } else {
-            groupParts = n;
-        }
-        MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
-        if (origMesh == null) {
-            MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
-            groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
-            origMesh =
-                    groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
-        }
-        // println("N={n} ORIG_MESH={origMesh}");
-        if (origMesh == null) {
-            return null;
-        }
-        if (origMesh.isInstanceOf(meshType)) {
-            return origMesh;
-        }
-        if (origMesh.isInstanceOf(blendShapeType)) {
-            // return the blend shape's output
-            return resolveOutputMesh(origMesh);
-        }
-        return resolveInputMesh(origMesh);
-    }
-
-    MNode resolveOrigInputMesh(MNode n) {
-
-        MNode groupParts;
-        if (!n.isInstanceOf(groupPartsType)) {
-            groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
-        } else {
-            groupParts = n;
-        }
-        MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
-        if (origMesh == null) {
-            MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
-            groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
-            origMesh =
-                    groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
-        }
-        if (origMesh == null) {
-            return null;
-        }
-        // println("N={n} ORIG_MESH={origMesh}");
-        if (origMesh.isInstanceOf(meshType)) {
-            return origMesh;
-        }
-        return resolveOrigInputMesh(origMesh);
-    }
-
-    Affine convertMatrix(MFloatArray mayaMatrix) {
-        if (mayaMatrix == null || mayaMatrix.getSize() < 16) {
-            return new Affine();
-        }
-
-        Affine result = new Affine();
-        result.setMxx(mayaMatrix.get(0 * 4 + 0));
-        result.setMxy(mayaMatrix.get(1 * 4 + 0));
-        result.setMxz(mayaMatrix.get(2 * 4 + 0));
-        result.setMyx(mayaMatrix.get(0 * 4 + 1));
-        result.setMyy(mayaMatrix.get(1 * 4 + 1));
-        result.setMyz(mayaMatrix.get(2 * 4 + 1));
-        result.setMzx(mayaMatrix.get(0 * 4 + 2));
-        result.setMzy(mayaMatrix.get(1 * 4 + 2));
-        result.setMzz(mayaMatrix.get(2 * 4 + 2));
-        result.setTx(mayaMatrix.get(3 * 4 + 0));
-        result.setTy(mayaMatrix.get(3 * 4 + 1));
-        result.setTz(mayaMatrix.get(3 * 4 + 2));
-        return result;
-    }
-
-}
+package com.javafx.experiments.importers.maya;
+
+import com.javafx.experiments.importers.SmoothingGroups;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.beans.property.DoubleProperty;
+import javafx.scene.DepthTest;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.CullFace;
+import javafx.scene.shape.Mesh;
+import javafx.scene.shape.MeshView;
+import javafx.scene.shape.TriangleMesh;
+import javafx.scene.transform.Affine;
+import javafx.util.Duration;
+import com.javafx.experiments.importers.maya.parser.MParser;
+import com.javafx.experiments.importers.maya.values.MArray;
+import com.javafx.experiments.importers.maya.values.MBool;
+import com.javafx.experiments.importers.maya.values.MCompound;
+import com.javafx.experiments.importers.maya.values.MData;
+import com.javafx.experiments.importers.maya.values.MFloat;
+import com.javafx.experiments.importers.maya.values.MFloat2Array;
+import com.javafx.experiments.importers.maya.values.MFloat3;
+import com.javafx.experiments.importers.maya.values.MFloat3Array;
+import com.javafx.experiments.importers.maya.values.MFloatArray;
+import com.javafx.experiments.importers.maya.values.MInt;
+import com.javafx.experiments.importers.maya.values.MInt3Array;
+import com.javafx.experiments.importers.maya.values.MIntArray;
+import com.javafx.experiments.importers.maya.values.MPolyFace;
+import com.javafx.experiments.importers.maya.values.MString;
+import com.javafx.experiments.shape3d.PolygonMesh;
+import com.javafx.experiments.shape3d.PolygonMeshView;
+import com.javafx.experiments.shape3d.SkinningMesh;
+import com.sun.javafx.geom.Vec3f;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Arrays;
+import javafx.animation.AnimationTimer;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+
+/** Loader */
+class Loader {
+    public static final boolean DEBUG = false;
+    public static final boolean WARN = false;
+
+    MEnv env;
+
+    int startFrame;
+    int endFrame;
+
+    MNodeType transformType;
+    MNodeType jointType;
+    MNodeType meshType;
+    MNodeType cameraType;
+    MNodeType animCurve;
+    MNodeType animCurveTA;
+    MNodeType animCurveUA;
+    MNodeType animCurveUL;
+    MNodeType animCurveUT;
+    MNodeType animCurveUU;
+
+    MNodeType lambertType;
+    MNodeType reflectType;
+    MNodeType blinnType;
+    MNodeType phongType;
+    MNodeType fileType;
+    MNodeType skinClusterType;
+    MNodeType blendShapeType;
+    MNodeType groupPartsType;
+    MNodeType shadingEngineType;
+
+    // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
+    // Joint rootJoint; //NO_JOINTS
+    Map<MNode, Node> loaded = new HashMap<MNode, Node>();
+
+    Map<Float, List<KeyValue>> keyFrameMap = new TreeMap();
+
+    Map<Node, MNode> meshParents = new HashMap();
+
+    private MFloat3Array mVerts;
+    // Optionally force per-face per-vertex normal generation
+    private int[] edgeData;
+
+    private List<MData> uvSet;
+    private int uvChannel;
+    private MFloat3Array mPointTweaks;
+    private URL url;
+    private boolean asPolygonMesh;
+
+    //=========================================================================
+    // Loader.load
+    //-------------------------------------------------------------------------
+    // Called from MayaImporter.load
+    //=========================================================================
+    public void load(URL url, boolean asPolygonMesh) {
+        this.url = url;
+        this.asPolygonMesh = asPolygonMesh;
+        env = new MEnv();
+        MParser parser = new MParser(env);
+        try {
+            parser.parse(url);
+            loadModel();
+            for (MNode n : env.getNodes()) {
+                // System.out.println("____________________________________________________________");
+                // System.out.println("==> .......Node: " + n);
+                resolveNode(n);
+            }
+        } catch (Exception e) {
+            if (WARN) System.err.println("Error loading url: [" + url + "]");
+            throw new RuntimeException(e);
+        }
+    }
+
+    //=========================================================================
+    // Loader.loadModel
+    //=========================================================================
+    void loadModel() {
+        startFrame = (int) Math.round(env.getPlaybackStart() - 1);
+        endFrame = (int) Math.round(env.getPlaybackEnd() - 1);
+        transformType = env.findNodeType("transform");
+        jointType = env.findNodeType("joint");
+        meshType = env.findNodeType("mesh");
+        cameraType = env.findNodeType("camera");
+        animCurve = env.findNodeType("animCurve");
+        animCurveTA = env.findNodeType("animCurveTA");
+        animCurveUA = env.findNodeType("animCurveUA");
+        animCurveUL = env.findNodeType("animCurveUL");
+        animCurveUT = env.findNodeType("animCurveUT");
+        animCurveUU = env.findNodeType("animCurveUU");
+
+        lambertType = env.findNodeType("lambert");
+        reflectType = env.findNodeType("reflect");
+        blinnType = env.findNodeType("blinn");
+        phongType = env.findNodeType("phong");
+        fileType = env.findNodeType("file");
+        skinClusterType = env.findNodeType("skinCluster");
+        groupPartsType = env.findNodeType("groupParts");
+        shadingEngineType = env.findNodeType("shadingEngine");
+        blendShapeType = env.findNodeType("blendShape");
+    }
+
+    //=========================================================================
+    // Loader.resolveNode
+    //-------------------------------------------------------------------------
+    // Loader.resolveNode looks up MNode in the HashMap Map<MNode, Node> loaded
+    // and returns the Node to which this map maps the MNode.
+    // Also, if the node that its looking up hasn't been processed yet,
+    // it processes the node.
+    //=========================================================================
+    Node resolveNode(MNode n) {
+        // System.out.println("--> resolveNode: " + n);
+        // if the node hasn't already been processed, then process the node
+        if (!loaded.containsKey(n)) {
+            // System.out.println("--> containsKey: " + n);
+            processNode(n);
+            // System.out.println("    loaded.get(n) " + loaded.get(n));
+        }
+        return loaded.get(n);
+    }
+
+    //=========================================================================
+    // Loader.processNode
+    //=========================================================================
+    void processNode(MNode n) {
+        Group parentNode = null;
+        for (MNode p : n.getParentNodes()) {
+            parentNode = (Group) resolveNode(p);
+        }
+        Node result = loaded.get(n);
+        // if the result is null, then it hasn't been added to the map yet
+        // so go ahead and process it
+        if (result == null) {
+            if (n.isInstanceOf(shadingEngineType)) {
+                //                System.out.println("==> Found a node of shadingEngineType: " + n);
+            } else if (n.isInstanceOf(lambertType)) {
+                //                System.out.println("==> Found a node of lambertType: " + n);
+            } else if (n.isInstanceOf(reflectType)) {
+                //                System.out.println("==> Found a node of reflectType: " + n);
+            } else if (n.isInstanceOf(blinnType)) {
+                //                System.out.println("==> Found a node of blinnType: " + n);
+            } else if (n.isInstanceOf(phongType)) {
+                //                System.out.println("==> Found a node of phongType: " + n);
+            } else if (n.isInstanceOf(fileType)) {
+                //                System.out.println("==> Found a node of fileType: " + n);
+            } else if (n.isInstanceOf(skinClusterType)) {
+                processClusterType(n);
+            } else if (n.isInstanceOf(meshType)) {
+                processMeshType(n, parentNode);
+            } else if (n.isInstanceOf(jointType)) {
+                processJointType(n, parentNode);
+            } else if (n.isInstanceOf(transformType)) {
+                processTransformType(n, parentNode);
+            } else if (n.isInstanceOf(animCurve)) {
+                processAnimCurve(n);
+            }
+        }
+    }
+
+    protected void processClusterType(MNode n) {
+        loaded.put(n, null);
+        MArray ma = (MArray) n.getAttr("ma");
+
+        List<Joint> jointNodes = new ArrayList<Joint>();
+        Set<Parent> jointForest = new HashSet<Parent>(); // root's children that have joints in their trees
+        for (int i = 0; i < ma.getSize(); i++) {
+            // hack... ?
+            MNode c = n.getIncomingConnectionToType("ma[" + i + "]", "joint");
+            Joint jn = (Joint) resolveNode(c);
+            jointNodes.add(jn);
+            
+            Parent rootChild = jn; // root's child, which is an ancestor of joint jn
+            while (rootChild.getParent() != null) {
+                rootChild = rootChild.getParent();
+            }
+            jointForest.add(rootChild);
+        }
+        
+        MNode outputMeshMNode = resolveOutputMesh(n);
+        MNode inputMeshMNode = resolveInputMesh(n);
+        if (inputMeshMNode == null || outputMeshMNode == null) {
+            return;
+        }
+        // We must be able to find the original converter in the meshConverters map
+        MNode origOrigMesh = resolveOrigInputMesh(n);
+        //               println("ORIG ORIG={origOrigMesh}");
+        
+        // TODO: What is with this? origMesh
+        resolveNode(origOrigMesh).setVisible(false);
+
+        MArray bindPreMatrixArray = (MArray) n.getAttr("pm");
+        Affine bindGlobalMatrix = convertMatrix((MFloatArray) n.getAttr("gm"));
+
+        Affine[] bindPreMatrix = new Affine[bindPreMatrixArray.getSize()];
+        for (int i = 0; i < bindPreMatrixArray.getSize(); i++) {
+            bindPreMatrix[i] = convertMatrix((MFloatArray) bindPreMatrixArray.getData(i));
+        }
+
+        MArray mayaWeights = (MArray) n.getAttr("wl");
+        float[][] weights = new float [jointNodes.size()][mayaWeights.getSize()];
+        for (int i=0; i<mayaWeights.getSize(); i++) {
+            MFloatArray curWeights = (MFloatArray) mayaWeights.getData(i).getData("w");
+            for (int j = 0; j < jointNodes.size(); j++) {
+                weights[j][i] = j < curWeights.getSize() ? curWeights.get(j) : 0;
+            }
+        }
+        
+        Node sourceMayaMeshNode = resolveNode(inputMeshMNode);
+        Node targetMayaMeshNode = resolveNode(outputMeshMNode);
+        
+        if (sourceMayaMeshNode.getClass().equals(PolygonMeshView.class)) {
+            PolygonMeshView sourceMayaMeshView = (PolygonMeshView) sourceMayaMeshNode;
+            PolygonMeshView targetMayaMeshView = (PolygonMeshView) targetMayaMeshNode;
+            
+            PolygonMesh sourceMesh = (PolygonMesh) sourceMayaMeshView.getMesh();
+            SkinningMesh targetMesh = new SkinningMesh(sourceMesh, weights, bindPreMatrix, bindGlobalMatrix, jointNodes, new ArrayList(jointForest));
+            targetMayaMeshView.setMesh(targetMesh);
+
+            final SkinningMeshTimer skinningMeshTimer = new SkinningMeshTimer(targetMesh);
+            if (targetMayaMeshNode.getScene() != null) {
+                skinningMeshTimer.start();
+            }
+            targetMayaMeshView.sceneProperty().addListener(new ChangeListener<Scene>() {
+                @Override
+                public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
+                    if (newValue == null) {
+                        skinningMeshTimer.stop();
+                    } else {
+                        skinningMeshTimer.start();
+                    }
+                }
+            });
+        } else {
+            Logger.getLogger(MayaImporter.class.getName()).log(Level.INFO, "Mesh skinning is not supported for triangle meshes. Select the 'Load as Polygons' option to load the mesh as polygon mesh.");
+            MeshView sourceMayaMeshView = (MeshView) sourceMayaMeshNode;
+            MeshView targetMayaMeshView = (MeshView) targetMayaMeshNode;
+            TriangleMesh sourceMesh = (TriangleMesh) sourceMayaMeshView.getMesh();
+            TriangleMesh targetMesh = (TriangleMesh) targetMayaMeshView.getMesh();
+            targetMesh.getPoints().setAll(sourceMesh.getPoints());
+            targetMesh.getTexCoords().setAll(sourceMesh.getTexCoords());
+            targetMesh.getFaces().setAll(sourceMesh.getFaces());
+            targetMesh.getFaceSmoothingGroups().setAll(sourceMesh.getFaceSmoothingGroups());
+        }
+    }
+    
+    private class SkinningMeshTimer extends AnimationTimer {
+        private SkinningMesh mesh;
+        SkinningMeshTimer(SkinningMesh mesh) {
+            this.mesh = mesh;
+        }
+        @Override
+        public void handle(long l) {
+            mesh.update();
+        }
+    }
+
+    protected Image loadImageFromFtnAttr(MNode fileNode, String name) {
+        Image image = null;
+        MString fileName = (MString) fileNode.getAttr("ftn");
+        String imageFilename = (String) fileName.get();
+        try {
+            File file = new File(imageFilename);
+            String filePath;
+            if (file.exists()) {
+                filePath = file.toURI().toString();
+            } else {
+                filePath = new URL(url, imageFilename).toString();
+            }
+            image = new Image(filePath);
+            if (DEBUG) {
+                System.out.println(name + " = " + filePath);
+                System.out.println(name + " w = " + image.getWidth() + " h = " + image.getHeight());
+            }
+        } catch (MalformedURLException ex) {
+            Logger.getLogger(MayaImporter.class.getName()).log(Level.SEVERE, "Failed to load " + name + " '" + imageFilename + "'!", ex);
+        }
+        return image;
+    }
+
+    protected void processMeshType(MNode n, Group parentNode) throws RuntimeException {
+        //=============================================================
+        // When JavaFX supports polygon mesh geometry,
+        // add the polygon mesh geometry here.
+        // Until then, add a unit square as a placeholder.
+        //=============================================================
+        Node node = resolveNode(n.getParentNodes().get(0));
+        //                if (node != null) {
+        //                if (node != null && !n.getName().endsWith("Orig")) {
+        // Original approach to mesh placeholder:
+        //                     meshParents.put(node, n);
+
+        // Try to find an image or color from n (MNode)
+        if (DEBUG) { System.out.println("________________________________________"); }
+        if (DEBUG) { System.out.println("n.getName(): " + n.getName()); }
+        if (DEBUG) { System.out.println("n.getNodeType(): " + n.getNodeType()); }
+        MNode shadingGroup = n.getOutgoingConnectionToType("iog", "shadingEngine", true);
+        MNode mat;
+        MNode mFile;
+        if (DEBUG) { System.out.println("shadingGroup: " + shadingGroup); }
+
+        MFloat3 mColor;
+        Vec3f diffuseColor = null;
+        Vec3f specularColor = null;
+
+        Image diffuseImage = null;
+        Image normalImage = null;
+        Image specularImage = null;
+        Float specularPower = null;
+
+        if (shadingGroup != null) {
+            mat = shadingGroup.getIncomingConnectionToType("ss", "lambert");
+            if (mat != null) {
+                // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
+                if (DEBUG) { System.out.println("lambert mat: " + mat); }
+                mColor = (MFloat3) mat.getAttr("c");
+                float diffuseIntensity = ((MFloat) mat.getAttr("dc")).get();
+                if (mColor != null) {
+                    diffuseColor = new Vec3f(
+                            mColor.get()[0] * diffuseIntensity,
+                            mColor.get()[1] * diffuseIntensity,
+                            mColor.get()[2] * diffuseIntensity);
+                    if (DEBUG) { System.out.println("diffuseColor = " + diffuseColor); }
+                }
+
+                mFile = mat.getIncomingConnectionToType("c", "file");
+                if (mFile != null) {
+                    diffuseImage = loadImageFromFtnAttr(mFile, "diffuseImage");
+                }
+                MNode bump2d = mat.getIncomingConnectionToType("n", "bump2d");
+                if (bump2d != null) {
+                    mFile = bump2d.getIncomingConnectionToType("bv", "file");
+                    if (mFile != null) {
+                        normalImage = loadImageFromFtnAttr(mFile, "normalImage");
+                    }
+                }
+            }
+            mat = shadingGroup.getIncomingConnectionToType("ss", "phong");
+            if (mat != null) {
+                // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
+                if (DEBUG) { System.out.println("phong mat: " + mat); }
+                mColor = (MFloat3) mat.getAttr("sc");
+                if (mColor != null) {
+                    specularColor = new Vec3f(
+                            mColor.get()[0],
+                            mColor.get()[1],
+                            mColor.get()[2]);
+                    if (DEBUG) { System.out.println("specularColor = " + specularColor); }
+                }
+                mFile = mat.getIncomingConnectionToType("sc", "file");
+                if (mFile != null) {
+                    specularImage = loadImageFromFtnAttr(mFile, "specularImage");
+                }
+
+                specularPower = ((MFloat) mat.getAttr("cp")).get();
+                if (DEBUG) { System.out.println("specularPower = " + specularPower); }
+            }
+        }
+
+        PhongMaterial material = new PhongMaterial();
+
+        if (diffuseImage != null) {
+            material.setDiffuseMap(diffuseImage);
+            material.setDiffuseColor(Color.WHITE);
+        } else {
+            if (diffuseColor != null) {
+                material.setDiffuseColor(
+                        new Color(
+                                diffuseColor.x,
+                                diffuseColor.y,
+                                diffuseColor.z, 1));
+                //                            material.setDiffuseColor(new Color(
+                //                                    0.5,
+                //                                    0.5,
+                //                                    0.5, 0));
+            } else {
+                material.setDiffuseColor(Color.GRAY);
+            }
+        }
+
+        if (normalImage != null) {
+            material.setBumpMap(normalImage);
+        }
+
+        if (specularImage != null) {
+            material.setSpecularMap(specularImage);
+        } else {
+            if (specularColor != null && specularPower != null) {
+                material.setSpecularColor(
+                        new Color(
+                                specularColor.x,
+                                specularColor.y,
+                                specularColor.z, 1));
+                material.setSpecularPower(specularPower / 33);
+                //                            material.setSpecularColor(new Color(
+                //                                    0,
+                //                                    1,
+                //                                    0, 1));
+                //                            material.setSpecularPower(1);
+            } else {
+                //                            material.setSpecularColor(new Color(
+                //                                    0.2,
+                //                                    0.2,
+                //                                    0.2, 1));
+                //                            material.setSpecularPower(1);
+                material.setSpecularColor(null);
+            }
+        }
+
+        Object mesh = convertToFXMesh(n);
+
+        if (asPolygonMesh) {
+            PolygonMeshView mv = new PolygonMeshView();
+            mv.setId(n.getName());
+            mv.setMaterial(material);
+            mv.setMesh((PolygonMesh) mesh);
+//            mv.setCullFace(CullFace.NONE); //TODO
+            loaded.put(n, mv);
+            if (node != null) {
+                ((Group) node).getChildren().add(mv);
+            }
+        } else {
+            MeshView mv = new MeshView();
+            mv.setId(n.getName());
+            mv.setMaterial(material);
+
+//            // TODO HACK for [JIRA] (RT-30449) FX 8 3D: Need to handle mirror transformation (flip culling);
+//            mv.setCullFace(CullFace.FRONT);
+
+            mv.setMesh((TriangleMesh) mesh);
+
+            loaded.put(n, mv);
+            if (node != null) {
+                ((Group) node).getChildren().add(mv);
+            }
+        }
+    }
+    
+    protected void processJointType(MNode n, Group parentNode) {
+        // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
+        Node result;
+        MFloat3 t = (MFloat3) n.getAttr("t");
+        MFloat3 jo = (MFloat3) n.getAttr("jo");
+        MFloat3 r = (MFloat3) n.getAttr("r");
+        MFloat3 s = (MFloat3) n.getAttr("s");
+        String id = n.getName();
+
+        Joint j = new Joint();
+        j.setId(id);
+
+        // There's various ways to get the same thing:
+        // n.getAttr("r").get()[0]
+        // n.getAttr("r").getX()
+        // n.getAttr("rx")
+        // Up to you which you prefer
+
+        j.t.setX(t.get()[0]);
+        j.t.setY(t.get()[1]);
+        j.t.setZ(t.get()[2]);
+
+        // if ssc (Segment Scale Compensate) is false, then it is = 1, 1, 1
+        boolean ssc = ((MBool) n.getAttr("ssc")).get();
+        if (ssc) {
+            List<MNode> parents = n.getParentNodes();
+            if (parents.size() > 0) {
+                MFloat3 parent_s = (MFloat3) n.getParentNodes().get(0).getAttr("s");
+                j.is.setX(1f / parent_s.getX());
+                j.is.setY(1f / parent_s.getY());
+                j.is.setZ(1f / parent_s.getZ());
+            } else {
+                j.is.setX(1f);
+                j.is.setY(1f);
+                j.is.setZ(1f);
+            }
+        } else {
+            j.is.setX(1f);
+            j.is.setY(1f);
+            j.is.setZ(1f);
+        }
+
+        /*
+        // This code doesn't seem to work right:
+        MFloat jox = (MFloat) n.getAttr("jox");
+        MFloat joy = (MFloat) n.getAttr("joy");
+        MFloat joz = (MFloat) n.getAttr("joz");
+        j.jox.setAngle(jox.get());
+        j.joy.setAngle(joy.get());
+        j.joz.setAngle(joz.get());
+        // The following code works right:
+        */
+
+        if (jo != null) {
+            j.jox.setAngle(jo.getX());
+            j.joy.setAngle(jo.getY());
+            j.joz.setAngle(jo.getZ());
+        } else {
+            j.jox.setAngle(0f);
+            j.joy.setAngle(0f);
+            j.joz.setAngle(0f);
+        }
+
+        MFloat rx = (MFloat) n.getAttr("rx");
+        MFloat ry = (MFloat) n.getAttr("ry");
+        MFloat rz = (MFloat) n.getAttr("rz");
+        j.rx.setAngle(rx.get());
+        j.ry.setAngle(ry.get());
+        j.rz.setAngle(rz.get());
+
+        j.s.setX(s.get()[0]);
+        j.s.setY(s.get()[1]);
+        j.s.setZ(s.get()[2]);
+
+        result = j;
+        // Add the Joint to the map
+        loaded.put(n, j);
+        j.setDepthTest(DepthTest.ENABLE);
+        // Add the Joint to its JavaFX parent
+        if (parentNode != null) {
+            parentNode.getChildren().add(j);
+            if (DEBUG) System.out.println("j.getDepthTest() : " + j.getDepthTest());
+        }
+        if (parentNode == null || !(parentNode instanceof Joint)) {
+            // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
+            // rootJoint = j;
+        }
+    }
+
+    protected void processTransformType(MNode n, Group parentNode) {
+        MFloat3 t = (MFloat3) n.getAttr("t");
+        MFloat3 r = (MFloat3) n.getAttr("r");
+        MFloat3 s = (MFloat3) n.getAttr("s");
+        String id = n.getName();
+        // ignore cameras
+        if ("persp".equals(id) ||
+                "top".equals(id) ||
+                "front".equals(id) ||
+                "side".equals(id)) {
+            return;
+        }
+
+        MayaGroup mGroup = new MayaGroup();
+        mGroup.setId(n.getName());
+        // g.setBlendMode(BlendMode.SRC_OVER);
+
+        // if (DEBUG) System.out.println("t = " + t);
+        // if (DEBUG) System.out.println("r = " + r);
+        // if (DEBUG) System.out.println("s = " + s);
+
+        mGroup.t.setX(t.get()[0]);
+        mGroup.t.setY(t.get()[1]);
+        mGroup.t.setZ(t.get()[2]);
+
+        MFloat rx = (MFloat) n.getAttr("rx");
+        MFloat ry = (MFloat) n.getAttr("ry");
+        MFloat rz = (MFloat) n.getAttr("rz");
+        mGroup.rx.setAngle(rx.get());
+        mGroup.ry.setAngle(ry.get());
+        mGroup.rz.setAngle(rz.get());
+
+        mGroup.s.setX(s.get()[0]);
+        mGroup.s.setY(s.get()[1]);
+        mGroup.s.setZ(s.get()[2]);
+
+        MFloat rptx = (MFloat) n.getAttr("rptx");
+        MFloat rpty = (MFloat) n.getAttr("rpty");
+        MFloat rptz = (MFloat) n.getAttr("rptz");
+        mGroup.rpt.setX(rptx.get());
+        mGroup.rpt.setY(rpty.get());
+        mGroup.rpt.setZ(rptz.get());
+
+        MFloat rpx = (MFloat) n.getAttr("rpx");
+        MFloat rpy = (MFloat) n.getAttr("rpy");
+        MFloat rpz = (MFloat) n.getAttr("rpz");
+        mGroup.rp.setX(rpx.get());
+        mGroup.rp.setY(rpy.get());
+        mGroup.rp.setZ(rpz.get());
+
+        mGroup.rpi.setX(-rpx.get());
+        mGroup.rpi.setY(-rpy.get());
+        mGroup.rpi.setZ(-rpz.get());
+
+        MFloat sptx = (MFloat) n.getAttr("sptx");
+        MFloat spty = (MFloat) n.getAttr("spty");
+        MFloat sptz = (MFloat) n.getAttr("sptz");
+        mGroup.spt.setX(sptx.get());
+        mGroup.spt.setY(spty.get());
+        mGroup.spt.setZ(sptz.get());
+
+        MFloat spx = (MFloat) n.getAttr("spx");
+        MFloat spy = (MFloat) n.getAttr("spy");
+        MFloat spz = (MFloat) n.getAttr("spz");
+        mGroup.sp.setX(spx.get());
+        mGroup.sp.setY(spy.get());
+        mGroup.sp.setZ(spz.get());
+
+        mGroup.spi.setX(-spx.get());
+        mGroup.spi.setY(-spy.get());
+        mGroup.spi.setZ(-spz.get());
+
+        // Add the MayaGroup to the map
+        loaded.put(n, mGroup);
+        // Add the MayaGroup to its JavaFX parent
+        if (parentNode != null) {
+            parentNode.getChildren().add(mGroup);
+        }
+    }
+
+    protected void processAnimCurve(MNode n) {
+        // if (DEBUG) System.out.println("processing anim curve");
+        List<MPath> toPaths = n.getPathsConnectingFrom("o");
+        loaded.put(n, null);
+        for (MPath path : toPaths) {
+            MNode toNode = path.getTargetNode();
+            // if (DEBUG) System.out.println("toNode = "+ toNode.getNodeType());
+            if (toNode.isInstanceOf(transformType)) {
+                Node to = resolveNode(toNode);
+                if (to instanceof MayaGroup) {
+                    MayaGroup g = (MayaGroup) to;
+                    DoubleProperty ref = null;
+                    String s = path.getComponentSelector();
+                    // if (DEBUG) System.out.println("selector = " + s);
+                    if ("t[0]".equals(s)) {
+                        ref = g.t.xProperty();
+                    } else if ("t[1]".equals(s)) {
+                        ref = g.t.yProperty();
+                    } else if ("t[2]".equals(s)) {
+                        ref = g.t.zProperty();
+                    } else if ("s[0]".equals(s)) {
+                        ref = g.s.xProperty();
+                    } else if ("s[1]".equals(s)) {
+                        ref = g.s.yProperty();
+                    } else if ("s[2]".equals(s)) {
+                        ref = g.s.zProperty();
+                    } else if ("r[0]".equals(s)) {
+                        ref = g.rx.angleProperty();
+                    } else if ("r[1]".equals(s)) {
+                        ref = g.ry.angleProperty();
+                    } else if ("r[2]".equals(s)) {
+                        ref = g.rz.angleProperty();
+                    } else if ("rp[0]".equals(s)) {
+                        ref = g.rp.xProperty();
+                    } else if ("rp[1]".equals(s)) {
+                        ref = g.rp.yProperty();
+                    } else if ("rp[2]".equals(s)) {
+                        ref = g.rp.zProperty();
+                    } else if ("sp[0]".equals(s)) {
+                        ref = g.sp.xProperty();
+                    } else if ("sp[1]".equals(s)) {
+                        ref = g.sp.yProperty();
+                    } else if ("sp[2]".equals(s)) {
+                        ref = g.sp.zProperty();
+                    }
+                    // Note: may also want to consider adding rpt in addition to rp and sp
+                    if (ref != null) {
+                        convertAnimCurveRange(n, ref, true);
+                    }
+                }
+                // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
+                if (to instanceof Joint) {
+                    Joint j = (Joint) to;
+                    DoubleProperty ref = null;
+                    String s = path.getComponentSelector();
+                    // if (DEBUG) System.out.println("selector = " + s);
+                    if ("t[0]".equals(s)) {
+                        ref = j.t.xProperty();
+                    } else if ("t[1]".equals(s)) {
+                        ref = j.t.yProperty();
+                    } else if ("t[2]".equals(s)) {
+                        ref = j.t.zProperty();
+                    } else if ("s[0]".equals(s)) {
+                        ref = j.s.xProperty();
+                    } else if ("s[1]".equals(s)) {
+                        ref = j.s.yProperty();
+                    } else if ("s[2]".equals(s)) {
+                        ref = j.s.zProperty();
+                    } else if ("jo[0]".equals(s)) {
+                        ref = j.jox.angleProperty();
+                    } else if ("jo[1]".equals(s)) {
+                        ref = j.joy.angleProperty();
+                    } else if ("jo[2]".equals(s)) {
+                        ref = j.joz.angleProperty();
+                    } else if ("r[0]".equals(s)) {
+                        ref = j.rx.angleProperty();
+                    } else if ("r[1]".equals(s)) {
+                        ref = j.ry.angleProperty();
+                    } else if ("r[2]".equals(s)) {
+                        ref = j.rz.angleProperty();
+                    }
+                    if (ref != null) {
+                        convertAnimCurveRange(n, ref, true);
+                    }
+                }
+                break;
+            }
+        }
+    }
+
+    private Object convertToFXMesh(MNode n) {
+        mVerts = (MFloat3Array) n.getAttr("vt");
+        MPolyFace mPolys = (MPolyFace) n.getAttr("fc");
+        mPointTweaks = (MFloat3Array) n.getAttr("pt");
+        MInt3Array mEdges = (MInt3Array) n.getAttr("ed");
+        edgeData = mEdges.get();
+        uvSet = ((MArray) n.getAttr("uvst")).get();
+        String currentUVSet = ((MString) n.getAttr("cuvs")).get();
+        for (int i = 0; i < uvSet.size(); i++) {
+            if (((MString) uvSet.get(i).getData("uvsn")).get().equals(currentUVSet)) {
+                uvChannel = i;
+            }
+        }
+
+        if (mPolys.getFaces() == null) {
+            if (asPolygonMesh) {
+                return new PolygonMesh();
+            } else {
+                return new TriangleMesh();
+            }
+        }
+
+        MFloat3Array normals = (MFloat3Array) n.getAttr("n");
+        return buildMeshData(mPolys.getFaces(), normals);
+    }
+
+    private int edgeVert(int edgeNumber, boolean start) {
+        boolean reverse = (edgeNumber < 0);
+        if (reverse) {
+            edgeNumber = reverse(edgeNumber);
+            return edgeData[3 * edgeNumber + (start ? 1 : 0)];
+        } else {
+            return edgeData[3 * edgeNumber + (start ? 0 : 1)];
+        }
+    }
+
+    private int reverse(int edge) {
+        if (edge < 0) {
+            return -edge - 1;
+        }
+        return edge;
+    }
+
+    private boolean edgeIsSmooth(int edgeNumber) {
+        edgeNumber = reverse(edgeNumber);
+        return edgeData[3 * edgeNumber + 2] != 0;
+    }
+
+    private int edgeStart(int edgeNumber) {
+        return edgeVert(edgeNumber, true);
+    }
+
+    private int edgeEnd(int edgeNumber) {
+        return edgeVert(edgeNumber, false);
+    }
+
+    private float[] getTexCoords(int uvChannel) {
+        if (uvSet == null || uvChannel < 0 || uvChannel >= uvSet.size()) {
+            return new float[] {0,0};
+        }
+        MCompound compound = (MCompound) uvSet.get(uvChannel);
+        MFloat2Array uvs = (MFloat2Array) compound.getFieldData("uvsp");
+        if (uvs == null || uvs.get() == null) {
+            return new float[] {0,0};
+        }
+
+        float[] texCoords = new float[uvs.getSize() * 2];
+        float[] uvsData = uvs.get();
+        for (int i = 0; i < uvs.getSize(); i++) {
+            //note the 1 - v
+            texCoords[i * 2] = uvsData[2 * i];
+            texCoords[i * 2 + 1] = 1 - uvsData[2 * i + 1];
+        }
+        return texCoords;
+    }
+
+    private void getVert(int index, Vec3f vert) {
+        float[] verts = mVerts.get();
+        float[] tweaks = null;
+        if (mPointTweaks != null) {
+            tweaks = mPointTweaks.get();
+            if (tweaks != null) {
+                if ((3 * index + 2) >= tweaks.length) {
+                    tweaks = null;
+                }
+            }
+        }
+        if (tweaks == null) {
+            vert.set(verts[3 * index + 0], verts[3 * index + 1], verts[3 * index + 2]);
+        } else {
+            vert.set(
+                    verts[3 * index + 0] + tweaks[3 * index + 0],
+                    verts[3 * index + 1] + tweaks[3 * index + 1],
+                    verts[3 * index + 2] + tweaks[3 * index + 2]);
+        }
+    }
+
+    float FPS = 24.0f;
+    float TAN_FIXED = 1;
+    float TAN_LINEAR = 2;
+    float TAN_FLAT = 3;
+    float TAN_STEPPED = 5;
+    float TAN_SPLINE = 9;
+    float TAN_CLAMPED = 10;
+    float TAN_PLATEAU = 16;
+
+    // Experimentally trying to land the frames on whole frame values
+    // Duration is still double, but internally, in Animation/Timeline,
+    // the time is discrete.  6000 units per second.
+    // Without this EPSILON, the frames might not land on whole frame values.
+    // 0.000001f seems to work for now
+    // 0.0000001f was too small on a trial run
+    static final float EPSILON = 0.000001f;
+
+    static final float MAXIMUM = 10000000.0f;
+
+    // Empirically derived from playing with animation curve editor
+    float TAN_EPSILON = 0.05f;
+
+    //=========================================================================
+    // Loader.convertAnimCurveRange
+    //-------------------------------------------------------------------------
+    // This method adds to keyFrameMap which is a
+    // TreeMap Map<Float, List<KeyValue>>
+    //=========================================================================
+    void convertAnimCurveRange(
+            MNode n, final DoubleProperty property,
+            boolean convertAnglesToDegrees) {
+        Collection inputs = n.getConnectionsTo("i");
+        boolean isDrivenAnimCurve = (inputs.size() > 0);
+        boolean useTangentInterpolator = true;  // use the NEW tangent interpolator
+
+        //---------------------------------------------------------------------
+        // Tangent types we need to handle:
+        //   2 = Linear
+        //       - The in/out tangent points in the direction of the previous/next key
+        //   3 = Flat
+        //       - The in/out tangent has no y component
+        //   5 = Stepped
+        //       - If this is seen on the out tangent of the previous
+        //         frame, immediately goes to the next value
+        //   9 = Spline
+        //       - The in / out tangents around the current keyframe
+        //         match the slope defined by the previous and next
+        //         keyframes.
+        //  10 = Clamped
+        //       - Uses spline tangents unless the keyframe is very close to the next or
+        //         previous value, in which case it uses linear tangents.
+        //  16 = Plateau
+        //       - Generally speaking, if the keyframe is a local maximum or minimum,
+        //         uses flat tangents to prevent the curve from overshooting the keyframe.
+        //         Seems to use spline tangents when the keyframe is not a local extremum.
+        //         There is an epsilon factor built in when deciding whether the flattening
+        //         behavior is to be applied.
+        // Tangent types we aren't handling:
+        //   1 = Fixed
+        //  17 = StepNext
+        //---------------------------------------------------------------------
+
+        MArray ktv = (MArray) n.getAttr("ktv");
+        MInt tan = (MInt) n.getAttr("tan");
+        int len = ktv.getSize();
+
+        // Note: the kix, kiy, kox, koy from Maya
+        // are most likely unit vectors [kix, kiy] and [kox, koy]
+        // in some tricky units that Ken figured out.
+        MFloatArray kix = (MFloatArray) n.getAttr("kix");
+        MFloatArray kiy = (MFloatArray) n.getAttr("kiy");
+        MFloatArray kox = (MFloatArray) n.getAttr("kox");
+        MFloatArray koy = (MFloatArray) n.getAttr("koy");
+        MIntArray kit = (MIntArray) n.getAttr("kit");
+        MIntArray kot = (MIntArray) n.getAttr("kot");
+        boolean hasTangent = kix != null && kix.get() != null && kix.get().length > 0;
+        boolean isRotation = n.isInstanceOf(animCurveTA) || n.isInstanceOf(animCurveUA);
+        boolean keyTimesInSeconds =
+                (n.isInstanceOf(animCurveUA) || n.isInstanceOf(animCurveUL) ||
+                        n.isInstanceOf(animCurveUT) || n.isInstanceOf(animCurveUU));
+
+        List<KeyFrame> drivenKeys = new LinkedList();
+
+        // Many incoming animation curves start at keyframe 1; to
+        // correctly interpret these we need to subtract off one frame
+        // from each key time
+        boolean needsOneFrameAdjustment = false;
+
+        // For computing tangents around the current point
+        float[] keyTimes = new float[3];
+        float[] keyValues = new float[3];
+        boolean[] keysValid = new boolean[3];
+        float[] prevOutTan = new float[3];  // for orig interpolator
+        float[] curOutTan = new float[3];  // for tan interpolator
+        float[] curInTan = new float[3];  // for both interpolators
+        Collection toPaths = n.getPathsConnectingFrom("o");
+        String keyName = null;
+        String targetName = null;
+        for (Object obj : toPaths) {
+            MPath toPath = (MPath) obj;
+            keyName = toPath.getComponentSelector();
+            targetName = toPath.getTargetNode().getName();
+        }
+
+        for (int j = 0; j < len; j++) {
+            MCompound k1 = (MCompound) ktv.getData(j);
+
+            float kt = ((MFloat) k1.getData("kt")).get();
+            float kv = ((MFloat) k1.getData("kv")).get();
+            if (j == 0 && !keyTimesInSeconds) {
+                needsOneFrameAdjustment = (kt != 0.0f);
+                //                if (DEBUG) System.out.println("needsOneFrameAdjustment = " + needsOneFrameAdjustment);
+            }
+
+            //------------------------------------------------------------
+            // Find out the previous times, values, and durations,
+            // if they exist
+            // (this code is both for tan interpolator and orig interpolator)
+            // Ken's duration is now called durationPrev
+            // Ken's k0 is now called kPrev
+            //------------------------------------------------------------
+            float durationPrev = 0.0f;
+            float ktPrev = 0.0f;
+            float kvPrev = 0.0f;
+            if (j > 0) {
+                MCompound kPrev = (MCompound) ktv.getData(j - 1);
+                ktPrev = ((MFloat) kPrev.getData("kt")).get();
+                kvPrev = ((MFloat) kPrev.getData("kv")).get();  // NEW
+                durationPrev = kt - ktPrev;
+            }
+
+            //------------------------------------------------------------
+            // Find out the next times, values, and durations,
+            // if they exist
+            // (this code is specifically for TangentInterpolator)
+            //------------------------------------------------------------
+            float durationNext = 0.0f;
+            float ktNext = 0.0f;
+            float kvNext = 0.0f;
+            if ((j + 1) < len) {
+                MCompound kNext = (MCompound) ktv.getData(j + 1);
+                ktNext = ((MFloat) kNext.getData("kt")).get();
+                kvNext = ((MFloat) kNext.getData("kv")).get();  // NEW
+                durationNext = ktNext - kt;
+            }
+
+            if (!keyTimesInSeconds) {
+                // convert frames to seconds
+                kt /= FPS;
+                ktPrev /= FPS;  // NEW
+                ktNext /= FPS;  // NEW
+            } else {
+                // convert seconds to frames
+                durationPrev *= FPS;
+                durationNext *= FPS;  // NEW
+            }
+            /*
+              var ktd = kt;
+              if (range != null) {
+              if (range.start > ktd or range.end < ktd) {
+              continue;
+              }
+              }
+            */
+
+
+            // Determine the tangent types on both sides
+            int prevOutTanType = tan.get();  // for orig interpolator
+            int curInTanType = tan.get();  // for both interpolators
+            int curOutTanType = tan.get();  // for tan intepolator
+            if (j > 0 && j < kot.getSize()) {
+                int tmp = kot.get(j - 1);
+                // Will be 0 if not actually written in the file
+                if (tmp != 0) {
+                    prevOutTanType = tmp;
+                }
+            }
+            if (j < kot.getSize()) {  // NEW
+                int tmp = kot.get(j);
+                if (tmp != 0) {
+                    curOutTanType = tmp;
+                }
+            }
+            if (j < kit.getSize()) {
+                int tmp = kit.get(j);
+                if (tmp != 0) {
+                    curInTanType = tmp;
+                }
+            }
+
+            // Get previous out tangent
+            getTangent(
+                    ktv, kix, kiy, kox, koy,
+                    j - 1,
+                    prevOutTanType,
+                    false,
+                    isRotation,
+                    keyTimesInSeconds,
+                    prevOutTan,
+                    // Temporaries
+                    keyTimes, keyValues, keysValid);
+
+            // NEW
+            // for tangentInterpolator, we also need curOutTangent
+            // Get current out tangent
+            getTangent(
+                    ktv, kix, kiy, kox, koy,
+                    j,
+                    curOutTanType,
+                    false,
+                    isRotation,
+                    keyTimesInSeconds,
+                    curOutTan,
+                    // Temporaries
+                    keyTimes, keyValues, keysValid);
+
+            // Get current in tangent
+            getTangent(
+                    ktv, kix, kiy, kox, koy,
+                    j,
+                    curInTanType,
+                    true,
+                    isRotation,
+                    keyTimesInSeconds,
+                    curInTan,
+                    // Temporaries
+                    keyTimes, keyValues, keysValid);
+
+            // Create the appropriate interpolator type:
+            // [*] DISCRETE for STEPPED type for prevOutTanType
+            // [*] Interpolator.TANGENT
+            // [*] custom Maya animation curve interpolator if specified
+            Interpolator interp = Interpolator.DISCRETE;
+            if (prevOutTanType == TAN_STEPPED) {
+                // interp = DISCRETE;
+            } else {
+                if (useTangentInterpolator) {
+                    //--------------------------------------------------
+                    // TangentIntepolator
+                    double k_ix = curInTan[0];
+                    double k_iy = curInTan[1];
+                    // don't use prevOutTan for tangentInterpolator
+                    // double k_ox = prevOutTan[0];
+                    // double k_oy = prevOutTan[1];
+                    double k_ox = curOutTan[0];
+                    double k_oy = curOutTan[1];
+
+                    /*
+                      if (DEBUG) System.out.println("n.getName(): " + n.getName());
+                      if (DEBUG) System.out.println("(k_ix = " + k_ix + ", " +
+                      "k_iy = " + k_iy + ", " +
+                      "k_ox = " + k_ox + ", " +
+                      "k_oy = " + k_oy + ")"
+                      );
+                    */
+
+                    // if (DEBUG) System.out.println("FPS = " + FPS);
+
+                    double inTangent = 0.0;
+                    double outTangent = 0.0;
+
+                    // Compute the in tangent
+                    if (k_ix != 0) {
+                        inTangent = k_iy / (k_ix * FPS);
+                    }
+                    // Compute the out tangent
+                    if (k_ox != 0) {
+                        outTangent = k_oy / (k_ox * FPS);
+                    }
+
+                    // Compute 1/3 of the time interval of this keyframe
+                    double oneThirdDeltaPrev = durationPrev / 3.0f;
+                    double oneThirdDeltaNext = durationNext / 3.0f;
+
+                    // Note: for angular animation curves, the tangents encode
+                    // changes in radians rather than degrees. Now that our
+                    // animation curves also emit radians, no conversion is
+                    // necessary here.
+                    double inTangentValue = -1 * inTangent * oneThirdDeltaPrev + kv;
+                    double outTangentValue = outTangent * oneThirdDeltaNext + kv;
+                    // We need to add "+ kv", because the value for the tangent
+                    // interpolator is in "world space" and not relative to the key
+
+                    if (inTangentValue > MAXIMUM) {
+                        inTangentValue = MAXIMUM;
+                    }
+                    if (outTangentValue > MAXIMUM) {
+                        outTangentValue = MAXIMUM;
+                    }
+
+                    double timeDeltaPrev = (durationPrev / FPS) * 1000f / 3.0f;  // in ms
+                    double timeDeltaNext = (durationNext / FPS) * 1000f / 3.0f;  // in ms
+
+                    if (true) {
+                        //                        if (DEBUG) System.out.println("________________________________________");
+                        //                        if (DEBUG) System.out.println("n.getName() = " + n.getName());
+                        //                        if (DEBUG) System.out.println("kv = " + kv);
+                        //                        if (DEBUG) System.out.println("Interpolator.TANGENT(" +
+                        //                                           "Duration.valueOf(" +
+                        //                                           timeDeltaPrev + ")" + ", " +
+                        //                                           inTangentValue + ", " +
+                        //                                           "Duration.valueOf(" +
+                        //                                           timeDeltaNext + ")" + ", " +
+                        //                                           outTangentValue + ");"
+                        //                                           );
+
+                    }
+
+                    //--------------------------------------------------
+                    // Given the diagram below, where
+                    //     k = keyframe
+                    //     i = inTangent
+                    //     o = outTangent
+                    //     + = timeDelta
+                    // Its extremely important to note that
+                    // inTangent's and outTangent's values for "i" and "o"
+                    // are NOT relative to "k".  They are in "worldSpace".
+                    // However, the timeDeltaNext and timeDeltaPrev
+                    // are in fact relative to the keyframe "k",
+                    // and are always an absolute value.
+                    // So, in summary,
+                    // the Y-axis values are not relative, but
+                    // the X-axis values are relative, and always positive
+                    //--------------------------------------------------
+                    // (Y-axis worldSpace value for i)
+                    //    inTangent i
+                    //              |
+                    //              |        timeDeltaNext (relative to x)
+                    //              |         |<------->|
+                    //              +---------k---------+
+                    //              |<------->|         |
+                    //             timeDeltaPrev        |
+                    //                                  |
+                    //                                  o outTangent
+                    //                  (Y-axis worldSpace value for o)
+                    //--------------------------------------------------
+                    Duration inDuration = Duration.millis(timeDeltaPrev);
+                    if (inDuration.toMillis() == 0) {
+                        interp = Interpolator.TANGENT(Duration.millis(timeDeltaNext), outTangentValue);
+                    } else {
+                        interp = Interpolator.TANGENT(
+                                inDuration, inTangentValue,
+                                Duration.millis(timeDeltaNext), outTangentValue);
+                    }
+                } else {
+                    MayaAnimationCurveInterpolator mayaInterp =
+                            createMayaAnimationCurveInterpolator(
+                                    prevOutTan[0], prevOutTan[1],
+                                    curInTan[0], curInTan[1],
+                                    durationPrev,
+                                    true);
+                    // mayaInterp.isRotation = isRotation;  // was commented out long ago by Ken/Chris
+                    // mayaInterp.debug = targetName + "." + keyName + "@"+ kt;
+                    interp = mayaInterp;
+                }
+            }
+
+            float t = kt - EPSILON;
+            if (t < 0.0) {
+                continue; // just skipping all the negative frames
+            }
+
+            /*
+            // This was the old way of adjusting
+            // for the one frame adjustment.
+            if (needsOneFrameAdjustment) {
+                t = kt - 1.0f/FPS;
+            } else {
+                t = kt;
+            }
+            // The new way is below ...
+            // See: (needsOneFrameAdjustment && (j == 0))
+            */
+
+            // if (DEBUG) System.out.println("j = " + j);
+            //            if (DEBUG) System.out.println("t = " + t);
+            if (isRotation) {
+                // Maya angular animation curves implicitly output in radians.
+                // In order to properly process them throughout the utility node
+                // network, we have to follow this convention, and implicitly
+                // convert the inputs of transforms' rotation angles to degrees
+                // at the end.
+                if (!convertAnglesToDegrees) {
+                    kv = (float) Math.toRadians(kv);
+                }
+            }
+            // if (DEBUG) System.out.println("creating key value at: " + t + ": " + targetName + "." + keyName);
+            KeyValue keyValue = new KeyValue(property, kv, interp);  // [!] API change
+
+            // If the first frame is at frame 1,
+            // at least for now, try adding in a frame at frame 0
+            // which is a duplicate of the frame at frame 1,
+            // to counter-act some strange behavior we are seeing
+            // if there is no key at frame 0.
+            if (needsOneFrameAdjustment && (j == 0)) {
+                if (DEBUG) System.out.println("[!] ATTEMPTING FRAME ONE ADJUSTMENT [!]");
+                // [!] API change
+                // KeyValue keyValue0 = new KeyValue(property, kv, Interpolator.LINEAR);
+                KeyValue keyValue0 = new KeyValue(property, kv);
+                addKeyframe(0.0f, keyValue0);
+            }
+
+            // Add keyframe
+            addKeyframe(t, keyValue);
+
+            /*
+            // If you're at the last keyframe,
+            // at least for now, try adding in an extra frame
+            // to pad the ending
+            if (j == (len - 1)) {
+                addKeyframe((t+0.0001667f), keyValue);
+            }
+            */
+        }
+    }
+
+    //=========================================================================
+    // Loader.addKeyframe
+    //=========================================================================
+    void addKeyframe(float t, KeyValue keyValue) {
+        List<KeyValue> vals = keyFrameMap.get(t);
+        if (vals == null) {
+            vals = new LinkedList<KeyValue>();
+            keyFrameMap.put(t, vals);
+        }
+        vals.add(keyValue);
+    }
+
+    //=========================================================================
+    // Loader.createMayaAnimationCurveInterpolator
+    //=========================================================================
+    MayaAnimationCurveInterpolator createMayaAnimationCurveInterpolator(
+            float kox,
+            float koy,
+            float kix,
+            float kiy,
+            float duration,
+            boolean hasTangent) {
+        if (duration == 0.0f) {
+            return new MayaAnimationCurveInterpolator(0, 0, true);
+        } else {
+            // Compute the out tangent
+            float outTangent = koy / (kox * FPS);
+            // Compute the in tangent
+            float inTangent = kiy / (kix * FPS);
+            // Compute 1/3 of the time interval of this keyframe
+            float oneThirdDelta = duration / 3.0f;
+
+            // Note: for angular animation curves, the tangents encode
+            // changes in radians rather than degrees. Now that our
+            // animation curves also emit radians, no conversion is
+            // necessary here.
+            float p1Delta = outTangent * oneThirdDelta;
+            float p2Delta = -inTangent * oneThirdDelta;
+            return new MayaAnimationCurveInterpolator(p1Delta, p2Delta, false);
+        }
+    }
+
+    //=========================================================================
+    // Loader.getTangent
+    //=========================================================================
+    void getTangent(
+            MArray ktv,
+            MFloatArray kix,
+            MFloatArray kiy,
+            MFloatArray kox,
+            MFloatArray koy,
+            int index,
+            int tangentType,
+            boolean inTangent,
+            boolean isRotation,
+            boolean keyTimesInSeconds,
+            float[] result,
+            // Temporaries
+            float[] tmpKeyTimes,
+            float[] tmpKeyValues,
+            boolean[] tmpKeysValid) {
+        float[] output = result;
+        float[] keyTimes = tmpKeyTimes;
+        float[] keyValues = tmpKeyValues;
+        boolean[] keysValid = tmpKeysValid;
+        if (inTangent) {
+            if (index >= 0 && index < kix.getSize() && index < kiy.getSize()) {
+                output[0] = kix.get(index);
+                output[1] = kiy.get(index);
+                if (output[0] != 0.0f ||
+                        output[1] != 0.0f) {
+                    // A keyframe was specified in the file
+                    return;
+                }
+            }
+        } else {
+            if (index >= 0 && index < kox.getSize() && index < koy.getSize()) {
+                output[0] = kox.get(index);
+                output[1] = koy.get(index);
+                if (output[0] != 0.0f ||
+                        output[1] != 0.0f) {
+                    // A keyframe was specified in the file
+                    return;
+                }
+            }
+        }
+
+        // Need to compute the tangent from the surrounding key times and values
+        int i = -1;
+        while (i < 2) {
+            int cur = index + i;
+            if (cur >= 0 && cur < ktv.getSize()) {
+                MCompound k1 = (MCompound) ktv.getData(cur);
+                float kt = ((MFloat) k1.getData("kt")).get();
+                if (keyTimesInSeconds) {
+                    // Convert seconds to frames
+                    kt *= FPS;
+                }
+                float kv = ((MFloat) k1.getData("kv")).get();
+                if (isRotation) {
+                    // Maya angular animation curves implicitly output in radians -- see below
+                    kv = (float) Math.toRadians(kv);
+                }
+                keyTimes[1 + i] = kt;
+                keyValues[1 + i] = kv;
+                keysValid[1 + i] = true;
+            } else {
+                keysValid[1 + i] = false;
+            }
+            ++i;
+        }
+        computeTangent(keyTimes, keyValues, keysValid, tangentType, inTangent, result);
+    }
+
+    //=========================================================================
+    // Loader.computeTangent
+    //=========================================================================
+    void computeTangent(
+            float[] keyTimes,
+            float[] keyValues,
+            boolean[] keysValid,
+            float tangentType,
+            boolean inTangent,
+            float[] computedTangent) {
+        float[] output = computedTangent;
+        if (tangentType == TAN_LINEAR) {
+            float x0;
+            float x1;
+            float y0;
+            float y1;
+            if (inTangent) {
+                if (!keysValid[0]) {
+                    // Start of the animation curve: doesn't matter
+                    output[0] = 1.0f;
+                    output[1] = 0.0f;
+                    return;
+                }
+                x0 = keyTimes[0];
+                x1 = keyTimes[1];
+                y0 = keyValues[0];
+                y1 = keyValues[1];
+            } else {
+                if (!keysValid[2]) {
+                    // End of the animation curve: doesn't matter
+                    output[0] = 1.0f;
+                    output[1] = 0.0f;
+                    return;
+                }
+                x0 = keyTimes[1];
+                x1 = keyTimes[2];
+                y0 = keyValues[1];
+                y1 = keyValues[2];
+            }
+            float dx = x1 - x0;
+            float dy = y1 - y0;
+            output[0] = dx;
+            output[1] = dy;
+            // Fall through to perform normalization
+        } else if (tangentType == TAN_FLAT) {
+            output[0] = 1.0f;
+            output[1] = 0.0f;
+            return;
+        } else if (tangentType == TAN_STEPPED) {
+            // Doesn't matter what the tangent values are -- will use discrete type interpolator
+            return;
+        } else if (tangentType == TAN_SPLINE) {
+            // Whether we're computing the in or out tangent, if we don't have one or the other
+            // keyframe, it reduces to a simpler case
+            if (!(keysValid[0] && keysValid[2])) {
+                // Reduces to the linear case
+                computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
+                return;
+            }
+
+            // Figure out the slope between the adjacent keyframes
+            output[0] = keyTimes[2] - keyTimes[0];
+            output[1] = keyValues[2] - keyValues[0];
+        } else if (tangentType == TAN_CLAMPED) {
+            if (!(keysValid[0] && keysValid[2])) {
+                // Reduces to the linear case at the ends of the animation curve
+                computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
+                return;
+            }
+
+            float inDiff = Math.abs(keyValues[1] - keyValues[0]);
+            float outDiff = Math.abs(keyValues[2] - keyValues[1]);
+
+            if (inDiff <= TAN_EPSILON || outDiff <= TAN_EPSILON) {
+                // The Maya docs say that this reduces to the linear
+                // case. If this were true, then the apparent behavior
+                // would be to compute the linear tangent between the
+                // two keyframes which are closest together, and
+                // reflect that tangent about the current keyframe.
+                // computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, (inDiff < outDiff), computedTangent);
+
+                // However, experimentation in the curve editor
+                // clearly indicates for our test cases that flat
+                // rather than linear interpolation is used in this
+                // case. Therefore to match Maya's actual behavior
+                // more closely we do the following.
+                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
+            } else {
+                // Use spline tangents
+                computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
+            }
+
+            return;
+        } else if (tangentType == TAN_PLATEAU) {
+            if (!(keysValid[0] && keysValid[2])) {
+                // Reduces to the flat case at the ends of the animation curve
+                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
+                return;
+            }
+
+            // Otherwise, figure out whether we have any local extremum
+            if ((keyValues[1] > keyValues[0] &&
+                    keyValues[1] > keyValues[2]) ||
+                    (keyValues[1] < keyValues[0] &&
+                            keyValues[1] < keyValues[2])) {
+                // Use flat tangent
+                computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
+            } else {
+                // The rule is that we use spline tangents unless
+                // doing so would cause the curve to go outside the
+                // envelope of the keyvalues. To figure this out, we
+                // have to compute both the in and out tangents as
+                // though we were using splines, and see whether the
+                // intermediate bezier control points go outside the
+                // hull.
+                //
+                // Note that it doesn't matter whether we compute the
+                // "in" or "out" tangent at the current point -- the
+                // result is the same.
+                computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
+
+                // Compute the values from the keyframe along the
+                // tangent 1/3 of the way to the previous and next
+                // keyframes
+                float tangent = computedTangent[1] / (computedTangent[0] * FPS);
+                float prev13 = keyValues[1] - tangent * ((keyTimes[1] - keyTimes[0]) / 3.0f);
+                float next13 = keyValues[1] + tangent * ((keyTimes[2] - keyTimes[1]) / 3.0f);
+
+                if (isBetween(prev13, keyValues[0], keyValues[2]) &&
+                        isBetween(next13, keyValues[0], keyValues[2])) {
+                } else {
+                    // Use flat tangent
+                    computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
+                }
+            }
+
+            return;
+        }
+
+        // Perform normalization
+        // NOTE the scaling of the X coordinate -- this is needed to match Maya's math
+        output[0] /= FPS;
+        float len = (float) Math.sqrt(
+                output[0] * output[0] +
+                        output[1] * output[1]);
+        if (len != 0.0f) {
+            output[0] /= len;
+            output[1] /= len;
+        }
+        // println("TAN LINEAR {output[0]} {output[1]}");
+    }
+
+    //=========================================================================
+    // Loader.isBetween
+    //=========================================================================
+    boolean isBetween(
+            float value,
+            float v1,
+            float v2) {
+        return ((v1 <= value && value <= v2) ||
+                (v1 >= value && value >= v2));
+    }
+
+
+    static class VertexHash {
+        private int vertexIndex;
+        private int normalIndex;
+        private int[] uvIndices;
+
+        VertexHash(
+                int vertexIndex,
+                int normalIndex,
+                int[] uvIndices) {
+            this.vertexIndex = vertexIndex;
+            this.normalIndex = normalIndex;
+            if (uvIndices != null) {
+                this.uvIndices = (int[]) uvIndices.clone();
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            int code = vertexIndex;
+            code *= 17;
+            code += normalIndex;
+            if (uvIndices != null) {
+                for (int i = 0; i < uvIndices.length; i++) {
+                    code *= 17;
+                    code += uvIndices[i];
+                }
+            }
+            return code;
+        }
+
+        @Override
+        public boolean equals(Object arg) {
+            if (arg == null || !(arg instanceof VertexHash)) {
+                return false;
+            }
+
+            VertexHash other = (VertexHash) arg;
+            if (vertexIndex != other.vertexIndex) {
+                return false;
+            }
+            if (normalIndex != other.normalIndex) {
+                return false;
+            }
+            if ((uvIndices != null) != (other.uvIndices != null)) {
+                return false;
+            }
+            if (uvIndices != null) {
+                if (uvIndices.length != other.uvIndices.length) {
+                    return false;
+                }
+                for (int i = 0; i < uvIndices.length; i++) {
+                    if (uvIndices[i] != other.uvIndices[i]) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+    }
+
+    private Object buildMeshData(List<MPolyFace.FaceData> faces, MFloat3Array normals) {
+        // Setup vertexes
+        float[] verts = mVerts.get();
+        float[] tweaks = null;
+        if (mPointTweaks != null) {
+            tweaks = mPointTweaks.get();
+        }
+        float[] points = new float[verts.length];
+        for (int index = 0; index < verts.length; index += 3) {
+            if (tweaks != null && tweaks.length > index + 2) {
+                points[index] = verts[index] + tweaks[index];
+                points[index + 1] = verts[index + 1] + tweaks[index + 1];
+                points[index + 2] = verts[index + 2] + tweaks[index + 2];
+            } else {
+                points[index] = verts[index];
+                points[index + 1] = verts[index + 1];
+                points[index + 2] = verts[index + 2];
+            }
+        }
+
+        // copy UV as-is (if any)
+        float[] texCoords = getTexCoords(uvChannel);
+
+        if (asPolygonMesh) {
+            List<int[]> ff = new ArrayList<int[]>();
+            for (int f = 0; f < faces.size(); f++) {
+                MPolyFace.FaceData faceData = faces.get(f);
+                int[] faceEdges = faceData.getFaceEdges();
+                int[][] uvData = faceData.getUVData();
+                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
+                if (faceEdges != null && faceEdges.length > 0) {
+                    int[] polyFace = new int[faceEdges.length * 2];
+                    for (int i = 0; i < faceEdges.length; i++) {
+                        int vIndex = edgeStart(faceEdges[i]);
+                        int uvIndex = uvIndices == null ? 0 : uvIndices[i];
+                        polyFace[i*2] = vIndex;
+                        polyFace[i*2+1] = uvIndex;
+                    }
+                    ff.add(polyFace);
+                }
+            }
+            int[][] facesArray = ff.toArray(new int[ff.size()][]);
+            
+            int[][] faceNormals = new int[facesArray.length][];
+            int normalInd = 0;
+            for (int f = 0; f < faceNormals.length; f++) {
+                faceNormals[f] = new int[facesArray[f].length/2];
+                for (int e = 0; e < faceNormals[f].length; e++) {
+                    faceNormals[f][e] = normalInd++;
+                }
+            }
+            int[] smGroups;
+            // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
+            if (normalInd == normals.getSize()) {
+                smGroups = SmoothingGroups.calcSmoothGroups(facesArray, faceNormals, normals.get());
+            } else {
+                smGroups = new int[facesArray.length];
+                Arrays.fill(smGroups, 1);
+            }
+
+            PolygonMesh mesh = new PolygonMesh();
+            mesh.getPoints().setAll(points);
+            mesh.getTexCoords().setAll(texCoords);
+            mesh.faces = facesArray;
+            mesh.getFaceSmoothingGroups().setAll(smGroups);
+            return mesh;
+        } else {
+            // Split the polygonal faces into triangle faces
+            List<Integer> ff = new ArrayList<Integer>();
+            List<Integer> nn = new ArrayList<Integer>();
+            int nIndex = 0;
+            
+            for (int f = 0; f < faces.size(); f++) {
+                MPolyFace.FaceData faceData = faces.get(f);
+                int[] faceEdges = faceData.getFaceEdges();
+                int[][] uvData = faceData.getUVData();
+                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
+                if (faceEdges != null && faceEdges.length > 0) {
+
+                    // Generate triangle fan about the first vertex
+                    int vIndex0 = edgeStart(faceEdges[0]);
+                    int uvIndex0 = uvIndices == null ? 0 : uvIndices[0];
+                    int nIndex0 = nIndex++;
+
+                    int vIndex1 = edgeStart(faceEdges[1]);
+                    int uvIndex1 = uvIndices == null ? 0 : uvIndices[1];
+                    int nIndex1 = nIndex++;
+
+                    for (int i = 2; i < faceEdges.length; i++) {
+                        int vIndex2 = edgeStart(faceEdges[i]);
+                        int uvIndex2 = uvIndices == null ? 0 : uvIndices[i];
+                        int nIndex2 = nIndex++;
+
+                        ff.add(vIndex0);
+                        ff.add(uvIndex0);
+                        ff.add(vIndex1);
+                        ff.add(uvIndex1);
+                        ff.add(vIndex2);
+                        ff.add(uvIndex2);
+                        nn.add(nIndex0);
+                        nn.add(nIndex1);
+                        nn.add(nIndex2);
+
+                        vIndex1 = vIndex2;
+                        uvIndex1 = uvIndex2;
+                    }
+                }
+            }
+            int[] fff = new int[ff.size()];
+            for (int i = 0; i < fff.length; i++) {
+                fff[i] = ff.get(i);
+            }
+            
+            int[] smGroups;
+            // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
+            if (nIndex == normals.getSize()) {
+                int[] faceNormals = new int[nn.size()];
+                for (int i = 0; i < faceNormals.length; i++) {
+                    faceNormals[i] = nn.get(i);
+                }
+                smGroups = SmoothingGroups.calcSmoothGroups(fff, faceNormals, normals.get());
+            } else {
+                smGroups = new int[fff.length];
+                Arrays.fill(smGroups, 1);
+            }
+            
+            TriangleMesh mesh = new TriangleMesh();
+            mesh.getPoints().setAll(points);
+            mesh.getTexCoords().setAll(texCoords);
+            mesh.getFaces().setAll(fff);
+            mesh.getFaceSmoothingGroups().setAll(smGroups);
+            return mesh;
+        }
+    }
+
+    MNode resolveOutputMesh(MNode n) {
+        MNode og;
+        List<MPath> ogc0 = n.getPathsConnectingFrom("og[0]");
+        if (ogc0.size() > 0) {
+            og = ogc0.get(0).getTargetNode();
+        } else {
+            ogc0 = n.getPathsConnectingFrom("og");
+            if (ogc0.size() > 0) {
+                og = ogc0.get(0).getTargetNode();
+            } else {
+                return null;
+            }
+        }
+        if (og.isInstanceOf(meshType)) {
+            return og;
+        }
+        // println("r.OG={og}");
+        while (og.isInstanceOf(groupPartsType)) {
+            og = og.getPathsConnectingFrom("og").get(0).getTargetNode();
+        }
+        if (og.isInstanceOf(meshType)) {
+            return og;
+        }
+        // println("r1.OG={og}");
+        if (og == null) {
+            return null;
+        }
+        return resolveOutputMesh(og);
+    }
+
+    MNode resolveInputMesh(MNode n) {
+        return resolveInputMesh(n, true);
+    }
+
+    MNode resolveInputMesh(MNode n, boolean followBlend) {
+        MNode groupParts;
+        if (!n.isInstanceOf(groupPartsType)) {
+            groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
+        } else {
+            groupParts = n;
+        }
+        MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
+        if (origMesh == null) {
+            MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
+            groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
+            origMesh =
+                    groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
+        }
+        // println("N={n} ORIG_MESH={origMesh}");
+        if (origMesh == null) {
+            return null;
+        }
+        if (origMesh.isInstanceOf(meshType)) {
+            return origMesh;
+        }
+        if (origMesh.isInstanceOf(blendShapeType)) {
+            // return the blend shape's output
+            return resolveOutputMesh(origMesh);
+        }
+        return resolveInputMesh(origMesh);
+    }
+
+    MNode resolveOrigInputMesh(MNode n) {
+
+        MNode groupParts;
+        if (!n.isInstanceOf(groupPartsType)) {
+            groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
+        } else {
+            groupParts = n;
+        }
+        MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
+        if (origMesh == null) {
+            MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
+            groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
+            origMesh =
+                    groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
+        }
+        if (origMesh == null) {
+            return null;
+        }
+        // println("N={n} ORIG_MESH={origMesh}");
+        if (origMesh.isInstanceOf(meshType)) {
+            return origMesh;
+        }
+        return resolveOrigInputMesh(origMesh);
+    }
+
+    Affine convertMatrix(MFloatArray mayaMatrix) {
+        if (mayaMatrix == null || mayaMatrix.getSize() < 16) {
+            return new Affine();
+        }
+
+        Affine result = new Affine();
+        result.setMxx(mayaMatrix.get(0 * 4 + 0));
+        result.setMxy(mayaMatrix.get(1 * 4 + 0));
+        result.setMxz(mayaMatrix.get(2 * 4 + 0));
+        result.setMyx(mayaMatrix.get(0 * 4 + 1));
+        result.setMyy(mayaMatrix.get(1 * 4 + 1));
+        result.setMyz(mayaMatrix.get(2 * 4 + 1));
+        result.setMzx(mayaMatrix.get(0 * 4 + 2));
+        result.setMzy(mayaMatrix.get(1 * 4 + 2));
+        result.setMzz(mayaMatrix.get(2 * 4 + 2));
+        result.setTx(mayaMatrix.get(3 * 4 + 0));
+        result.setTy(mayaMatrix.get(3 * 4 + 1));
+        result.setTz(mayaMatrix.get(3 * 4 + 2));
+        return result;
+    }
+
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/MayaGroup.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/MayaGroup.java	Tue Sep 24 10:09:57 2013 -0700
@@ -54,6 +54,7 @@
     Translate rpt = new Translate();  // rotate pivot translate
     Translate rp = new Translate();  // rotate pivot
     Translate rpi = new Translate();  // rotate pivot inverse
+    Translate spt = new Translate();  // scale pivot translate
     Translate sp = new Translate();  // scale pivot
     Translate spi = new Translate();  // scale pivot inverse
     // should bind rpi = -rp, but doesn't currently work afaict
@@ -95,6 +96,6 @@
     }
 
     private void initTransforms() {
-        getTransforms().setAll(t, rpt, rp, rz, ry, rx, rpi, sp, s, spi);
+        getTransforms().setAll(t, rpt, rp, rz, ry, rx, rpi, spt, sp, s, spi);
     }
 }
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/AutoScalingGroup.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/AutoScalingGroup.java	Tue Sep 24 10:09:57 2013 -0700
@@ -14,10 +14,10 @@
 public class AutoScalingGroup extends Group {
     private double size;
     private double twoSize;
-    private boolean autoScale = true;
+    private boolean autoScale = false;
     private Translate translate = new Translate(0,0,0);
     private Scale scale = new Scale(1,1,1,0,0,0);
-    private SimpleBooleanProperty enabled = new SimpleBooleanProperty(true) {
+    private SimpleBooleanProperty enabled = new SimpleBooleanProperty(false) {
         @Override protected void invalidated() {
             if (get()) {
                 getTransforms().setAll(scale, translate);
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/ContentModel.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/ContentModel.java	Tue Sep 24 10:09:57 2013 -0700
@@ -43,6 +43,8 @@
 import javafx.scene.PerspectiveCamera;
 import javafx.scene.PointLight;
 import javafx.scene.SubScene;
+import javafx.scene.input.*;
+import javafx.scene.layout.Region;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.PhongMaterial;
 import javafx.scene.shape.Box;
@@ -55,26 +57,26 @@
 import com.javafx.experiments.shape3d.SubdivisionMesh;
 import javafx.beans.property.ObjectProperty;
 import javafx.event.EventHandler;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.input.ScrollEvent;
+import javafx.event.EventType;
+import javafx.scene.Scene;
+import javafx.util.Duration;
 
 /**
  * 3D Content Model for Viewer App. Contains the 3D scene and everything related to it: light, cameras etc.
  */
 public class ContentModel {
-    private final SubScene subScene;
+    private final SimpleObjectProperty<SubScene> subScene = new SimpleObjectProperty<>();
     private final Group root3D = new Group();
     private final PerspectiveCamera camera = new PerspectiveCamera(true);
     private final Rotate cameraXRotate = new Rotate(-20,0,0,0,Rotate.X_AXIS);
     private final Rotate cameraYRotate = new Rotate(-20,0,0,0,Rotate.Y_AXIS);
     private final Rotate cameraLookXRotate = new Rotate(0,0,0,0,Rotate.X_AXIS);
     private final Rotate cameraLookZRotate = new Rotate(0,0,0,0,Rotate.Z_AXIS);
-    //private final Translate cameraPosition = new Translate(0,0,-7);
     private final Translate cameraPosition = new Translate(0,0,0);
-    final Xform cameraXform = new Xform();
-    final Xform cameraXform2 = new Xform();
-    final Xform cameraXform3 = new Xform();
-    final double cameraDistance = 200;
+    private final Xform cameraXform = new Xform();
+    private final Xform cameraXform2 = new Xform();
+    private final Xform cameraXform3 = new Xform();
+    private final double cameraDistance = 200;
     private double dragStartX, dragStartY, dragStartRotateX, dragStartRotateY;
     private ObjectProperty<Node> content = new SimpleObjectProperty<>();
     private AutoScalingGroup autoScalingGroup = new AutoScalingGroup(2);
@@ -154,22 +156,252 @@
             }
         }
     };
+    private SimpleBooleanProperty msaa = new SimpleBooleanProperty(){
+        @Override protected void invalidated() {
+            rebuildSubScene();
+        }
+    };
     private boolean wireframe = false;
     private int subdivisionLevel = 0;
     private SubdivisionMesh.BoundaryMode boundaryMode = SubdivisionMesh.BoundaryMode.CREASE_EDGES;
     private SubdivisionMesh.MapBorderMode mapBorderMode = SubdivisionMesh.MapBorderMode.NOT_SMOOTH;
-    
-    double mousePosX;
-    double mousePosY;
-    double mouseOldX;
-    double mouseOldY;
-    double mouseDeltaX;
-    double mouseDeltaY;
+
+    private double mousePosX;
+    private double mousePosY;
+    private double mouseOldX;
+    private double mouseOldY;
+    private double mouseDeltaX;
+    private double mouseDeltaY;
+
+    private final EventHandler<MouseEvent> mouseEventHandler = event -> {
+        // System.out.println("MouseEvent ...");
+
+        double yFlip = 1.0;
+        if (getYUp()) {
+            yFlip = 1.0;
+        }
+        else {
+            yFlip = -1.0;
+        }
+        if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
+            dragStartX = event.getSceneX();
+            dragStartY = event.getSceneY();
+            dragStartRotateX = cameraXRotate.getAngle();
+            dragStartRotateY = cameraYRotate.getAngle();
+            mousePosX = event.getSceneX();
+            mousePosY = event.getSceneY();
+            mouseOldX = event.getSceneX();
+            mouseOldY = event.getSceneY();
+
+        } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
+            double xDelta = event.getSceneX() -  dragStartX;
+            double yDelta = event.getSceneY() -  dragStartY;
+            //cameraXRotate.setAngle(dragStartRotateX - (yDelta*0.7));
+            //cameraYRotate.setAngle(dragStartRotateY + (xDelta*0.7));
+
+            double modifier = 1.0;
+            double modifierFactor = 0.3;
+
+            if (event.isControlDown()) {
+                modifier = 0.1;
+            }
+            if (event.isShiftDown()) {
+                modifier = 10.0;
+            }
+
+            mouseOldX = mousePosX;
+            mouseOldY = mousePosY;
+            mousePosX = event.getSceneX();
+            mousePosY = event.getSceneY();
+            mouseDeltaX = (mousePosX - mouseOldX); //*DELTA_MULTIPLIER;
+            mouseDeltaY = (mousePosY - mouseOldY); //*DELTA_MULTIPLIER;
+
+            double flip = -1.0;
+
+            boolean alt = (true || event.isAltDown());  // For now, don't require ALT to be pressed
+            if (alt && (event.isMiddleButtonDown() || (event.isPrimaryButtonDown() && event.isSecondaryButtonDown()))) {
+                cameraXform2.t.setX(cameraXform2.t.getX() + flip*mouseDeltaX*modifierFactor*modifier*0.3);  // -
+                cameraXform2.t.setY(cameraXform2.t.getY() + yFlip*mouseDeltaY*modifierFactor*modifier*0.3);  // -
+            }
+            else if (alt && event.isPrimaryButtonDown()) {
+                cameraXform.ry.setAngle(cameraXform.ry.getAngle() - yFlip*mouseDeltaX*modifierFactor*modifier*2.0);  // +
+                cameraXform.rx.setAngle(cameraXform.rx.getAngle() + flip*mouseDeltaY*modifierFactor*modifier*2.0);  // -
+            }
+            else if (alt && event.isSecondaryButtonDown()) {
+                double z = cameraPosition.getZ();
+                // double z = camera.getTranslateZ();
+                // double newZ = z + yFlip*flip*mouseDeltaX*modifierFactor*modifier;
+                double newZ = z - flip*(mouseDeltaX+mouseDeltaY)*modifierFactor*modifier;
+                System.out.println("newZ = " + newZ);
+                cameraPosition.setZ(newZ);
+                // camera.setTranslateZ(newZ);
+            }
+
+        }
+    };
+    private final EventHandler<ScrollEvent> scrollEventHandler = event -> {
+        if (event.getTouchCount() > 0) { // touch pad scroll
+            cameraXform2.t.setX(cameraXform2.t.getX() - (0.01*event.getDeltaX()));  // -
+            cameraXform2.t.setY(cameraXform2.t.getY() + (0.01*event.getDeltaY()));  // -
+        } else {
+            double z = cameraPosition.getZ()-(event.getDeltaY()*0.2);
+            z = Math.max(z,-1000);
+            z = Math.min(z,0);
+            cameraPosition.setZ(z);
+        }
+    };
+    private final EventHandler<ZoomEvent> zoomEventHandler = event -> {
+        if (!Double.isNaN(event.getZoomFactor()) && event.getZoomFactor() > 0.8 && event.getZoomFactor() < 1.2) {
+            double z = cameraPosition.getZ()/event.getZoomFactor();
+            z = Math.max(z,-1000);
+            z = Math.min(z,0);
+            cameraPosition.setZ(z);
+        }
+    };
+    private final EventHandler<KeyEvent> keyEventHandler = event -> {
+        /*
+        if (!Double.isNaN(event.getZoomFactor()) && event.getZoomFactor() > 0.8 && event.getZoomFactor() < 1.2) {
+            double z = cameraPosition.getZ()/event.getZoomFactor();
+            z = Math.max(z,-1000);
+            z = Math.min(z,0);
+            cameraPosition.setZ(z);
+        }
+        */
+        System.out.println("KeyEvent ...");
+        Timeline timeline = getTimeline();
+        Duration currentTime;
+        double CONTROL_MULTIPLIER = 0.1;
+        double SHIFT_MULTIPLIER = 0.1;
+        double ALT_MULTIPLIER = 0.5;
+        //System.out.println("--> handleKeyboard>handle");
+        
+        // event.getEventType();
+        
+        switch (event.getCode()) {
+            case F:
+                if (event.isControlDown()) {
+                    //onButtonSave();
+                }
+                break;
+            case O:
+                if (event.isControlDown()) {
+                    //onButtonLoad();
+                }
+                break;
+            case Z:
+                if (event.isShiftDown()) {
+                    cameraXform.ry.setAngle(0.0);
+                    cameraXform.rx.setAngle(0.0);
+                    camera.setTranslateZ(-300.0);
+                }   
+                cameraXform2.t.setX(0.0);
+                cameraXform2.t.setY(0.0);
+                break;
+            /*
+            case SPACE:
+                if (timelinePlaying) {
+                    timeline.pause();
+                    timelinePlaying = false;
+                }
+                else {
+                    timeline.play();
+                    timelinePlaying = true;
+                }
+                break;
+            */
+            case UP:
+                if (event.isControlDown() && event.isShiftDown()) {
+                    cameraXform2.t.setY(cameraXform2.t.getY() - 10.0*CONTROL_MULTIPLIER);  
+                }  
+                else if (event.isAltDown() && event.isShiftDown()) {
+                    cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 10.0*ALT_MULTIPLIER);  
+                }
+                else if (event.isControlDown()) {
+                    cameraXform2.t.setY(cameraXform2.t.getY() - 1.0*CONTROL_MULTIPLIER);  
+                }
+                else if (event.isAltDown()) {
+                    cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 2.0*ALT_MULTIPLIER);  
+                }
+                else if (event.isShiftDown()) {
+                    double z = camera.getTranslateZ();
+                    double newZ = z + 5.0*SHIFT_MULTIPLIER;
+                    camera.setTranslateZ(newZ);
+                }
+                break;
+            case DOWN:
+                if (event.isControlDown() && event.isShiftDown()) {
+                    cameraXform2.t.setY(cameraXform2.t.getY() + 10.0*CONTROL_MULTIPLIER);  
+                }  
+                else if (event.isAltDown() && event.isShiftDown()) {
+                    cameraXform.rx.setAngle(cameraXform.rx.getAngle() + 10.0*ALT_MULTIPLIER);  
+                }
+                else if (event.isControlDown()) {
+                    cameraXform2.t.setY(cameraXform2.t.getY() + 1.0*CONTROL_MULTIPLIER);  
+                }
+                else if (event.isAltDown()) {
+                    cameraXform.rx.setAngle(cameraXform.rx.getAngle() + 2.0*ALT_MULTIPLIER);  
+                }
+                else if (event.isShiftDown()) {
+                    double z = camera.getTranslateZ();
+                    double newZ = z - 5.0*SHIFT_MULTIPLIER;
+                    camera.setTranslateZ(newZ);
+                }
+                break;
+            case RIGHT:
+                if (event.isControlDown() && event.isShiftDown()) {
+                    cameraXform2.t.setX(cameraXform2.t.getX() + 10.0*CONTROL_MULTIPLIER);  
+                }  
+                else if (event.isAltDown() && event.isShiftDown()) {
+                    cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 10.0*ALT_MULTIPLIER);  
+                }
+                else if (event.isControlDown()) {
+                    cameraXform2.t.setX(cameraXform2.t.getX() + 1.0*CONTROL_MULTIPLIER);  
+                }
+                else if (event.isShiftDown()) {
+                    currentTime = timeline.getCurrentTime();
+                    timeline.jumpTo(Frame.frame(Math.round(Frame.toFrame(currentTime)/10.0)*10 + 10));
+                    // timeline.jumpTo(Duration.seconds(currentTime.toSeconds() + ONE_FRAME));
+                }
+                else if (event.isAltDown()) {
+                    cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 2.0*ALT_MULTIPLIER);  
+                }
+                else {
+                    currentTime = timeline.getCurrentTime();
+                    timeline.jumpTo(Frame.frame(Frame.toFrame(currentTime) + 1));
+                    // timeline.jumpTo(Duration.seconds(currentTime.toSeconds() + ONE_FRAME));
+                }
+                break;
+            case LEFT:
+                if (event.isControlDown() && event.isShiftDown()) {
+                    cameraXform2.t.setX(cameraXform2.t.getX() - 10.0*CONTROL_MULTIPLIER);  
+                }  
+                else if (event.isAltDown() && event.isShiftDown()) {
+                    cameraXform.ry.setAngle(cameraXform.ry.getAngle() + 10.0*ALT_MULTIPLIER);  // -
+                }
+                else if (event.isControlDown()) {
+                    cameraXform2.t.setX(cameraXform2.t.getX() - 1.0*CONTROL_MULTIPLIER);  
+                }
+                else if (event.isShiftDown()) {
+                    currentTime = timeline.getCurrentTime();
+                    timeline.jumpTo(Frame.frame(Math.round(Frame.toFrame(currentTime)/10.0)*10 - 10));
+                    // timeline.jumpTo(Duration.seconds(currentTime.toSeconds() - ONE_FRAME));
+                }
+                else if (event.isAltDown()) {
+                    cameraXform.ry.setAngle(cameraXform.ry.getAngle() + 2.0*ALT_MULTIPLIER);  // -
+                }
+                else {
+                    currentTime = timeline.getCurrentTime();
+                    timeline.jumpTo(Frame.frame(Frame.toFrame(currentTime) - 1));
+                    // timeline.jumpTo(Duration.seconds(currentTime.toSeconds() - ONE_FRAME));
+                }
+                break;
+        }
+        //System.out.println(cameraXform.getTranslateX() + ", " + cameraXform.getTranslateY() + ", " + cameraXform.getTranslateZ());
+
+
+    };
 
     public ContentModel() {
-        subScene = new SubScene(root3D,400,400,true,false);
-        subScene.setFill(Color.ALICEBLUE);
-
         // CAMERA
         camera.setNearClip(1.0); // TODO: Workaround as per RT-31255
         camera.setFarClip(10000.0); // TODO: Workaround as per RT-31255
@@ -181,7 +413,6 @@
                 cameraPosition,
                 cameraLookXRotate,
                 cameraLookZRotate);
-        subScene.setCamera(camera);
         //root3D.getChildren().add(camera);
         root3D.getChildren().add(cameraXform);
         cameraXform.getChildren().add(cameraXform2);
@@ -191,81 +422,6 @@
         // camera.setTranslateZ(-cameraDistance);
         root3D.getChildren().add(autoScalingGroup);
 
-        // SCENE EVENT HANDLING FOR CAMERA NAV
-        subScene.addEventHandler(MouseEvent.ANY, new EventHandler<MouseEvent>() {
-            @Override public void handle(MouseEvent event) {
-                double yFlip = 1.0;
-                if (getYUp()) {
-                    yFlip = 1.0;
-                }
-                else {
-                    yFlip = -1.0;
-                }
-                if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
-                    dragStartX = event.getSceneX();
-                    dragStartY = event.getSceneY();
-                    dragStartRotateX = cameraXRotate.getAngle();
-                    dragStartRotateY = cameraYRotate.getAngle();
-                    mousePosX = event.getSceneX();
-                    mousePosY = event.getSceneY();
-                    mouseOldX = event.getSceneX();
-                    mouseOldY = event.getSceneY();
-
-                } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
-                    double xDelta = event.getSceneX() -  dragStartX;
-                    double yDelta = event.getSceneY() -  dragStartY;
-                    //cameraXRotate.setAngle(dragStartRotateX - (yDelta*0.7));
-                    //cameraYRotate.setAngle(dragStartRotateY + (xDelta*0.7));
-                    
-                    double modifier = 1.0;
-                    double modifierFactor = 0.3;
-
-                    if (event.isControlDown()) {
-                        modifier = 0.1;
-                    } 
-                    if (event.isShiftDown()) {
-                        modifier = 10.0;
-                    }     
-
-                    mouseOldX = mousePosX;
-                    mouseOldY = mousePosY;
-                    mousePosX = event.getSceneX();
-                    mousePosY = event.getSceneY();
-                    mouseDeltaX = (mousePosX - mouseOldX); //*DELTA_MULTIPLIER;
-                    mouseDeltaY = (mousePosY - mouseOldY); //*DELTA_MULTIPLIER;    
-                    
-                    double flip = -1.0;
-
-                    boolean alt = (true || event.isAltDown());
-                    if (alt && event.isPrimaryButtonDown()) {
-                        cameraXform.ry.setAngle(cameraXform.ry.getAngle() - yFlip*mouseDeltaX*modifierFactor*modifier*2.0);  // +
-                        cameraXform.rx.setAngle(cameraXform.rx.getAngle() + flip*mouseDeltaY*modifierFactor*modifier*2.0);  // -
-                    }
-                    else if (alt && event.isSecondaryButtonDown()) {
-                        double z = cameraPosition.getZ();
-                        // double z = camera.getTranslateZ();
-                        // double newZ = z + yFlip*flip*mouseDeltaX*modifierFactor*modifier;
-                        double newZ = z - flip*mouseDeltaX*modifierFactor*modifier;
-                        System.out.println("newZ = " + newZ);
-                        cameraPosition.setZ(newZ);
-                        // camera.setTranslateZ(newZ);
-                    }
-                    else if (alt && event.isMiddleButtonDown()) {
-                        cameraXform2.t.setX(cameraXform2.t.getX() + flip*mouseDeltaX*modifierFactor*modifier*0.3);  // -
-                        cameraXform2.t.setY(cameraXform2.t.getY() + yFlip*mouseDeltaY*modifierFactor*modifier*0.3);  // -
-                    }
-                }
-            }
-        });
-        subScene.addEventHandler(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
-            @Override public void handle(ScrollEvent event) {
-                double z = cameraPosition.getZ()-(event.getDeltaY()*0.2);
-                z = Math.max(z,-1000);
-                z = Math.min(z,0);
-                cameraPosition.setZ(z);
-            }
-        });
-
         SessionManager sessionManager = SessionManager.getSessionManager();
         sessionManager.bind(cameraLookXRotate.angleProperty(), "cameraLookXRotate");
         sessionManager.bind(cameraLookZRotate.angleProperty(), "cameraLookZRotate");
@@ -276,6 +432,43 @@
         sessionManager.bind(cameraYRotate.angleProperty(), "cameraYRotate");
         sessionManager.bind(camera.nearClipProperty(), "cameraNearClip");
         sessionManager.bind(camera.farClipProperty(), "cameraFarClip");
+
+        // Build SubScene
+        rebuildSubScene();
+    }
+
+    private void rebuildSubScene() {
+        SubScene oldSubScene = this.subScene.get();
+        if (oldSubScene != null) {
+            oldSubScene.setRoot(new Region());
+            oldSubScene.setCamera(null);
+            oldSubScene.removeEventHandler(MouseEvent.ANY, mouseEventHandler);
+            oldSubScene.removeEventHandler(KeyEvent.ANY, keyEventHandler);
+            oldSubScene.removeEventHandler(ScrollEvent.ANY, scrollEventHandler);
+        }
+
+        SubScene subScene = new SubScene(root3D,400,400,true,msaa.get());
+        this.subScene.set(subScene);
+        subScene.setFill(Color.ALICEBLUE);
+        subScene.setCamera(camera);
+        // SCENE EVENT HANDLING FOR CAMERA NAV
+        subScene.addEventHandler(MouseEvent.ANY, mouseEventHandler);
+        subScene.addEventHandler(KeyEvent.ANY, keyEventHandler);
+        // subScene.addEventFilter(KeyEvent.ANY, keyEventHandler);
+        subScene.addEventHandler(ZoomEvent.ANY, zoomEventHandler);
+        subScene.addEventHandler(ScrollEvent.ANY, scrollEventHandler);
+        
+        // Scene scene = subScene.getScene();
+        // scene.addEventFilter(KeyEvent.ANY, keyEventHandler);
+        
+        /*
+        subScene.sceneProperty().addListener(new ChangeListener() {
+            @Override
+            public void changed(ObservableValue ov, Object t, Object t1) {
+                System.out.println("hello world");
+            }
+        });
+        */
     }
 
     public boolean getAmbientLightEnabled() {
@@ -376,9 +569,7 @@
 
     {
         contentProperty().addListener(new ChangeListener<Node>() {
-
-            @Override
-            public void changed(ObservableValue<? extends Node> ov, Node oldContent, Node newContent) {
+            @Override public void changed(ObservableValue<? extends Node> ov, Node oldContent, Node newContent) {
                 autoScalingGroup.getChildren().remove(oldContent);
                 autoScalingGroup.getChildren().add(newContent);
                 setWireFrame(newContent,wireframe);
@@ -390,7 +581,23 @@
         });
     }
 
+    public boolean getMsaa() {
+        return msaa.get();
+    }
+
+    public SimpleBooleanProperty msaaProperty() {
+        return msaa;
+    }
+
+    public void setMsaa(boolean msaa) {
+        this.msaa.set(msaa);
+    }
+
     public SubScene getSubScene() {
+        return subScene.get();
+    }
+
+    public SimpleObjectProperty<SubScene> subSceneProperty() {
         return subScene;
     }
 
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/MainController.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/MainController.java	Tue Sep 24 10:09:57 2013 -0700
@@ -110,7 +110,7 @@
             // CREATE SETTINGS PANEL
             settingsPanel = FXMLLoader.load(MainController.class.getResource("settings.fxml"));
             // SETUP SPLIT PANE
-            splitPane.getItems().addAll(new SubSceneResizer(contentModel.getSubScene(),navigationPanel), settingsPanel);
+            splitPane.getItems().addAll(new SubSceneResizer(contentModel.subSceneProperty(),navigationPanel), settingsPanel);
             splitPane.getDividers().get(0).setPosition(1);
         } catch (IOException e) {
             e.printStackTrace();
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/NavigationController.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/NavigationController.java	Tue Sep 24 10:09:57 2013 -0700
@@ -41,9 +41,9 @@
  * Controller class for settings panel
  */
 public class NavigationController implements Initializable {
-    public FourWayNavControl eyeNav;
+//    public FourWayNavControl eyeNav;
     public ScrollBar zoomBar;
-    public FourWayNavControl camNav;
+//    public FourWayNavControl camNav;
     private ContentModel contentModel = Jfx3dViewerApp.getContentModel();
 
     @Override public void initialize(URL location, ResourceBundle resources) {
@@ -52,41 +52,41 @@
         zoomBar.setValue(contentModel.getCameraPosition().getZ());
         zoomBar.setVisibleAmount(5);
         contentModel.getCameraPosition().zProperty().bindBidirectional(zoomBar.valueProperty());
-        eyeNav.setListener(new FourWayNavControl.FourWayListener() {
-            @Override public void navigateStep(Side direction, double amount) {
-                switch (direction) {
-                    case TOP:
-                        contentModel.getCameraLookXRotate().setAngle(contentModel.getCameraLookXRotate().getAngle()+amount);
-                        break;
-                    case BOTTOM:
-                        contentModel.getCameraLookXRotate().setAngle(contentModel.getCameraLookXRotate().getAngle()-amount);
-                        break;
-                    case LEFT:
-                        contentModel.getCameraLookZRotate().setAngle(contentModel.getCameraLookZRotate().getAngle()-amount);
-                        break;
-                    case RIGHT:
-                        contentModel.getCameraLookZRotate().setAngle(contentModel.getCameraLookZRotate().getAngle()+amount);
-                        break;
-                }
-            }
-        });
-        camNav.setListener(new FourWayNavControl.FourWayListener() {
-            @Override public void navigateStep(Side direction, double amount) {
-                switch (direction) {
-                    case TOP:
-                        contentModel.getCameraXRotate().setAngle(contentModel.getCameraXRotate().getAngle()-amount);
-                        break;
-                    case BOTTOM:
-                        contentModel.getCameraXRotate().setAngle(contentModel.getCameraXRotate().getAngle()+amount);
-                        break;
-                    case LEFT:
-                        contentModel.getCameraYRotate().setAngle(contentModel.getCameraYRotate().getAngle()+amount);
-                        break;
-                    case RIGHT:
-                        contentModel.getCameraYRotate().setAngle(contentModel.getCameraYRotate().getAngle()-amount);
-                        break;
-                }
-            }
-        });
+//        eyeNav.setListener(new FourWayNavControl.FourWayListener() {
+//            @Override public void navigateStep(Side direction, double amount) {
+//                switch (direction) {
+//                    case TOP:
+//                        contentModel.getCameraLookXRotate().setAngle(contentModel.getCameraLookXRotate().getAngle()+amount);
+//                        break;
+//                    case BOTTOM:
+//                        contentModel.getCameraLookXRotate().setAngle(contentModel.getCameraLookXRotate().getAngle()-amount);
+//                        break;
+//                    case LEFT:
+//                        contentModel.getCameraLookZRotate().setAngle(contentModel.getCameraLookZRotate().getAngle()-amount);
+//                        break;
+//                    case RIGHT:
+//                        contentModel.getCameraLookZRotate().setAngle(contentModel.getCameraLookZRotate().getAngle()+amount);
+//                        break;
+//                }
+//            }
+//        });
+//        camNav.setListener(new FourWayNavControl.FourWayListener() {
+//            @Override public void navigateStep(Side direction, double amount) {
+//                switch (direction) {
+//                    case TOP:
+//                        contentModel.getCameraXRotate().setAngle(contentModel.getCameraXRotate().getAngle()-amount);
+//                        break;
+//                    case BOTTOM:
+//                        contentModel.getCameraXRotate().setAngle(contentModel.getCameraXRotate().getAngle()+amount);
+//                        break;
+//                    case LEFT:
+//                        contentModel.getCameraYRotate().setAngle(contentModel.getCameraYRotate().getAngle()+amount);
+//                        break;
+//                    case RIGHT:
+//                        contentModel.getCameraYRotate().setAngle(contentModel.getCameraYRotate().getAngle()-amount);
+//                        break;
+//                }
+//            }
+//        });
     }
 }
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SettingsController.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SettingsController.java	Tue Sep 24 10:09:57 2013 -0700
@@ -78,7 +78,7 @@
     public CheckBox showAxisCheckBox;
     public CheckBox yUpCheckBox;
     public Slider fovSlider;
-    public CheckBox scaleToFitCheckBox;
+    public CheckBox msaaCheckBox;
     public ColorPicker light1ColorPicker;
     public CheckBox ambientEnableCheckbox;
     public CheckBox light1EnabledCheckBox;
@@ -130,7 +130,7 @@
             }
         });
         // wire up settings in OPTIONS
-        contentModel.getAutoScalingGroup().enabledProperty().bind(scaleToFitCheckBox.selectedProperty());
+        contentModel.msaaProperty().bind(msaaCheckBox.selectedProperty());
         contentModel.showAxisProperty().bind(showAxisCheckBox.selectedProperty());
         contentModel.yUpProperty().bind(yUpCheckBox.selectedProperty());
         backgroundColorPicker.setValue((Color)contentModel.getSubScene().getFill());
@@ -369,7 +369,7 @@
 
         sessionManager.bind(showAxisCheckBox.selectedProperty(), "showAxis");
         sessionManager.bind(yUpCheckBox.selectedProperty(), "yUp");
-        sessionManager.bind(scaleToFitCheckBox.selectedProperty(), "scaleToFit");
+        sessionManager.bind(msaaCheckBox.selectedProperty(), "msaa");
         sessionManager.bind(wireFrameCheckbox.selectedProperty(), "wireFrame");
         sessionManager.bind(backgroundColorPicker.valueProperty(), "backgroundColor");
         sessionManager.bind(fovSlider.valueProperty(), "fieldOfView");
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SubSceneResizer.java	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SubSceneResizer.java	Tue Sep 24 10:09:57 2013 -0700
@@ -31,6 +31,7 @@
  */
 package com.javafx.experiments.jfx3dviewer;
 
+import javafx.beans.property.ObjectProperty;
 import javafx.scene.Node;
 import javafx.scene.SubScene;
 import javafx.scene.layout.Pane;
@@ -39,7 +40,7 @@
  * Resizable container for a SubScene
  */
 public class SubSceneResizer extends Pane {
-    private final SubScene subScene;
+    private SubScene subScene;
     private final Node controlsPanel;
 
     public SubSceneResizer(SubScene subScene, Node controlsPanel) {
@@ -51,11 +52,36 @@
         getChildren().addAll(subScene, controlsPanel);
     }
 
+    public SubSceneResizer(ObjectProperty<SubScene> subScene, Node controlsPanel) {
+        this.subScene = subScene.get();
+        this.controlsPanel = controlsPanel;
+        if (this.subScene != null) {
+            setPrefSize(this.subScene.getWidth(),this.subScene.getHeight());
+            getChildren().add(this.subScene);
+        }
+        subScene.addListener((o,old,newSubScene) -> {
+            this.subScene = newSubScene;
+            if (this.subScene != null) {
+                setPrefSize(this.subScene.getWidth(),this.subScene.getHeight());
+                if (getChildren().size() == 1) {
+                    getChildren().add(0,this.subScene);
+                } else {
+                    getChildren().set(0,this.subScene);
+                }
+            }
+        });
+        setMinSize(50,50);
+        setMaxSize(Double.MAX_VALUE,Double.MAX_VALUE);
+        getChildren().add(controlsPanel);
+    }
+
     @Override protected void layoutChildren() {
         final double width = getWidth();
         final double height = getHeight();
-        subScene.setWidth(width);
-        subScene.setHeight(height);
+        if (subScene!=null) {
+            subScene.setWidth(width);
+            subScene.setHeight(height);
+        }
         final int controlsWidth = (int)snapSize(controlsPanel.prefWidth(-1));
         final int controlsHeight = (int)snapSize(controlsPanel.prefHeight(-1));
         controlsPanel.resizeRelocate(width-controlsWidth,0,controlsWidth,controlsHeight);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/utils3d/DragSupport.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * 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.
+ *  - Redistributions 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 Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.utils3d;
+
+
+import javafx.beans.property.Property;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.scene.Scene;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+
+
+/**
+ * Utility class that binds simple mouse gestures to number properties so that
+ * their values can be controlled with mouse drag events.
+ */
+public class DragSupport {
+    public EventHandler<KeyEvent> keyboardEventHandler;
+    public EventHandler<MouseEvent> mouseEventHandler;
+    private Number anchor;
+    private double dragAnchor;
+    private MouseEvent lastMouseEvent;
+    private Scene target;
+
+    /**
+     * Creates DragSupport instance that attaches EventHandlers to the given scene 
+     * and responds to mouse and keyboard events in order to change given 
+     * property values according to mouse drag events of given orientation
+     * @param target scene
+     * @param modifier null if no modifier needed
+     * @param orientation vertical or horizontal
+     * @param property number property to control
+     * @see #DragSupport(javafx.scene.Scene, javafx.scene.input.KeyCode, javafx.geometry.Orientation, javafx.beans.property.Property, double) 
+     */
+    public DragSupport(Scene target, final KeyCode modifier, final Orientation orientation, final Property<Number> property) {
+        this(target, modifier, MouseButton.PRIMARY, orientation, property, 1);
+    }
+    
+    public DragSupport(Scene target, final KeyCode modifier, MouseButton mouseButton, final Orientation orientation, final Property<Number> property) {
+        this(target, modifier, mouseButton, orientation, property, 1);
+    }
+
+    /**
+     * Removes event handlers of this DragSupport instance from the target scene
+     */
+    public void detach() {
+        target.removeEventHandler(MouseEvent.ANY, mouseEventHandler);
+        target.removeEventHandler(KeyEvent.ANY, keyboardEventHandler);
+    }
+
+    /**
+     * Creates DragSupport instance that attaches EventHandlers to the given scene 
+     * and responds to mouse and keyboard events in order to change given 
+     * property values according to mouse drag events of given orientation.
+     * Mouse movement amount is multiplied by given factor.
+     * @param target scene
+     * @param modifier null if no modifier needed
+     * @param orientation vertical or horizontal
+     * @param property number property to control
+     * @param factor multiplier for mouse movement amount
+     */
+    public DragSupport(Scene target, final KeyCode modifier, final Orientation orientation, final Property<Number> property, final double factor) {
+        this(target, modifier, MouseButton.PRIMARY, orientation, property, factor);
+    }
+
+    public DragSupport(Scene target, final KeyCode modifier, final MouseButton mouseButton, final Orientation orientation, final Property<Number> property, final double factor) {
+        this.target = target;
+        mouseEventHandler = new EventHandler<MouseEvent>() {
+            
+            @Override
+            public void handle(MouseEvent t) {
+                if (t.getEventType() != MouseEvent.MOUSE_ENTERED_TARGET
+                        && t.getEventType() != MouseEvent.MOUSE_EXITED_TARGET) {
+                    lastMouseEvent = t;
+                }
+                if (t.getEventType() == MouseEvent.MOUSE_PRESSED) {
+                    if (t.getButton() == mouseButton
+                            && isModifierCorrect(t, modifier)) {
+                        anchor = property.getValue();
+                        dragAnchor = getCoord(t, orientation);
+                        t.consume();
+                    }
+                } else if (t.getEventType() == MouseEvent.MOUSE_DRAGGED) {
+                    if (t.getButton() == mouseButton
+                            && isModifierCorrect(t, modifier)) {
+                        property.setValue(anchor.doubleValue()
+                                + (getCoord(t, orientation) - dragAnchor) * factor);
+                        t.consume();
+                    }
+                }
+            }
+        };
+        keyboardEventHandler = new EventHandler<KeyEvent>() {
+            
+            @Override
+            public void handle(KeyEvent t) {
+                if (t.getEventType() == KeyEvent.KEY_PRESSED) {
+                    if (t.getCode() == modifier) {
+                        anchor = property.getValue();
+                        if (lastMouseEvent != null) {
+                            dragAnchor = getCoord(lastMouseEvent, orientation);
+                        }
+                        t.consume();
+                    }
+                } else if (t.getEventType() == KeyEvent.KEY_RELEASED) {
+                    if (t.getCode() != modifier && isModifierCorrect(t, modifier)) {
+                        anchor = property.getValue();
+                        if (lastMouseEvent != null) {
+                            dragAnchor = getCoord(lastMouseEvent, orientation);
+                        }
+                        t.consume();
+                    }
+                }
+            }
+        };
+        target.addEventHandler(MouseEvent.ANY, mouseEventHandler);
+        target.addEventHandler(KeyEvent.ANY, keyboardEventHandler);
+    }
+
+    private boolean isModifierCorrect(KeyEvent t, KeyCode keyCode) {
+        return (keyCode != KeyCode.ALT ^ t.isAltDown()) 
+                && (keyCode != KeyCode.CONTROL ^ t.isControlDown()) 
+                && (keyCode != KeyCode.SHIFT ^ t.isShiftDown()) 
+                && (keyCode != KeyCode.META ^ t.isMetaDown());
+    }
+
+    private boolean isModifierCorrect(MouseEvent t, KeyCode keyCode) {
+        return (keyCode != KeyCode.ALT ^ t.isAltDown()) 
+                && (keyCode != KeyCode.CONTROL ^ t.isControlDown()) 
+                && (keyCode != KeyCode.SHIFT ^ t.isShiftDown()) 
+                && (keyCode != KeyCode.META ^ t.isMetaDown());
+    }
+
+    private double getCoord(MouseEvent t, Orientation orientation) {
+        switch (orientation) {
+            case HORIZONTAL:
+                return t.getScreenX();
+            case VERTICAL:
+                return t.getScreenY();
+            default:
+                throw new IllegalArgumentException("This orientation is not supported: " + orientation);
+        }
+    }
+    
+}
--- a/apps/experiments/3DViewer/src/main/resources/com/javafx/experiments/jfx3dviewer/navigation.fxml	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/resources/com/javafx/experiments/jfx3dviewer/navigation.fxml	Tue Sep 24 10:09:57 2013 -0700
@@ -44,8 +44,8 @@
 
 <VBox id="controller" alignment="CENTER" spacing="10.0" xmlns:fx="http://javafx.com/fxml" fx:controller="com.javafx.experiments.jfx3dviewer.NavigationController">
   <children>
-    <FourWayNavControl fx:id="eyeNav" />
-    <FourWayNavControl fx:id="camNav" />
+    <!--<FourWayNavControl fx:id="eyeNav" />-->
+    <!--<FourWayNavControl fx:id="camNav" />-->
     <ScrollBar fx:id="zoomBar" orientation="VERTICAL" rotate="180.0" />
   </children>
   <padding>
--- a/apps/experiments/3DViewer/src/main/resources/com/javafx/experiments/jfx3dviewer/settings.fxml	Thu Sep 19 07:57:58 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/resources/com/javafx/experiments/jfx3dviewer/settings.fxml	Tue Sep 24 10:09:57 2013 -0700
@@ -70,8 +70,8 @@
             <Label text="Y Up:" GridPane.columnIndex="0" GridPane.rowIndex="1" />
             <CheckBox fx:id="showAxisCheckBox" mnemonicParsing="false" text="" GridPane.columnIndex="1" GridPane.rowIndex="0" />
             <CheckBox fx:id="yUpCheckBox" mnemonicParsing="false" selected="true" text="" GridPane.columnIndex="1" GridPane.rowIndex="1" />
-            <Label text="Scale to Fit" GridPane.columnIndex="0" GridPane.rowIndex="2" />
-            <CheckBox id="yUpCheckBox" fx:id="scaleToFitCheckBox" mnemonicParsing="false" selected="false" text="" GridPane.columnIndex="1" GridPane.rowIndex="2" />
+            <Label text="MSAA Antialiasing" GridPane.columnIndex="0" GridPane.rowIndex="2" />
+            <CheckBox id="msaaCheckBox" fx:id="msaaCheckBox" mnemonicParsing="false" selected="true" text="" GridPane.columnIndex="1" GridPane.rowIndex="2" />
             <Label text="Background Color" GridPane.columnIndex="0" GridPane.rowIndex="3" />
             <ColorPicker fx:id="backgroundColorPicker" GridPane.columnIndex="1" GridPane.rowIndex="3" />
             <Label text="Wireframe" GridPane.columnIndex="0" GridPane.rowIndex="4" />
@@ -187,29 +187,29 @@
                 <Label text="Lock to Camera" GridPane.columnIndex="0" GridPane.rowIndex="6" />
                 <CheckBox fx:id="light1followCameraCheckBox" mnemonicParsing="false" selected="true" text="" GridPane.columnIndex="1" GridPane.rowIndex="6" />
                 <Label text="X" GridPane.columnIndex="0" GridPane.rowIndex="7" />
-                <Slider fx:id="light1x" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="7" />
+                <Slider fx:id="light1x" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="7" />
                 <Label text="Y" GridPane.columnIndex="0" GridPane.rowIndex="8" />
-                <Slider fx:id="light1y" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="8" />
+                <Slider fx:id="light1y" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="8" />
                 <Label text="Z" GridPane.columnIndex="0" GridPane.rowIndex="9" />
-                <Slider fx:id="light1z" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="9" />
+                <Slider fx:id="light1z" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="9" />
                 <Label maxWidth="1.7976931348623157E308" styleClass="settings-header" text="Light 2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="10" />
                 <Label text="Enabled" GridPane.columnIndex="0" GridPane.rowIndex="11" />
                 <CheckBox fx:id="light2EnabledCheckBox" mnemonicParsing="false" text="" GridPane.columnIndex="1" GridPane.rowIndex="11" />
                 <Label text="X" GridPane.columnIndex="0" GridPane.rowIndex="13" />
-                <Slider fx:id="light2x" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="13" />
+                <Slider fx:id="light2x" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="13" />
                 <Label text="Y" GridPane.columnIndex="0" GridPane.rowIndex="14" />
-                <Slider fx:id="light2y" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="14" />
+                <Slider fx:id="light2y" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="14" />
                 <Label text="Z" GridPane.columnIndex="0" GridPane.rowIndex="15" />
-                <Slider fx:id="light2z" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="15" />
+                <Slider fx:id="light2z" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="15" />
                 <Label maxWidth="1.7976931348623157E308" styleClass="settings-header" text="Light 3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="16" />
                 <Label text="Enabled" GridPane.columnIndex="0" GridPane.rowIndex="17" />
                 <CheckBox fx:id="light3EnabledCheckBox" mnemonicParsing="false" text="" GridPane.columnIndex="1" GridPane.rowIndex="17" />
                 <Label text="Lock to Camera" GridPane.columnIndex="0" GridPane.rowIndex="19" />
-                <Slider fx:id="light3x" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="19" />
+                <Slider fx:id="light3x" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="19" />
                 <Label text="Y" GridPane.columnIndex="0" GridPane.rowIndex="20" />
-                <Slider fx:id="light3y" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="20" />
+                <Slider fx:id="light3y" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="20" />
                 <Label text="Z" GridPane.columnIndex="0" GridPane.rowIndex="21" />
-                <Slider fx:id="light3z" majorTickUnit="20.0" max="20.0" min="-20.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="21" />
+                <Slider fx:id="light3z" majorTickUnit="20.0" max="100.0" min="-100.0" minorTickCount="5" showTickLabels="true" showTickMarks="false" value="0.0" GridPane.columnIndex="1" GridPane.rowIndex="21" />
                 <Label text="Color:" GridPane.columnIndex="0" GridPane.rowIndex="12" />
                 <ColorPicker fx:id="light2ColorPicker" GridPane.columnIndex="1" GridPane.rowIndex="12" />
                 <Label text="Color" GridPane.columnIndex="0" GridPane.rowIndex="18" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/nb-configuration.xml	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project-shared-configuration>
+    <!--
+This file contains additional configuration written by modules in the NetBeans IDE.
+The configuration is intended to be shared among all the users of project and
+therefore it is assumed to be part of version control checkout.
+Without this configuration present, some functionality in the IDE may be limited or fail altogether.
+-->
+    <properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
+        <!--
+Properties that influence various parts of the IDE, especially code formatting and the like. 
+You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
+That way multiple projects can share the same settings (useful for formatting rules for example).
+Any value defined here will override the pom.xml file value but is only applicable to the current project.
+-->
+        <netbeans.hint.j2eeVersion>1.7-web</netbeans.hint.j2eeVersion>
+    </properties>
+</project-shared-configuration>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/pom.xml	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.oracle.chess</groupId>
+    <artifactId>ChessLibrary</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <parent>
+        <groupId>com.oracle.chess</groupId>
+        <artifactId>Chess</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <name>ChessLibrary</name>
+
+    <properties>
+        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    
+    <dependencies>
+        <dependency>
+            <groupId>javax</groupId>
+            <artifactId>javaee-web-api</artifactId>
+            <version>7.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.10</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>javax.json</artifactId>
+            <version>1.0.1</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                    <compilerArguments>
+                        <endorseddirs>${endorsed.dir}</endorseddirs>
+                    </compilerArguments>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.3</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <version>2.6</version>
+                <executions>
+                    <execution>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${endorsed.dir}</outputDirectory>
+                            <silent>true</silent>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>javax</groupId>
+                                    <artifactId>javaee-endorsed-api</artifactId>
+                                    <version>7.0</version>
+                                    <type>jar</type>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Bishop.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Bishop class.
+ *
+ */
+public final class Bishop extends Piece {
+
+    protected Bishop(Color color) {
+        super(color);
+    }
+
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        return Math.abs(x2 - x1) == Math.abs(y2 - y1);
+    }
+
+    @Override
+    public String toNotation() {
+        return "B";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        if (!isValidMove(from, to)) {
+            throw new GameException(this, from, to);
+        }
+
+        int x, y;
+        final List<Point> path = new ArrayList<>();
+        if (from.getX() >= to.getX()) {
+            if (from.getY() >= to.getY()) {
+                // (-,-)
+                for (x = from.getX() - 1, y = from.getY() - 1; x > to.getX() && y > to.getY(); x--, y--) {
+                    path.add(Point.fromXY(x, y));
+                }
+            } else {
+                // (-,+)
+                for (x = from.getX() - 1, y = from.getY() + 1; x > to.getX() && y < to.getY(); x--, y++) {
+                    path.add(Point.fromXY(x, y));
+                }
+            }
+        } else {
+            if (from.getY() >= to.getY()) {
+                // (+,-)
+                for (x = from.getX() + 1, y = from.getY() - 1; x < to.getX() && y > to.getY(); x++, y--) {
+                    path.add(Point.fromXY(x, y));
+                }
+            } else {
+                // (+,+)
+                for (x = from.getX() + 1, y = from.getY() + 1; x < to.getX() && y < to.getY(); x++, y++) {
+                    path.add(Point.fromXY(x, y));
+                }
+            }
+        }
+        return path;
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        int x, y;
+        final List<Point> moves = new ArrayList<>();
+
+        // (+, +)
+        x = from.getX() + 1;
+        y = from.getY() + 1;
+        while (x < Board.N_SQUARES && y < Board.N_SQUARES) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x++; y++;
+        }
+        // (+, -)
+        x = from.getX() + 1;
+        y = from.getY() - 1;
+        while (x < Board.N_SQUARES && y >= 0) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x++; y--;
+        }
+        // (-, +)
+        x = from.getX() - 1;
+        y = from.getY() + 1;
+        while (x >= 0 && y < Board.N_SQUARES) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x--; y++;
+        }
+        // (-, -)
+        x = from.getX() - 1;
+        y = from.getY() - 1;
+        while (x >= 0 && y >= 0) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x--; y--;
+        }
+        return moves;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Board.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,407 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import static com.oracle.chess.model.Piece.*;
+
+/**
+ * Board class.
+ *
+ */
+public final class Board {
+
+    public static final int N_SQUARES = 8;
+
+    private static final Piece[] WHITE_ROW = {
+        WHITE_ROOK, WHITE_KNIGHT, WHITE_BISHOP, WHITE_QUEEN,
+        WHITE_KING, WHITE_BISHOP, WHITE_KNIGHT, WHITE_ROOK };
+
+    private static final Piece[] BLACK_ROW = {
+        BLACK_ROOK, BLACK_KNIGHT, BLACK_BISHOP, BLACK_QUEEN,
+        BLACK_KING, BLACK_BISHOP, BLACK_KNIGHT, BLACK_ROOK };
+
+    /**
+     * Board is comprised of 8x8 squares.
+     */
+    private Square[][] squares = new Square[N_SQUARES][N_SQUARES];
+
+    /**
+     * Square of white king on the board.
+     */
+    private Square whiteKing;
+
+    /**
+     * Square of black king on the board.
+     */
+    private Square blackKing;
+
+    /**
+     * List of moves made so far.
+     */
+    private List<Move> moves = new ArrayList<>();
+
+    public Board() {
+        initialize();
+    }
+
+    public void initialize() {
+        // Init white pieces on board
+        for (int x = 0; x < N_SQUARES; x++) {
+            squares[x][0] = new Square(x, 0, WHITE_ROW[x]);
+            if (WHITE_ROW[x] == WHITE_KING) {
+                whiteKing = squares[x][0];
+            }
+        }
+        for (int x = 0; x < N_SQUARES; x++) {
+            squares[x][1] = new Square(x, 1, WHITE_PAWN);
+        }
+
+        // Init black pieces on board
+        for (int x = 0; x < N_SQUARES; x++) {
+            squares[x][7] = new Square(x, 7, BLACK_ROW[x]);
+            if (BLACK_ROW[x] == BLACK_KING) {
+                blackKing = squares[x][7];
+            }
+        }
+        for (int x = 0; x < N_SQUARES; x++) {
+            squares[x][6] = new Square(x, 6, BLACK_PAWN);
+        }
+
+        // Init all other empty squares
+        for (int y = 2; y <= 5; y++) {
+            for (int x = 0; x < N_SQUARES; x++) {
+                squares[x][y] = new Square(x, y);
+            }
+        }
+    }
+
+    public List<Move> getMoves() {
+        return moves;
+    }
+
+    public void setMoves(List<Move> moves) {
+        this.moves = moves;
+    }
+
+    public Move getLastMove() {
+        return moves.isEmpty() ? null : moves.get(moves.size() - 1);
+    }
+
+    public void doMove(Move move) {
+        final Point from = move.getFrom();
+        final Point to = move.getTo();
+        final Piece piece = move.getPiece();
+
+        // Carry out the move on the board
+        squares[from.getX()][from.getY()].setPiece(null);
+        move.setCaptured(squares[to.getX()][to.getY()].getPiece());
+        squares[to.getX()][to.getY()].setPiece(piece);
+
+        // Keep track of the kings
+        if (piece == WHITE_KING) {
+            whiteKing = squares[to.getX()][to.getY()];
+        } else if (piece == BLACK_KING) {
+            blackKing = squares[to.getX()][to.getY()];
+        }
+
+        // Check for castling first
+        if (move.isLeftCastling()) {
+            if (piece.getColor() == Color.W) {
+                setPiece(null, King.W_LEFT_ROOK);
+                setPiece(Piece.WHITE_ROOK, to.incrementX(1));
+            } else {
+                setPiece(null, King.B_LEFT_ROOK);
+                setPiece(Piece.BLACK_ROOK, to.incrementX(1));
+            }
+        } else if (move.isRightCastling()) {
+            if (piece.getColor() == Color.W) {
+                setPiece(null, King.W_RIGHT_ROOK);
+                setPiece(Piece.WHITE_ROOK, to.decrementX(1));
+            } else {
+                setPiece(null, King.B_RIGHT_ROOK);
+                setPiece(Piece.BLACK_ROOK, to.decrementX(1));
+            }
+        } else {
+            // Check for pawn promotions
+            if (piece.isPromoted(to)) {
+                setPiece(piece.getColor().getQueen(), to);
+                move.setPromoted(true);
+            }
+
+            // En passant?
+            final Move lastMove = getLastMove();
+            if (move.isEnPassantAllowed(lastMove)) {
+                final Point lastTo = lastMove.getTo();
+                move.setEnPassant(true);
+                move.setCaptured(squares[lastTo.getX()][lastTo.getY()].getPiece());
+                squares[lastTo.getX()][lastTo.getY()].setPiece(null);
+            }
+        }
+
+        // Record last move
+        moves.add(move);
+    }
+
+    public void undoLastMove() {
+        // Check that we have a move to undo
+        if (moves.isEmpty()) {
+            throw new InternalError("No move available to undo");
+        }
+
+        final Move lastMove = getLastMove();
+        final Point from = lastMove.getFrom();
+        final Point to = lastMove.getTo();
+        final Piece piece = lastMove.getPiece();
+
+        squares[from.getX()][from.getY()].setPiece(piece);
+
+        if (lastMove.isLeftCastling()) {
+            squares[to.getX()][to.getY()].setPiece(null);
+            if (piece.getColor() == Color.W) {
+                setPiece(Piece.WHITE_ROOK, King.W_LEFT_ROOK);
+                setPiece(null, to.incrementX(1));
+            } else {
+                setPiece(Piece.BLACK_ROOK, King.B_LEFT_ROOK);
+                setPiece(null, to.incrementX(1));
+            }
+        } else if (lastMove.isRightCastling()) {
+            squares[to.getX()][to.getY()].setPiece(null);
+            if (piece.getColor() == Color.W) {
+                setPiece(Piece.WHITE_ROOK, King.W_RIGHT_ROOK);
+                setPiece(null, to.decrementX(1));
+            } else {
+                setPiece(Piece.BLACK_ROOK, King.B_RIGHT_ROOK);
+                setPiece(null, to.decrementX(1));
+            }
+        } else {
+            final Piece captured = lastMove.getCaptured();
+
+            // Undoing an en passant move?
+            if (lastMove.isEnPassant()) {
+                if (captured.getColor() == Color.B) {
+                    squares[to.getX()][to.getY() - 1].setPiece(captured);
+                } else {
+                    squares[to.getX()][to.getY() + 1].setPiece(captured);
+                }
+                squares[to.getX()][to.getY()].setPiece(null);
+            } else {
+                squares[to.getX()][to.getY()].setPiece(captured);
+            }
+
+            // Keep track of the kings
+            if (piece == WHITE_KING) {
+                whiteKing = squares[from.getX()][from.getY()];
+            } else if (piece == BLACK_KING) {
+                blackKing = squares[from.getX()][from.getY()];
+            }
+        }
+
+        // Remove move from history
+        moves.remove(moves.size() - 1);
+    }
+
+    public boolean hasPiecedMoved(Point from) {
+        for (Move move : moves) {
+            if (move.getFrom().equals(from)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public Piece getPiece(Point p) {
+        return squares[p.getX()][p.getY()].getPiece();
+    }
+    
+    public boolean hasPiece(Point p) {
+        return squares[p.getX()][p.getY()].getPiece() != null;
+    }
+
+    public Square getSquare(Point p) {
+        return squares[p.getX()][p.getY()];
+    }
+
+    public boolean hasColoredPiece(Point p, Color color) {
+        return hasPiece(p) && getPiece(p).getColor() == color;
+    }
+
+    public Square getKingSquare(Color color) {
+        return color == Color.W ? whiteKing : blackKing;
+    }
+
+    public void clear() {
+        for (int x = 0; x < N_SQUARES; x++) {
+            for (int y = 0; y < N_SQUARES; y++) {
+                squares[x][y] = new Square(x, y);
+            }
+        }
+
+        // Need at least the kings on the board
+        final Point wk = Point.fromXY(4, 0);
+        whiteKing = squares[wk.getX()][wk.getY()] = new Square(wk, WHITE_KING);
+        final Point bk = Point.fromXY(4, 7);
+        blackKing = squares[bk.getX()][bk.getY()] = new Square(bk, BLACK_KING);
+    }
+
+    public void setPiece(Piece piece, Point p) {
+        final Square sq = squares[p.getX()][p.getY()];
+        sq.setPiece(piece);
+        
+        // Keep track of the kings
+        if (piece == WHITE_KING && whiteKing != sq) {
+            whiteKing.setPiece(null);
+            whiteKing = sq;
+        } else if (piece == BLACK_KING && blackKing != sq) {
+            blackKing.setPiece(null);
+            blackKing = sq;
+        }
+    }
+
+    public Iterator<Square> getIterator(final Color filter) {
+        return new Iterator<Square>() {
+            private static final int TOTAL_SQUARES = N_SQUARES * N_SQUARES;
+
+            private Color color = filter;
+
+            private int lastK = 0;
+
+            private Square next = null;
+
+            private Square findNext() {
+                int k;
+                for (k = lastK; k < TOTAL_SQUARES; k++) {
+                    final Point p = Point.fromXY(k % N_SQUARES, k / N_SQUARES);
+                    if (hasColoredPiece(p, color)) {
+                        lastK = k + 1;
+                        return getSquare(p);
+                    }
+                }
+                lastK = k;
+                return null;
+            }
+
+            @Override
+            public boolean hasNext() {
+                if (next == null) {
+                    next = findNext();
+                }
+                return next != null;
+            }
+
+            @Override
+            public Square next() {
+                if (next == null) {
+                    findNext();
+                    if (next == null) {
+                        throw new NoSuchElementException("No more pieces on the board");
+                    }
+                }
+                final Square result = next;
+                next = null;
+                return result;
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+        };
+    }
+
+    List<Point> queryMoves(Point from) {
+        final Piece piece = getPiece(from);
+        return piece.generateMoves(from, this);
+    }
+
+    /**
+     * Determines if the king of <code>color</code> is in check or not.
+     *
+     * @param color King's color.
+     * @return Result of in-check test.
+     */
+    public boolean isKingAttacked(Color color) {
+        final Point toKing = getKingSquare(color).getPoint();
+        return kingAttackers(color, toKing) != null;
+    }
+
+    /**
+     * Determines if the king of <code>color</code> is in check or not
+     * after moving to <code>toKing</code>.
+     *
+     * @param color King's color.
+     * @param toKing King's location on the board.
+     * @return Result of in-check test.
+     */
+    public boolean isKingAttacked(Color color, Point toKing) {
+        return kingAttackers(color, toKing) != null;
+    }
+
+    /**
+     * Returns the list of pieces that are attacking the king of <code>color</code> or
+     * <code>null</null> if king is not in check.
+     *
+     * @param color King's color.
+     * @param toKing King's location on the board.
+     * @return The list of pieces attacking king or <code>null</code> if not in check.
+     */
+    public List<Square> kingAttackers(Color color, Point toKing) {
+        List<Square> result = null;
+        final Color opponent = color.getOpponentColor();
+        Iterator<Square> iter = getIterator(opponent);
+        while (iter.hasNext()) {
+            final Square square = iter.next();
+            if (square.getPiece().isLegalMove(square.getPoint(), toKing, this)) {
+                if (result == null) {
+                    result = new ArrayList<>();
+                }
+                result.add(square);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer();
+        sb.append("+---+---+---+---+---+---+---+---+\n");
+        for (int y = N_SQUARES - 1; y >=0; y--) {
+            sb.append("|");
+            for (int x = 0; x < N_SQUARES; x++) {
+                sb.append(squares[x][y]).append("|");
+            }
+            sb.append("\n");
+            sb.append("+---+---+---+---+---+---+---+---+\n");
+        }
+        return sb.toString();
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Color.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+/**
+ * Color enumeration.
+ *
+ */
+public enum Color {
+
+    W {
+        @Override
+        public King getKing() {
+            return Piece.WHITE_KING;
+        }
+
+        @Override
+        public Queen getQueen() {
+            return Piece.WHITE_QUEEN;
+        }
+
+        @Override
+        public Bishop getBishop() {
+            return Piece.WHITE_BISHOP;
+        }
+
+        @Override
+        public Rook getRook() {
+            return Piece.WHITE_ROOK;
+        }
+
+        @Override
+        public Color getOpponentColor() {
+            return B;
+        }
+
+        @Override
+        public String toString() {
+            return "W";
+        }
+    },
+    B {
+        @Override
+        public King getKing() {
+            return Piece.BLACK_KING;
+        }
+        
+        @Override
+        public Queen getQueen() {
+            return Piece.BLACK_QUEEN;
+        }
+
+        @Override
+        public Bishop getBishop() {
+            return Piece.BLACK_BISHOP;
+        }
+
+        @Override
+        public Rook getRook() {
+            return Piece.BLACK_ROOK;
+        }
+
+        @Override
+        public Color getOpponentColor() {
+            return W;
+        }
+
+        @Override
+        public String toString() {
+            return "B";
+        }        
+    };
+
+    public abstract King getKing();
+    
+    public abstract Queen getQueen();
+
+    public abstract Bishop getBishop();
+
+    public abstract Rook getRook();
+
+    public abstract Color getOpponentColor();
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Game.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,789 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Iterator;
+import java.util.Collections;
+import java.util.Set;
+import java.util.Objects;
+import java.util.UUID;
+
+import static com.oracle.chess.model.GameException.ErrorCode.*;
+
+/**
+ * Game class.
+ *
+ * @param <P> Type of a game player.
+ * @param <O> Type of a game observer.
+ */
+public final class Game<P, O> {
+
+    public enum State {
+        PLAYING("Game being played"),
+        DRAW("Game ended by draw"),
+        WHITE_WINS("White player wins"),
+        BLACK_WINS("Black player wins");
+        
+        private String msg;
+        
+        State(String msg) {
+            this.msg = msg;
+        }
+        
+        @Override
+        public String toString() {
+            return msg;
+        }
+    };
+
+    private Board board;
+
+    private Color turn;
+
+    private Color startTurn;
+
+    private String summary;
+
+    private String gameId;
+
+    private P whitePlayer;
+
+    private P blackPlayer;
+
+    private P drawRequester;
+
+    private List<O> observers = new ArrayList<>();
+
+    private State state = State.PLAYING;
+
+    private GameWatcher<P, O> watcher;
+
+    private long creationStamp;
+
+    private long updateStamp;
+
+    public Game() {
+        this(new Board(), Color.W);
+    }
+
+    public Game(Color turn, String summary) {
+        this(new Board(), turn, summary);
+    }
+
+    public Game(Board board, Color turn) {
+        this(board, turn, null);
+    }
+
+    public Game(Board board, Color turn, String summary) {
+        this.board = board != null ? board : new Board();
+        this.turn = this.startTurn = turn;
+        this.summary = summary;
+        creationStamp = updateStamp = System.currentTimeMillis();
+        generateGameId();
+    }
+
+    /**
+     * Generates a unique ID for this game.
+     */
+    public void generateGameId() {
+        gameId = UUID.randomUUID().toString();
+    }
+
+    /**
+     * Gets the color that started the game. This is used when an initial
+     * board is specified (mostly for testing).
+     *
+     * @return Start color.
+     */
+    public synchronized Color getStartTurn() {
+        return startTurn;
+    }
+
+    /**
+     * Sets the start color for the game. This is used when an initial board
+     * is specified (mostly for testing).
+     *
+     * @param startTurn Start color.
+     */
+    public synchronized void setStartTurn(Color startTurn) {
+        if (watcher != null) {
+            watcher.setStartTurn(this, startTurn);
+        }
+        this.startTurn = startTurn;
+    }
+
+    /**
+     * Sets a player for a color.
+     *
+     * @param color The color.
+     * @param player The player.
+     */
+    public synchronized void setPlayer(Color color, P player) {
+        if (watcher != null) {
+            watcher.setPlayer(this, color, player);
+        }
+        if (color == Color.W) {
+            whitePlayer = player;
+        } else {
+            blackPlayer = player;
+        }
+    }
+
+    /**
+     * Gets a player of a certain color.
+     *
+     * @param color The color.
+     * @return The player of that color or <code>null</code> if no player exists.
+     */
+    public synchronized P getPlayer(Color color) {
+        return color == Color.W ? whitePlayer : blackPlayer;
+    }
+
+    /**
+     * Determines if there is a player of a certain color.
+     *
+     * @param color The color.
+     * @return Outcome of test.
+     */
+    public synchronized boolean hasPlayer(Color color) {
+        return color == Color.W ? whitePlayer != null : blackPlayer != null;
+    }
+
+    /**
+     * Returns the color of a player.
+     *
+     * @param player The player.
+     * @return Color of player or <code>null</code> if player is unknown.
+     */
+    public synchronized Color getPlayerColor(P player) {
+        return player.equals(whitePlayer) ? Color.W : player.equals(blackPlayer) ? Color.B : null;
+    }
+
+    /**
+     * Adds an observer to this game.
+     *
+     * @param observer The observer.
+     */
+    public synchronized void addObserver(O observer) {
+        observers.add(observer);
+    }
+
+    /**
+     * Determines if observer is in the game.
+     *
+     * @param observer The observer.
+     */
+    public synchronized void hasObserver(O observer) {
+        observers.contains(observer);
+    }
+
+    /**
+     * Removes an observer from a game.
+     *
+     * @param observer The observer.
+     * @return Boolean indicating if observer was found and removed.
+     */
+    public synchronized boolean removeObserver(O observer) {
+        return observers.remove(observer);
+    }
+
+    /**
+     * Gets a list of current observers.
+     *
+     * @return List of observers.
+     */
+    public synchronized List<O> getObservers() {
+        return observers;
+    }
+
+    /**
+     * Returns the opponent of a given player.
+     *
+     * @param player The player.
+     * @return Opponent or <code>null</code> if it doesn't exist.
+     */
+    public synchronized P getOpponent(P player) {
+        if (player != null) {
+            if (player.equals(whitePlayer)) {
+                return blackPlayer;
+            } else if (player.equals(blackPlayer)) {
+                return whitePlayer;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Determines if player has an opponent.
+     *
+     * @param player The player.
+     * @return Outcome of test.
+     */
+    public synchronized boolean hasOpponent(P player) {
+        return getOpponent(player) != null;
+    }
+
+    /**
+     * Determines if player has an opponent, using color.
+     *
+     * @param color Color of player.
+     * @return Outcome of test.
+     */
+    public synchronized boolean hasOpponent(Color color) {
+        return getPlayer(color.getOpponentColor()) != null;
+    }
+
+    /**
+     * Returns opponent of a given player, using color.
+     *
+     * @param color Color of player.
+     * @return Outcome of test.
+     */
+    public synchronized P getOpponent(Color color) {
+        return getPlayer(color.getOpponentColor());
+    }
+
+    /**
+     * Returns the next turn.
+     *
+     * @return Next turn.
+     */
+    public synchronized Color getTurn() {
+        return turn;
+    }
+
+    /**
+     * Sets the next turn.
+     *
+     * @param turn Next turn.
+     */
+    public synchronized void setTurn(Color turn) {
+        this.turn = turn;
+    }
+
+    /**
+     * Returns complete list of moves in game.
+     *
+     * @return List of moves.
+     */
+    public synchronized List<Move> getMoves() {
+        return board.getMoves();
+    }
+
+    /**
+     * Adds a move to the end of the list.
+     *
+     * @param move The move.
+     */
+    public synchronized void addMove(Move move) {
+        if (watcher != null) {
+            watcher.addMove(this, move);
+        }
+        board.getMoves().add(move);
+    }
+
+    /**
+     * Gets summary for this game.
+     *
+     * @return The summary.
+     */
+    public synchronized String getSummary() {
+        return summary;
+    }
+
+    /**
+     * Sets a summary for this game.
+     *
+     * @param summary The summary.
+     */
+    public synchronized void setSummary(String summary) {
+        if (watcher != null) {
+            watcher.setSummary(this, summary);
+        }
+        this.summary = summary;
+    }
+
+    /**
+     * Gets the underlying board.
+     *
+     * @return The board.
+     */
+    public synchronized Board getBoard() {
+        return board;
+    }
+
+    /**
+     * Sets the underlying board for this game.
+     *
+     * @param board The board.
+     */
+    public synchronized void setBoard(Board board) {
+        this.board = board;
+    }
+
+    /**
+     * Returns game ID for this game.
+     *
+     * @return Game ID.
+     */
+    public synchronized String getGameId() {
+        return gameId;
+    }
+
+    /**
+     * Sets game ID for this game.
+     *
+     * @param gameId Game ID.
+     */
+    public synchronized void setGameId(String gameId) {
+        this.gameId = gameId;
+    }
+
+    /**
+     * Determines if the game is open. I.e., if there is less than
+     * two players.
+     *
+     * @return Outcome of test.
+     */
+    public synchronized boolean isOpen() {
+        return whitePlayer == null || blackPlayer == null;
+    }
+
+    /**
+     * Returns the internal state of the game.
+     *
+     * @return The state.
+     */
+    public synchronized State getState() {
+        return state;
+    }
+
+    /**
+     * Sets the internal state for this game.
+     *
+     * @param state The state.
+     */
+    public synchronized void setState(State state) {
+        if (watcher != null) {
+            watcher.setState(this, state);
+        }
+        this.state = state;
+    }
+
+    /**
+     * Sets the winner for the game.
+     *
+     * @param color The player's color.
+     */
+    public synchronized void setWinner(Color color) {
+        setState(color == Color.W ? State.WHITE_WINS : State.BLACK_WINS);
+    }
+
+    /**
+     * Returns this game's watcher.
+     *
+     * @return Watcher or <code>null</code> if no watcher set.
+     */
+    public GameWatcher<P, O> getWatcher() {
+        return watcher;
+    }
+
+    /**
+     * Get creation timestamp.
+     *
+     * @return Game creation timestamp.
+     */
+    public long getCreationStamp() {
+        return creationStamp;
+    }
+
+    /**
+     * Sets creation timestamp.
+     *
+     * @param creationStamp Creation timestamp.
+     */
+    public void setCreationStamp(long creationStamp) {
+        this.creationStamp = creationStamp;
+    }
+
+    /**
+     * Get update timestamp. This timestamp is updated every time a {@link #makeMove}
+     * is called.
+     *
+     * @return Game update timestamp.
+     */
+    public long getUpdateStamp() {
+        return updateStamp;
+    }
+
+    /**
+     * Sets a watcher for this game.
+     *
+     * @param watcher A watcher for this game.
+     */
+    public void setWatcher(GameWatcher<P, O> watcher) {
+        this.watcher = watcher;
+    }
+
+    /**
+     * Updates the internal state of the game by making a piece move. Allows
+     * <code>from</code> to be either a column or null.
+     *
+     * @param piece Piece to move.
+     * @param from Initial location in notation format.
+     * @param to Final location in notation format.
+     * @return The move.
+     * @throws GameException If an error is found while trying to move the piece.
+     */
+    public synchronized Move makeMove(Piece piece, String from, String to) throws GameException {
+        Point pointFrom = null;
+        final Point pointTo = Point.fromNotation(to);
+
+        // If not from or only column in from, compute from
+        if (from == null || from.length() == 1) {
+            int x = -1, y = -1;
+            if (from != null && from.length() == 1) {
+                final char ch = from.charAt(0);
+                if (Character.isDigit(ch)) {
+                    y = (ch - '1');
+                } else if (Character.isLetter(ch)) {
+                    x = (ch - 'a');
+                } else {
+                    throw new GameException(ILLEGAL_MOVE, "Not a valid chess move!");
+                }
+            }
+
+            final Iterator<Square> it = board.getIterator(piece.getColor());
+            while (it.hasNext()) {
+                final Square square = it.next();
+                if (square.getPiece() == piece && piece.isLegalMove(square.getPoint(), pointTo, board)
+                        && (x == -1 || square.getPoint().getX() == x)
+                        && (y == -1 || square.getPoint().getY() == y)) {
+                    pointFrom = square.getPoint();
+                    break;
+                }
+            }
+        } else {
+            pointFrom = Point.fromNotation(from);
+        }
+        if (pointFrom == null) {
+            throw new GameException(ILLEGAL_MOVE, "Not a valid chess move!");
+        }
+        return makeMove(piece.getColor(), pointFrom, pointTo);
+    }
+
+    /**
+     * Updates the internal state of the game by making a piece move.
+     *
+     * @param color Color of piece to move.
+     * @param from Initial location in notation format.
+     * @param to Final location in notation format.
+     * @return The move.
+     * @throws GameException If an error is found while trying to move the piece.
+     */
+    public synchronized Move makeMove(Color color, String from, String to) throws GameException {
+        return makeMove(color, Point.fromNotation(from), Point.fromNotation(to));
+    }
+
+    /**
+     * Updates the internal state of the game by making a piece move.
+     *
+     * @param color Color of piece to move.
+     * @param from Initial location of piece.
+     * @param to Final location of piece.
+     * @return The move.
+     * @throws GameException If an error is found while trying to move the piece.
+     */
+    public synchronized Move makeMove(Color color, Point from, Point to) throws GameException {
+        if (state != State.PLAYING) {
+            throw new GameException(GAME_OVER, state.toString());
+        }
+        if (color != turn) {
+            throw new GameException(NOT_YOUR_TURN, "Slow down, it is not your turn to play");
+        }
+        if (!board.hasPiece(from)) {
+            throw new GameException(NO_PIECE_AT_LOCATION, "Get some glasses, there's no piece there");
+        }
+        final Piece piece = board.getPiece(from);
+        if (color != piece.getColor()) {
+            throw new GameException(NOT_YOUR_PIECE, "Cheater! That's not your piece");
+        }
+        if (!piece.isLegalMove(from, to, board)) {
+            throw new GameException(ILLEGAL_MOVE, "You need to learn Chess!");
+        }
+
+        // Apply move to board
+        final Move move = new Move(piece, from, to);
+        board.doMove(move);
+
+        // Inform game observer
+        if (watcher != null) {
+            watcher.addMove(this, move);
+        }
+
+        // Is my king in check after this move?
+        final Square kingSquare = board.getKingSquare(color);
+        if (board.isKingAttacked(color, kingSquare.getPoint())) {
+            board.undoLastMove();
+            throw new GameException(ILLEGAL_MOVE_KING_CHECK, "Can't leave your king in check!");
+        }
+
+        // Switch turns
+        turn = turn.getOpponentColor();
+
+        // Update timestamp
+        updateStamp = System.currentTimeMillis();
+        
+        return move;
+    }
+
+    /**
+     * Returns the list of moves that are legal for a piece of <code>color</code>
+     * located at position <code>from</code>.
+     *
+     * @param color Piece's color.
+     * @param from Piece's location in notation format.
+     * @return List of moves in algebraic notation format.
+     * @throws GameException If no piece at location or of the wrong color.
+     */
+    public synchronized List<String> queryMoves(Color color, String from) throws GameException {
+        return queryMoves(color, Point.fromNotation(from));
+    }
+
+    /**
+     * Returns the list of moves that are legal for a piece of <code>color</code>
+     * located at position <code>from</code>.
+     *
+     * @param color Piece's color.
+     * @param from Piece's location.
+     * @return List of moves in algebraic notation format.
+     * @throws GameException If no piece at location or of the wrong color.
+     */
+    public synchronized List<String> queryMoves(Color color, Point from) throws GameException {
+        if (!board.hasPiece(from)) {
+            throw new GameException(NO_PIECE_AT_LOCATION, "Get some glasses, there's no piece there");
+        }
+        final Piece piece = board.getPiece(from);
+        if (color != piece.getColor()) {
+            throw new GameException(NOT_YOUR_PIECE, "Piece at that location of wrong color");
+        }
+
+        final List<Point> points = board.queryMoves(from);
+        final List<String> result = new ArrayList<>(points.size());
+        for (Point to : points) {
+            // Filter out moves that leave king in check
+            final Move move = new Move(piece, from, to);
+            try {
+                board.doMove(move);
+                final Square kingSquare = board.getKingSquare(color);
+                if (!board.isKingAttacked(color, kingSquare.getPoint())) {
+                    result.add(to.toNotation());
+                }
+            } finally {
+                board.undoLastMove();
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Determines if the game is over due to a checkmate.
+     *
+     * @param color King's color to check.
+     * @return Outcome of checkmate test.
+     */
+    public synchronized boolean isCheckmate(Color color) {
+        final Square kingSquare = board.getKingSquare(color);
+        final Point kingPoint = kingSquare.getPoint();
+
+        // Is King in check?
+        List<Square> attackers = kingAttackers(color, kingPoint);
+        if (attackers == null) {
+            return false;
+        }
+
+        // Is it in check no matter where it moves?
+        List<Point> tos = kingSquare.getPiece().generateMoves(kingPoint, board);
+        for (Point to : tos) {
+            final Move move = new Move(kingSquare.getPiece(), kingPoint, to);
+            board.doMove(move);
+            if (!board.isKingAttacked(color, to)) {
+                board.undoLastMove();
+                return false;
+            }
+            board.undoLastMove();
+        }
+
+        try {
+            // Is there any other piece that can stop all attackers?
+            Iterator<Square> squares = board.getIterator(color);
+            while (squares.hasNext()) {
+                final Square square = squares.next();
+                final Piece piece = square.getPiece();
+                if (piece == color.getKing()) {
+                    continue;               // skip king!
+                }
+                List<Point> pieceMoves = piece.generateMoves(square.getPoint(), board);
+                int nAttackers = attackers.size();
+                for (Square attacker : attackers) {
+                    List<Point> path = attacker.getPiece().generatePath(attacker.getPoint(), kingPoint);
+                    if (!Collections.disjoint(path, pieceMoves)) {
+                        nAttackers--;       // piece that can block attacker
+                        continue;
+                    }
+                    Set<Point> attackerPoint = Collections.singleton(attacker.getPoint());
+                    if (!Collections.disjoint(attackerPoint, pieceMoves)) {
+                        nAttackers--;       // piece that can capture attacker
+                    }
+                }
+                if (nAttackers == 0) {
+                    return false;       // piece can stop all the attackers
+                }
+            }
+        } catch (GameException _) {
+            throw new InternalError();
+        }
+
+        return true;        // checkmate!
+    }
+
+    /**
+     * Determines if the king of <code>color</code> is in check or not.
+     *
+     * @param color King's color.
+     * @return Result of in-check test.
+     */
+    public synchronized boolean isKingAttacked(Color color) {
+        return board.isKingAttacked(color);
+    }
+
+    /**
+     * Returns the list of pieces that are attacking the king of <code>color</code> or
+     * <code>null</null> if king is not in check.
+     *
+     * @param color King's color.
+     * @param toKing King's location on the board.
+     * @return The list of pieces attacking king or <code>null</code> if not in check.
+     */
+    public synchronized List<Square> kingAttackers(Color color, Point toKing) {
+        return board.kingAttackers(color, toKing);
+    }
+
+    /**
+     * Determines if a stalemate situation is found for <code>color</code>. Game
+     * should end as a draw.
+     *
+     * @param color Color to check for stalemate situation.
+     * @return Outcome of test.
+     */
+    public synchronized boolean isStalemate(Color color) {
+        try {
+            Iterator<Square> squares = board.getIterator(color);
+            while (squares.hasNext()) {
+                final Square square = squares.next();
+                if (queryMoves(color, square.getPoint()).size() > 0) {
+                    return false;
+                }
+            }
+            return true;
+        } catch (GameException _) {
+            throw new InternalError();
+        }
+    }
+
+    /**
+     * Determines if a player has requested a draw.
+     *
+     * @return Outcome of test.
+     */
+    public synchronized boolean hasDrawRequester() {
+        return drawRequester != null;
+    }
+
+    /**
+     * Gets player that requested a draw or no null if there is no such player.
+     *
+     * @return Player that requested draw.
+     */
+    public synchronized P getDrawRequester() {
+        return drawRequester;
+    }
+
+    /**
+     * Sets player that requested a draw.
+     *
+     * @param color Draw requester's color.
+     */
+    public synchronized void setDrawRequester(Color color) {
+        this.drawRequester = getPlayer(color);
+    }
+
+    /**
+     * Computes hash code based on game ID.
+     *
+     * @return Hash code.
+     */
+    @Override
+    public int hashCode() {
+        return gameId != null ? gameId.hashCode() : super.hashCode();
+    }
+
+    /**
+     * Determines equality using game IDs.
+     *
+     * @param obj Other object.
+     * @return Outcome of test.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Game<P, O> other = (Game<P, O>) obj;
+        if (!Objects.equals(this.gameId, other.gameId)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns string representation for the game. Mostly for debugging purposes.
+     *
+     * @return String representation for game.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Next turn is ").append(turn).append("\n\n");
+        sb.append(board);
+        return sb.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/GameException.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+/**
+ * IllegalMoveException class.
+ *
+ */
+public class GameException extends Exception {
+
+    // Error codes must be between 0 and 999
+    public static enum ErrorCode {
+        NOT_YOUR_TURN(100),
+        NO_PIECE_AT_LOCATION(200),
+        NOT_YOUR_PIECE(300),
+        ILLEGAL_MOVE(400),
+        ILLEGAL_MOVE_KING_CHECK(500),
+        GAME_OVER(600);
+        
+        int code;
+
+        ErrorCode(int code) {
+            this.code = code;
+        }
+
+        public int getCode() {
+            return code;
+        }
+     };
+
+    private ErrorCode code;
+
+    private Piece piece;
+
+    private Point from;
+
+    private Point to;
+
+    public GameException(ErrorCode code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public GameException(Piece piece, Point from, Point to) {
+        this.piece = piece;
+        this.from = from;
+        this.to = to;
+    }
+
+    public ErrorCode getErrorCode() {
+        return code;
+    }
+
+    public Piece getPiece() {
+        return piece;
+    }
+
+    public Point getFrom() {
+        return from;
+    }
+
+    public Point getTo() {
+        return to;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/GameWatcher.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import com.oracle.chess.model.Game.State;
+
+/**
+ * GameObserver class.
+ *
+ * @param <P> Type of a game player.
+ * @param <O> Type of a game observer.
+ */
+public interface GameWatcher<P, O> {
+
+    void addMove(Game<P,O> game, Move move);
+
+    void setPlayer(Game<P,O> game, Color color, P player);
+
+    void setStartTurn(Game<P,O> game, Color startTurn);
+
+    void setState(Game<P,O> game, State state);
+
+    void setSummary(Game<P,O> game, String summary);
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/King.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * King class.
+ *
+ */
+public final class King extends Piece {
+
+    final static Point W_START_CASTLING = Point.fromXY(4, 0);
+    final static Point W_LEFT_CASTLING  = Point.fromXY(2, 0);
+    final static Point W_RIGHT_CASTLING = Point.fromXY(6, 0);
+    final static Point W_LEFT_ROOK      = Point.fromXY(0, 0);
+    final static Point W_RIGHT_ROOK     = Point.fromXY(7, 0);
+
+    final static Point B_START_CASTLING = Point.fromXY(4, 7);
+    final static Point B_LEFT_CASTLING  = Point.fromXY(2, 7);
+    final static Point B_RIGHT_CASTLING = Point.fromXY(6, 7);
+    final static Point B_LEFT_ROOK      = Point.fromXY(0, 7);
+    final static Point B_RIGHT_ROOK     = Point.fromXY(7, 7);
+
+    protected King(Color color) {
+        super(color);
+    }
+
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        return Math.abs(x2 - x1) <= 1 && Math.abs(y2 - y1) <= 1;
+    }
+
+    @Override
+    public boolean isLegalMove(Point from, Point to, Board board) {
+        if (super.isLegalMove(from, to, board)) {
+            return true;
+        }
+
+        // Check if this is a castling move
+        if (color == Color.W) {
+            if (from.equals(W_START_CASTLING)) {
+                // Has king been moved?
+                if (board.hasPiecedMoved(from)) {
+                    return false;
+                }
+                // Check additional castling conditions depending on direction
+                if (to.equals(W_LEFT_CASTLING)) {
+                    return checkCastlingConditions(W_LEFT_ROOK, board);
+                } else if (to.equals(W_RIGHT_CASTLING)) {
+                    return checkCastlingConditions(W_RIGHT_ROOK, board);
+                }
+            }
+        } else {
+            if (from.equals(B_START_CASTLING)) {
+                // Has king been moved?
+                if (board.hasPiecedMoved(from)) {
+                    return false;
+                }
+                // Check additional castling conditions depending on direction
+                if (to.equals(B_LEFT_CASTLING)) {
+                    return checkCastlingConditions(B_LEFT_ROOK, board);
+                } else if (to.equals(B_RIGHT_CASTLING)) {
+                    return checkCastlingConditions(B_RIGHT_ROOK, board);
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toNotation() {
+        return "K";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        final List<Point> moves = new ArrayList<>();
+        final int x = from.getX();
+        final int y = from.getY();
+
+        Point to;
+        if (x > 0) {
+            to = Point.fromXY(x - 1, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            if (y > 0) {
+                to = Point.fromXY(x - 1, y - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y < Board.N_SQUARES - 1) {
+                to = Point.fromXY(x - 1, y + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        if (y > 0) {
+            to = Point.fromXY(x, y - 1);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+        }
+        if (x < Board.N_SQUARES - 1) {
+            to = Point.fromXY(x + 1, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            if (y < Board.N_SQUARES - 1) {
+                to = Point.fromXY(x + 1, y + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y > 0) {
+                to = Point.fromXY(x + 1, y - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        if (y < Board.N_SQUARES - 1) {
+            to = Point.fromXY(x, y + 1);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+        }
+
+        if (color == Color.W && from.equals(W_START_CASTLING) ||
+                color == Color.B && from.equals(B_START_CASTLING)) {
+            to = Point.fromXY(x - 2, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            to = Point.fromXY(x + 2, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+        }
+
+        return moves;
+    }
+
+    /**
+     * Checks that (i) the rook has not been moved (ii) that there are no pieces
+     * between the rook and the king and (iii) that king is not in check when
+     * during and at the end of the castling move.
+     *
+     * @param rook Rook involved in move.
+     * @param board The board.
+     * @return Outcome of test.
+     */
+    private boolean checkCastlingConditions(Point rook, Board board) {
+        boolean isAllowed;
+        final boolean left = (rook.getX() == 0);
+        final Point start = color == Color.W ? W_START_CASTLING : B_START_CASTLING;
+
+        if (left) {
+            isAllowed = !board.hasPiecedMoved(rook)
+                    && !board.hasPiece(rook.incrementX(1))
+                    && !board.hasPiece(rook.incrementX(2))
+                    && !board.hasPiece(rook.incrementX(3))
+                    && !board.isKingAttacked(color);
+            if (isAllowed) {
+                board.doMove(new Move(this, start, start.decrementX(1)));
+                isAllowed = !board.isKingAttacked(color);
+                board.undoLastMove();
+                if (isAllowed) {
+                    board.doMove(new Move(this, start, start.decrementX(2)));
+                    isAllowed = !board.isKingAttacked(color);
+                    board.undoLastMove();
+                }
+            }
+        } else {
+            isAllowed = !board.hasPiecedMoved(rook)
+                    && !board.hasPiece(rook.decrementX(1))
+                    && !board.hasPiece(rook.decrementX(2))
+                    && !board.isKingAttacked(color);
+            if (isAllowed) {
+                board.doMove(new Move(this, start, start.incrementX(1)));
+                isAllowed = !board.isKingAttacked(color);
+                board.undoLastMove();
+                if (isAllowed) {
+                    board.doMove(new Move(this, start, start.incrementX(2)));
+                    isAllowed = !board.isKingAttacked(color);
+                    board.undoLastMove();
+                }
+            }
+        }
+        return isAllowed;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Knight.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Knight class.
+ *
+ */
+public final class Knight extends Piece {
+
+    protected Knight(Color color) {
+        super(color);
+    }
+
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        return Math.abs(y2 - y1) == 2 && Math.abs(x2 - x1) == 1
+                || Math.abs(y2 - y1) == 1 && Math.abs(x2 - x1) == 2;
+    }
+
+    @Override
+    public String toNotation() {
+        return "N";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        if (!isValidMove(from, to)) {
+            throw new GameException(this, from, to);
+        }
+        return Collections.EMPTY_LIST;      // horses can jump!
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        final List<Point> moves = new ArrayList<>();
+        int x = from.getX();
+        int y = from.getY();
+
+        Point to;
+        if (x + 1 < Board.N_SQUARES) {
+            if (y + 2 < Board.N_SQUARES) {
+                to = Point.fromXY(x + 1, y + 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y - 2 >= 0) {
+                to = Point.fromXY(x + 1, y - 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        if (x - 1 >= 0) {
+            if (y + 2 < Board.N_SQUARES) {
+                to = Point.fromXY(x - 1, y + 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y - 2 >= 0) {
+                to = Point.fromXY(x - 1, y - 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        if (x + 2 < Board.N_SQUARES) {
+            if (y + 1 < Board.N_SQUARES) {
+                to = Point.fromXY(x + 2, y + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y - 1 >= 0) {
+                to = Point.fromXY(x + 2, y - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        if (x - 2 >= 0) {
+            if (y + 1 < Board.N_SQUARES) {
+                to = Point.fromXY(x - 2, y + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+            if (y - 1 >= 0) {
+                to = Point.fromXY(x - 2, y - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+            }
+        }
+        return moves;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Move.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+/**
+ * Move class.
+ *
+ */
+public class Move {
+
+    public enum Type {
+        NORMAL, PROMOTION, EN_PASSANT, LEFT_CASTLING, RIGHT_CASTLING
+    };
+
+    private Point from;
+    
+    private Point to;
+    
+    private Piece piece;
+    
+    private Piece captured;
+
+    private boolean promoted;
+
+    private boolean enPassant;
+
+    public Move() {
+    }
+
+    public Move(Piece piece, Point from, Point to) {
+        this(piece, from, to, null);
+    }
+
+    public Move(Piece piece, Point from, Point to, Piece captured) {
+        this.piece = piece;
+        this.from = from;
+        this.to = to;
+        this.captured = captured;
+    }
+
+    public Point getFrom() {
+        return from;
+    }
+
+    public void setFrom(Point from) {
+        this.from = from;
+    }
+
+    public Point getTo() {
+        return to;
+    }
+
+    public void setTo(Point to) {
+        this.to = to;
+    }
+
+    public Piece getPiece() {
+        return piece;
+    }
+
+    public void setPiece(Piece piece) {
+        this.piece = piece;
+    }
+
+    public Color getColor() {
+        return piece.getColor();
+    }
+    
+    public Piece getCaptured() {
+        return captured;
+    }
+
+    public void setCaptured(Piece captured) {
+        this.captured = captured;
+    }
+
+    public boolean hasCaptured() {
+        return captured != null;
+    }
+
+    public boolean isPromoted() {
+        return promoted;
+    }
+
+    public void setPromoted(boolean promoted) {
+        this.promoted = promoted;
+    }
+
+    public boolean isEnPassant() {
+        return enPassant;
+    }
+
+    public void setEnPassant(boolean enPassant) {
+        this.enPassant = enPassant;
+    }
+
+    public boolean isEnPassantAllowed(Move lastMove) {
+        if (lastMove != null && piece instanceof Pawn) {
+            if (piece.getColor() == Color.W) {
+                return lastMove.getPiece() == Piece.BLACK_PAWN
+                        && from.getY() == lastMove.getTo().getY()
+                        && lastMove.getFrom().getY() == 6
+                        && to.getX() == lastMove.getTo().getX();
+            } else {
+                return lastMove.getPiece() == Piece.WHITE_PAWN
+                        && from.getY() == lastMove.getTo().getY()
+                        && lastMove.getFrom().getY() == 1
+                        && to.getX() == lastMove.getTo().getX();
+            }
+        }
+        return false;
+    }
+
+    public boolean isLeftCastling() {
+        return (piece == Piece.WHITE_KING
+                && from.equals(King.W_START_CASTLING)
+                && to.equals(King.W_START_CASTLING.decrementX(2))) ||
+                (piece == Piece.BLACK_KING
+                && from.equals(King.B_START_CASTLING)
+                && to.equals(King.B_START_CASTLING.decrementX(2)));
+    }
+
+    public boolean isRightCastling() {
+        return (piece == Piece.WHITE_KING 
+                && from.equals(King.W_START_CASTLING) 
+                && to.equals(King.W_START_CASTLING.incrementX(2))) ||
+                (piece == Piece.BLACK_KING 
+                && from.equals(King.B_START_CASTLING) 
+                && to.equals(King.B_START_CASTLING.incrementX(2)));
+    }
+
+    public Type getType() {
+        return enPassant ? Type.EN_PASSANT
+                : promoted ? Type.PROMOTION
+                : isLeftCastling() ? Type.LEFT_CASTLING
+                : isRightCastling() ? Type.RIGHT_CASTLING
+                : Type.NORMAL;
+    }
+
+    public String toNotation() {
+        return from.toNotation() + to.toNotation();     // TODO: capture/promotion?
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Pawn.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pawn class.
+ *
+ */
+public final class Pawn extends Piece {
+
+    protected Pawn(Color color) {
+        super(color);
+    }
+
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        switch (color) {
+            case W:
+                return x1 == x2 && y2 - y1 == 1 ||                 // one square forward
+                       x1 == x2 && y2 - y1 == 2 && y1 == 1 ||      // two squares forward at start
+                       y2 - y1 == 1 && Math.abs(x2 - x1) == 1;     // captures
+            case B:
+                return x1 == x2 && y2 - y1 == -1 ||                // one square forward
+                       x1 == x2 && y2 - y1 == -2 && y1 == 6 ||     // two squares forward at start
+                       y2 - y1 == -1 && Math.abs(x2 - x1) == 1;    // captures
+        }
+        throw new IllegalStateException();
+    }
+
+    @Override
+    public boolean isLegalMove(Point from, Point to, Board board) {
+        if (!super.isLegalMove(from, to, board)) {
+            return false;
+        }
+
+        if (from.getX() == to.getX()) {
+            return !board.hasPiece(to);
+        } else if (board.hasPiece(to)) {
+            return board.hasColoredPiece(to, color.getOpponentColor());        // a normal capture
+        } else {
+            // Perhaps an en passant move?
+            return new Move(this, from, to).isEnPassantAllowed(board.getLastMove());
+        }
+    }
+
+    @Override
+    public String toNotation() {
+        return "P";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        if (!isValidMove(from, to)) {
+            throw new GameException(this, from, to);
+        }
+        // Move is valid for pawn, so we use queen here
+        return WHITE_QUEEN.generatePath(from, to);
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        Point to;
+        final List<Point> moves = new ArrayList<>();
+
+        switch (color) {
+            case W:
+                to = Point.fromXY(from.getX(), from.getY() + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX(), from.getY() + 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX() + 1, from.getY() + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX() - 1, from.getY() + 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                break;
+            case B:
+                to = Point.fromXY(from.getX(), from.getY() - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX(), from.getY() - 2);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX() + 1, from.getY() - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                to = Point.fromXY(from.getX() - 1, from.getY() - 1);
+                if (isLegalMove(from, to, board)) {
+                    moves.add(to);
+                }
+                break;
+        }
+        return moves;
+    }
+
+    @Override
+    public boolean isPromoted(Point to) {
+        return color == Color.W && to.getY() == 7 || color == Color.B && to.getY() == 0;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Piece.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+
+import static com.oracle.chess.model.Board.N_SQUARES;
+import java.util.List;
+
+/**
+ * Piece class.
+ *
+ */
+public abstract class Piece {
+
+    public static final Pawn WHITE_PAWN = new Pawn(Color.W);
+    public static final Pawn BLACK_PAWN = new Pawn(Color.B);
+
+    public static final Rook WHITE_ROOK = new Rook(Color.W);
+    public static final Rook BLACK_ROOK = new Rook(Color.B);
+
+    public static final Knight WHITE_KNIGHT = new Knight(Color.W);
+    public static final Knight BLACK_KNIGHT = new Knight(Color.B);
+
+    public static final Bishop WHITE_BISHOP = new Bishop(Color.W);
+    public static final Bishop BLACK_BISHOP = new Bishop(Color.B);
+
+    public static final King WHITE_KING = new King(Color.W);
+    public static final King BLACK_KING = new King(Color.B);
+
+    public static final Queen WHITE_QUEEN = new Queen(Color.W);
+    public static final Queen BLACK_QUEEN = new Queen(Color.B);
+    
+    protected Color color;
+
+    protected Piece(Color color) {
+        this.color = color;
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    private boolean inRange(int z) {
+        return z >= 0 && z < N_SQUARES;
+    }
+
+    /**
+     * Determines if this piece can move from (x1,y1) to (x2,y2) based
+     * on its kind, regardless of other pieces or the game's state.
+     *
+     * @param x1 Source x coordinate.
+     * @param y1 Source y coordinate.
+     * @param x2 Destination x coordinate.
+     * @param y2 Destination y coordinate.
+     * @return Validity of move based on kind.
+     */
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        return (x1 != x2 || y1 != y2) && inRange(x1) && inRange(y1) && inRange(x2) && inRange(y2);
+    }
+
+    /**
+     * Determines is this piece can move from one point to another based
+     * on its kind, regardless of other pieces or the game's state.
+     *
+     * @param from From point.
+     * @param to To point.
+     * @return Validity of move based on kind.
+     */
+    public boolean isValidMove(Point from, Point to) {
+        return isValidMove(from.getX(), from.getY(), to.getX(), to.getY());
+    }
+
+    /**
+     * Generates a list of points between <code>from</code> and <code>to</code>
+     * for this piece, excluding <code>from</code> and <code>to</code>, or
+     * throws an exception if that isn't possible.
+     *
+     * @param from From point.
+     * @param to To point.
+     * @return List of points in path excluding <code>from</code> and <code>to</code>.
+     * @throws GameException If not allowed for this kind of piece.
+     */
+    public abstract List<Point> generatePath(Point from, Point to) throws GameException;
+
+    /**
+     * Generates a list of all <b>legal</b> moves for this piece.
+     *
+     * @param from From point.
+     * @param board The chessboard.
+     * @return List of points that this piece can move to.
+     */
+    public abstract List<Point> generateMoves(Point from, Board board);
+
+    /**
+     * A move is legal if (i) it is valid for this piece and (ii) it can be completed
+     * without being blocked by any other piece on the board. Note that this method
+     * does not check if the king is in check after this move.
+     *
+     * @param from From point.
+     * @param to To point.
+     * @param board The chess board.
+     * @return Legality of move based on kind and game state.
+     */
+    public boolean isLegalMove(Point from, Point to, Board board) {
+        if (!isValidMove(from, to)) {
+            return false;
+        }
+        try {
+            List<Point> path = generatePath(from, to);
+            for (Point p : path) {
+                if (board.hasPiece(p)) {
+                    return false;       // Another piece in the way
+                }
+            }
+            // Check if trying to capture piece of same color
+            Piece other = board.getPiece(to);
+            if (other != null && other.getColor() == color) {
+                return false;
+            }
+        } catch (GameException _) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Determines if this piece can be promoted when moved to this point. This
+     * method is overridden in {@link com.oracle.chess.model.Pawn}.
+     *
+     * @param to To point.
+     * @return Outcome of promotion test.
+     */
+    public boolean isPromoted(Point to) {
+        return false;
+    }
+
+    public abstract String toNotation();
+
+    public static Piece fromNotation(Color color, String notation) {
+        char ch = notation.charAt(0);
+        switch (ch) {
+            case 'P':
+                return color == Color.W ? WHITE_PAWN : BLACK_PAWN;
+            case 'R':
+                return color == Color.W ? WHITE_ROOK : BLACK_ROOK;
+            case 'N':
+                return color == Color.W ? WHITE_KNIGHT : BLACK_KNIGHT;
+            case 'B':
+                return color == Color.W ? WHITE_BISHOP : BLACK_BISHOP;
+            case 'K':
+                return color == Color.W ? WHITE_KING : BLACK_KING;
+            case 'Q':
+                return color == Color.W ? WHITE_QUEEN : BLACK_QUEEN;
+            default:
+                throw new InternalError("Unknown piece notation " + notation);
+        }
+    }
+
+    public static Piece fromString(String s) {
+        return fromNotation(Color.valueOf(s.substring(0, 1)), s.substring(1));
+    }
+
+    @Override
+    public String toString() {
+        return color.toString() + toNotation();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Point.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Point class.
+ *
+ */
+public final class Point {
+
+    static final char[] letters = { 'a', 'b', 'c', 'd', 'e', 'f', 'g' ,'h' };
+
+    private final int x;
+    private final int y;
+
+    public Point(int x, int y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public int getX() {
+        return x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public Point decrementX(int delta) {
+        return fromXY(x - delta, y);
+    }
+
+    public Point incrementX(int delta) {
+        return fromXY(x + delta, y);
+    }
+
+    public Point decrementY(int delta) {
+        return fromXY(x, y - delta);
+    }
+
+    public Point incrementY(int delta) {
+        return fromXY(x, y + delta);
+    }
+    
+    @Override
+    public int hashCode() {
+        return y * 8 + x;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final Point other = (Point) obj;
+        return this.x == other.x && this.y == other.y;
+    }
+
+    @Override
+    public String toString() {
+        return "(" + x + "," + y + ")";
+    }
+
+    private static final Map<Integer, Point> cache = new ConcurrentHashMap<>();
+
+    public static Point fromXY(int x, int y) {
+        final int index = y * Board.N_SQUARES + x;
+        Point point = cache.get(index);
+        if (point == null) {
+            point = new Point(x, y);
+            cache.put(index, point);
+        }
+        return point;
+    }
+
+    public static Point fromNotation(String s) {
+        return fromXY((int) s.charAt(0) - 'a', (int) s.charAt(1) - '1');
+    }
+
+    public String toNotation() {
+        return letters[x] + Integer.toString(y + 1);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Queen.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.List;
+
+/**
+ * Queen class.
+ *
+ */
+public final class Queen extends Piece {
+
+    public Queen(Color color) {
+        super(color);
+    }
+
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        return (x1 == x2 && y1 != y2)
+                || (x1 != x2 && y1 == y2)
+                || Math.abs(x2 - x1) == Math.abs(y2 - y1);
+    }
+
+    @Override
+    public String toNotation() {
+        return "Q";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        if (!isValidMove(from, to)) {
+            throw new GameException(this, from, to);
+        }
+        return (from.getX() != to.getX() && from.getY() != to.getY())
+                ? color.getBishop().generatePath(from, to) : color.getRook().generatePath(from, to);
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        final List<Point> moves = color.getBishop().generateMoves(from, board);
+        moves.addAll(color.getRook().generateMoves(from, board));
+        return moves;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Rook.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Rook class.
+ *
+ */
+public final class Rook extends Piece {
+
+    protected Rook(Color color) {
+        super(color);
+    }
+    
+    @Override
+    public boolean isValidMove(int x1, int y1, int x2, int y2) {
+        if (!super.isValidMove(x1, y1, x2, y2)) {
+            return false;
+        }
+        return (x1 == x2 && y1 != y2) || (x1 != x2 && y1 == y2);
+    }
+
+    @Override
+    public String toNotation() {
+        return "R";
+    }
+
+    @Override
+    public List<Point> generatePath(Point from, Point to) throws GameException {
+        if (!isValidMove(from, to)) {
+            throw new GameException(this, from, to);
+        }
+
+        int x, y;
+        final List<Point> path = new ArrayList<>();
+        if (from.getX() > to.getX()) {
+            for (x = from.getX() - 1; x > to.getX(); x--) {
+                path.add(Point.fromXY(x, from.getY()));
+            }
+        } else if (from.getX() < to.getX()) {
+            for (x = from.getX() + 1; x < to.getX(); x++) {
+                path.add(Point.fromXY(x, from.getY()));
+            }
+        } else if (from.getY() > to.getY()) {
+            for (y = from.getY() - 1; y > to.getY(); y--) {
+                path.add(Point.fromXY(from.getX(), y));
+            }
+        } else if (from.getY() < to.getY()) {
+            for (y = from.getY() + 1; y < to.getY(); y++) {
+                path.add(Point.fromXY(from.getX(), y));
+            }
+        } else {
+            throw new InternalError();
+        }
+        return path;
+    }
+
+    @Override
+    public List<Point> generateMoves(Point from, Board board) {
+        int x, y;
+        final List<Point> moves = new ArrayList<>();
+
+        // (+, y)
+        x = from.getX() + 1;
+        y = from.getY();
+        while (x < Board.N_SQUARES) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x++;
+        }
+        // (-, y)
+        x = from.getX() - 1;
+        y = from.getY();
+        while (x >= 0) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            x--;
+        }
+        // (x, +)
+        x = from.getX();
+        y = from.getY() + 1;
+        while (y < Board.N_SQUARES) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            y++;
+        }
+        // (x, -)
+        x = from.getX();
+        y = from.getY() - 1;
+        while (y >= 0) {
+            final Point to = Point.fromXY(x, y);
+            if (isLegalMove(from, to, board)) {
+                moves.add(to);
+            }
+            y--;
+        }
+        return moves;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/model/Square.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.model;
+
+/**
+ * Square class.
+ *
+ */
+public class Square {
+
+    /**
+     * Point or coordinate of this square in the board.
+     */
+    private final Point point;
+
+    /**
+     * Piece sitting on this square or <code>null</code> if square
+     * is empty.
+     */
+    private Piece piece;
+
+    public Square(int x, int y) {
+        this(x, y, null);
+    }
+    
+    public Square(int x, int y, Piece piece) {
+        point = new Point(x, y);
+        this.piece = piece;
+    }
+
+    public Square(Point point) {
+        this(point, null);
+    }
+
+    public Square(Point point, Piece piece) {
+        this.point = point;
+        this.piece = piece;
+    }
+
+    /**
+     * Returns the point of this square on the board.
+     *
+     * @return Point or coordinate for this square.
+     */
+    public Point getPoint() {
+        return point;
+    }
+
+    /**
+     * Returns the piece sitting on this square or <code>null<code>
+     * if the square is empty.
+     *
+     * @return Piece on square or <code>null</code>.
+     */
+    public Piece getPiece() {
+        return piece;
+    }
+
+    /**
+     * Sets a new piece on this square.
+     *
+     * @param piece New piece.
+     */
+    public void setPiece(Piece piece) {
+        this.piece = piece;
+    }
+
+    /**
+     * Determines if a square is empty or not.
+     *
+     * @return Value <code>true</code> if piece on square, <code>false</code> otherwise.
+     */
+    public boolean isEmpty() {
+        return piece == null;
+    }
+
+    /**
+     * Returns the color for this square on the board.
+     *
+     * @return Color for this square.
+     */
+    public Color getColor() {
+        return (point.getX() + point.getY()) % 2 == 0 ? Color.B : Color.W;
+    }
+
+    /**
+     * Returns representation in algebraic notation. Letter for piece followed
+     * by coordinate. For example, Ra1 for rook on a1 (0, 0). If no piece in
+     * square, returns a strings with spaces.
+     *
+     * @return Notation representation.
+     */
+    public String toNotation() {
+        if (piece == null) {
+            return "   ";
+        }
+        return piece.toNotation() + point.toNotation();
+    }
+
+    /**
+     * String representation for this square.
+     *
+     * @return String representation.
+     */
+    @Override
+    public String toString() {
+        return toNotation();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/BoardRep.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import java.util.List;
+
+import com.oracle.chess.model.Board;
+import com.oracle.chess.model.Color;
+import com.oracle.chess.model.Piece;
+import com.oracle.chess.model.Point;
+import com.oracle.chess.model.Square;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * BoardRep class.
+ *
+ */
+public class BoardRep {
+
+    private List<String> whites;
+
+    private List<String> blacks;
+
+    public BoardRep() {
+    }
+
+    public BoardRep(Board board) {
+        whites = new ArrayList<>();
+        final Iterator<Square> wi = board.getIterator(Color.W);
+        while (wi.hasNext()) {
+            whites.add(wi.next().toNotation());
+        }
+        blacks = new ArrayList<>();
+        final Iterator<Square> bi = board.getIterator(Color.B);
+        while (bi.hasNext()) {
+            blacks.add(bi.next().toNotation());
+        }
+    }
+
+    public List<String> getWhites() {
+        return whites;
+    }
+
+    public void setWhites(List<String> whites) {
+        this.whites = whites;
+    }
+
+    public List<String> getBlacks() {
+        return blacks;
+    }
+
+    public void setBlacks(List<String> blacks) {
+        this.blacks = blacks;
+    }
+
+    public Board toBoard() {
+        final Board board = new Board();
+        board.clear();
+        if (whites != null) {
+            for (String w : whites) {
+                board.setPiece(Piece.fromNotation(Color.W, w.substring(0, 1)),
+                               Point.fromNotation(w.substring(1)));
+            }
+        }
+        if (blacks != null) {
+            for (String b : blacks) {
+                board.setPiece(Piece.fromNotation(Color.B, b.substring(0, 1)),
+                               Point.fromNotation(b.substring(1)));
+            }
+        }
+        return board;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/CheckCredentials.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+/**
+ * CheckCredentials class.
+ *
+ */
+public class CheckCredentials extends Message {
+
+    @Override
+    public Message processMe(ServerMessageProcessor processor) {
+        return processor.process(this);
+    }
+
+    @Override
+    public CheckCredentialsRsp newResponse() {
+        return new CheckCredentialsRsp(getUsername(), getPassword());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/CheckCredentialsRsp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+/**
+ * CheckCredentialsRsp class.
+ *
+ */
+public class CheckCredentialsRsp extends MessageRsp {
+
+    public static enum Check {
+        VALID, INVALID, NOT_REGISTERED
+    };
+
+    public CheckCredentialsRsp() {
+    }
+
+    public CheckCredentialsRsp(String username, String password) {
+        setUsername(username);
+        setPassword(password);
+    }
+
+    private Check type;
+
+    public Check getCheck() {
+        return type;
+    }
+
+    public void setCheck(Check type) {
+        this.type = type;
+    }
+
+    @Override
+    public void processMe(ClientMessageProcessor processor) {
+        processor.process(this);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/ClientMessageProcessor.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+/**
+ * Interface ClientMessageProcessor.
+ *
+ */
+public interface ClientMessageProcessor {
+
+    public void process(CreateGameRsp message);
+
+    public void process(JoinGameRsp message);
+
+    public void process(SendMoveRsp message);
+
+    public void process(QueryMovesRsp message);
+
+    public void process(UpdateGame message);
+
+    public void process(QueryGamesRsp message);
+
+    public void process(QueryGameRsp message);
+
+    public void process(SendAction message);
+
+    public void process(SendActionRsp message);
+
+    public void process(CheckCredentialsRsp message);
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/CreateGame.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import com.oracle.chess.model.Color;
+
+/**
+ * CreateGame class.
+ *
+ */
+public class CreateGame extends Message {
+
+    protected String summary;
+
+    private BoardRep board;
+
+    private Color turn;
+
+    private boolean persisted;
+
+    public CreateGame() {
+    }
+    
+    public CreateGame(String gameId, Color color) {
+        this(gameId, color, null);
+    }
+
+    public CreateGame(String gameId, Color color, String summary) {
+        super(gameId);
+        this.color = color;
+        this.summary = summary;
+    }
+
+    public String getSummary() {
+        return summary;
+    }
+
+    public void setSummary(String summary) {
+        this.summary = summary;
+    }
+
+    public BoardRep getBoard() {
+        return board;
+    }
+
+    public void setBoard(BoardRep board) {
+        this.board = board;
+    }
+
+    public boolean hasBoard() {
+        return board != null;
+    }
+
+    public Color getTurn() {
+        return turn;
+    }
+
+    public void setTurn(Color turn) {
+        this.turn = turn;
+    }
+
+    public boolean hasTurn() {
+        return turn != null;
+    }
+
+    public boolean isPersisted() {
+        return persisted;
+    }
+
+    public void setPersisted(boolean persisted) {
+        this.persisted = persisted;
+    }
+
+    @Override
+    public Message processMe(ServerMessageProcessor processor) {
+        return processor.process(this);
+    }
+
+    @Override
+    public CreateGameRsp newResponse() {
+        return new CreateGameRsp(gameId, color);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/CreateGameRsp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import com.oracle.chess.model.Color;
+
+/**
+ * CreateGameRsp class.
+ *
+ */
+public class CreateGameRsp extends GameRsp {
+
+    public CreateGameRsp() {
+    }
+
+    public CreateGameRsp(String gameId, Color color) {
+        super(gameId);
+        this.color = color;
+    }
+
+    @Override
+    public void processMe(ClientMessageProcessor processor) {
+        processor.process(this);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/GameRsp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+/**
+ * GameRsp class.
+ *
+ */
+public abstract class GameRsp extends MessageRsp {
+
+    private String summary;
+
+    private boolean open;
+
+    private String whitePlayer;
+
+    private String blackPlayer;
+
+    private boolean completed;
+
+    public GameRsp() {
+    }
+    
+    public GameRsp(String gameId) {
+        super(gameId);
+    }
+
+    public String getSummary() {
+        return summary;
+    }
+
+    public void setSummary(String summary) {
+        this.summary = summary;
+    }
+
+    public boolean isOpen() {
+        return open;
+    }
+
+    public void setOpen(boolean open) {
+        this.open = open;
+    }
+
+    public String getWhitePlayer() {
+        return whitePlayer;
+    }
+
+    public void setWhitePlayer(String whitePlayer) {
+        this.whitePlayer = whitePlayer;
+    }
+
+    public String getBlackPlayer() {
+        return blackPlayer;
+    }
+
+    public void setBlackPlayer(String blackPlayer) {
+        this.blackPlayer = blackPlayer;
+    }
+
+    public boolean isCompleted() {
+        return completed;
+    }
+
+    public void setCompleted(boolean completed) {
+        this.completed = completed;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/JoinGame.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+/**
+ * JoinGame class.
+ *
+ */
+public class JoinGame extends Message {
+
+    private boolean observer;
+
+    private boolean replay;
+
+    public JoinGame() {
+    }
+
+    public JoinGame(String gameId) {
+        super(gameId);
+    }
+
+    public boolean isObserver() {
+        return observer;
+    }
+
+    public void setObserver(boolean observer) {
+        this.observer = observer;
+    }
+
+    public boolean isReplay() {
+        return replay;
+    }
+
+    public void setReplay(boolean replay) {
+        this.replay = replay;
+    }
+    
+    @Override
+    public Message processMe(ServerMessageProcessor processor) {
+        return processor.process(this);
+    }
+
+    @Override
+    public JoinGameRsp newResponse() {
+        return new JoinGameRsp(gameId);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/JoinGameRsp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JoinGameRsp class.
+ *
+ */
+public class JoinGameRsp extends GameRsp {
+
+    private List<String> moves;
+    
+    public JoinGameRsp() {
+    }
+    
+    public JoinGameRsp(String gameId) {
+        super(gameId);
+    }
+
+    public List<String> getMoves() {
+        return moves;
+    }
+
+    public void setMoves(List<String> moves) {
+        this.moves = moves;
+    }
+
+    public void addMove(String move) {
+        if (moves == null) {
+            moves = new ArrayList<>();
+        }
+        moves.add(move);
+    }
+
+    @Override
+    public void processMe(ClientMessageProcessor processor) {
+        processor.process(this);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/Message.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import javax.json.Json;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.JsonArray;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonWriter;
+import javax.json.JsonWriterFactory;
+import javax.json.stream.JsonGenerator;
+
+import com.oracle.chess.model.Color;
+import java.lang.reflect.ParameterizedType;
+
+/**
+ * Message class.
+ *
+ */
+public abstract class Message {
+
+    // Create JSON writer factory with pretty printing enabled
+    private static final Map<String, Boolean> config;
+    private static final JsonWriterFactory factory;
+    static {
+        config = new HashMap<>();
+        config.put(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE);
+        factory = Json.createWriterFactory(config);
+    }
+
+    protected String msg;
+    
+    protected String gameId;
+
+    protected Color color;
+    
+    private String username;
+
+    private String password;
+
+    public Message() {
+        msg = getClass().getSimpleName();
+    }
+
+    public Message(String gameId) {
+        msg = getClass().getSimpleName();
+        this.gameId = gameId;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public String getGameId() {
+        return gameId;
+    }
+
+    public void setGameId(String gameId) {
+        this.gameId = gameId;
+    }
+
+    public boolean hasGameId() {
+        return gameId != null;
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        this.color = color;
+    }
+
+    public boolean hasColor() {
+        return color != null;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public boolean hasUsername() {
+        return username != null;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public Message readFrom(JsonObject jobj) {
+        readFrom(this, jobj);
+        return this;
+    }
+
+    private static void readFrom(Object object, JsonObject jobj) {
+        try {
+            for (PropertyDescriptor pd
+                    : Introspector.getBeanInfo(object.getClass(), Object.class).getPropertyDescriptors()) {
+                final Method m = pd.getWriteMethod();
+                if (m != null) {
+                    JsonValue jv = jobj.get(pd.getName());
+                    if (jv == null) {
+                        continue;
+                    }
+                    Class<?> clazz = m.getParameterTypes()[0];
+                    switch (jv.getValueType()) {
+                        case NULL:
+                            break;
+                        case STRING:
+                            final String sv = ((JsonString) jv).getString();
+                            if (clazz.isEnum()) {
+                                m.invoke(object, Enum.valueOf((Class<? extends Enum>) clazz, sv));
+                            } else {
+                                m.invoke(object, sv);
+                            }
+                            break;
+                        case NUMBER:
+                            m.invoke(object, ((JsonNumber) jv).intValue());
+                            break;
+                        case TRUE:
+                            m.invoke(object, true);
+                            break;
+                        case FALSE:
+                            m.invoke(object, false);
+                            break;
+                        case OBJECT:
+                            Object instance = clazz.newInstance();
+                            readFrom(instance, (JsonObject) jv);
+                            m.invoke(object, instance);
+                            break;
+                        case ARRAY:     // only array of strings and objects supported!
+                            final JsonArray ja = (JsonArray) jv;
+                            final List<Object> list = new ArrayList<>(ja.size());
+                            for (JsonValue v : ja) {
+                                if (v instanceof JsonString) {
+                                    list.add(((JsonString) v).getString());
+                                } else {
+                                    ParameterizedType pt = (ParameterizedType) m.getGenericParameterTypes()[0];
+                                    clazz = (Class<?>) pt.getActualTypeArguments()[0];
+                                    instance = clazz.newInstance();
+                                    readFrom(instance, (JsonObject) v);
+                                    list.add(instance);
+                                }
+                            }
+                            m.invoke(object, list);
+                            break;
+                        default:
+                            throw new UnsupportedOperationException("Unsupported type " + jv.getValueType());
+                    }
+                }
+            }
+        } catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public void writeTo(JsonObjectBuilder jobj) {
+        writeTo(this, jobj);
+    }
+
+    private static void writeTo(Object object, JsonObjectBuilder jobj) {
+        try {
+            for (PropertyDescriptor pd
+                    : Introspector.getBeanInfo(object.getClass(), Object.class).getPropertyDescriptors()) {
+                final Method m = pd.getReadMethod();
+                if (m != null) {
+                    Object v = m.invoke(object);
+                    if (v == null) {
+                        continue;
+                    } else if (v instanceof String) {
+                        jobj.add(pd.getName(), (String) v);
+                    } else if (v instanceof Integer) {
+                        jobj.add(pd.getName(), (Integer) v);
+                    } else if (v instanceof Boolean) {
+                        jobj.add(pd.getName(), (Boolean) v);
+                    } else if (v instanceof Enum) {
+                        jobj.add(pd.getName(), ((Enum) v).toString());
+                    } else if (v instanceof List) {     // only list of strings or objects supported!
+                        JsonArrayBuilder jab = Json.createArrayBuilder();
+                        for (Object o : (List) v) {
+                            if (o instanceof String) {
+                                jab.add((String) o);
+                            } else {
+                                JsonObjectBuilder njobj = Json.createObjectBuilder();
+                                writeTo(o, njobj);
+                                jab.add(njobj);
+                            }
+                        }
+                        jobj.add(pd.getName(), jab.build());
+                    } else {
+                        JsonObjectBuilder newJobj = Json.createObjectBuilder();
+                        writeTo(v, newJobj);
+                        jobj.add(pd.getName(), newJobj.build());
+                    }
+                }
+            }
+        } catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public static Message createInstance(String className) {
+        try {
+            final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
+            return (Message) ccl.loadClass("com.oracle.chess.protocol." + className).newInstance();
+        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public abstract Message processMe(ServerMessageProcessor processor);
+
+    public MessageRsp newResponse() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public String toString() {
+        final JsonObjectBuilder jobj = Json.createObjectBuilder();
+        writeTo(jobj);
+        final StringWriter sw = new StringWriter();
+        try (JsonWriter jw = factory.createWriter(sw)) {
+            jw.writeObject(jobj.build());
+        }
+        return sw.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/Chess/ChessLibrary/src/main/java/com/oracle/chess/protocol/MessageRsp.java	Tue Sep 24 10:09:57 2013 -0700
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.oracle.chess.protocol;
+
+import com.oracle.chess.model.Color;
+
+/**
+ * MessageRsp class.
+ *
+ */
+public abstract class MessageRsp extends Message {
+
+    public static class Error {
+<