Browse Source

added Blender-esque manipulation tool to scene composer

git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@9452 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
bre..ns 13 years ago
parent
commit
20edfa9b0e

+ 6 - 0
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/Bundle.properties

@@ -55,3 +55,9 @@ SceneComposerTopComponent.jButton2.text=
 SceneComposerTopComponent.jButton3.text=
 SceneComposerTopComponent.jButton2.toolTipText=start the physics simulation and add all objects to the physics space
 SceneComposerTopComponent.jButton3.toolTipText=stop the physics simulation and remove all objects from the physics space
+SceneComposerTopComponent.snapToSceneCheckbox.text=Snap to Scene
+SceneComposerTopComponent.snapToGridCheckbox.text=Snap to Grid
+SceneComposerTopComponent.snapToGridCheckbox.toolTipText=Moving an object will snap to the unit grid coordinates
+SceneComposerTopComponent.snapToSceneCheckbox.toolTipText=When moving an object, it will snap to whatever is under the mouse
+SceneComposerTopComponent.selectTerrainCheckbox.text=Select Terrain
+SceneComposerTopComponent.selectTerrainCheckbox.toolTipText=If selected, terrain can be selected, otherwise it will be ignored by the mouse

+ 2 - 0
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/ComposerCameraController.java

@@ -80,6 +80,7 @@ public class ComposerCameraController extends AbstractCameraController {
                 forceCameraControls = false;
             }
         }
+        toolController.doKeyPressed(kie);
     }
 
     @Override
@@ -116,4 +117,5 @@ public class ComposerCameraController extends AbstractCameraController {
     public boolean useCameraControls() {
         return isToolUsesCameraControls() || forceCameraControls;
     }
+
 }

+ 44 - 1
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneComposerToolController.java

@@ -9,6 +9,8 @@ import com.jme3.audio.AudioNode;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.scene.controller.SceneToolController;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
+import com.jme3.input.event.KeyInputEvent;
 import com.jme3.light.Light;
 import com.jme3.light.PointLight;
 import com.jme3.light.SpotLight;
@@ -44,6 +46,10 @@ public class SceneComposerToolController extends SceneToolController {
     private Node nonSpatialMarkersNode;
     private Material lightMarkerMaterial;
     private Material audioMarkerMaterial;
+    private JmeSpatial selectedSpatial;
+    private boolean snapToGrid = false;
+    private boolean snapToScene = true;
+    private boolean selectTerrain = false;
 
     public SceneComposerToolController(final Node toolsNode, AssetManager manager, JmeNode rootNode) {
         super(toolsNode, manager);
@@ -190,7 +196,7 @@ public class SceneComposerToolController extends SceneToolController {
     public void doEditToolMoved(Vector2f mouseLoc, Camera camera) {
         if (editTool != null) {
             editTool.setCamera(camera);
-            editTool.mouseMoved(mouseLoc);
+            editTool.mouseMoved(mouseLoc, rootNode, editorController.getCurrentDataObject(), selectedSpatial);
         }
     }
 
@@ -208,6 +214,12 @@ public class SceneComposerToolController extends SceneToolController {
         }
     }
     
+    void doKeyPressed(KeyInputEvent kie) {
+        if (editTool != null) {
+            editTool.keyPressed(kie);
+        }
+    }
+    
     /**
      * Adds a marker for the light to the scene if it does not exist yet
      */
@@ -289,6 +301,30 @@ public class SceneComposerToolController extends SceneToolController {
             }
         }
     }
+
+    public boolean isSnapToGrid() {
+        return snapToGrid;
+    }
+
+    public void setSnapToGrid(boolean snapToGrid) {
+        this.snapToGrid = snapToGrid;
+    }
+
+    public void setSnapToScene(boolean snapToScene) {
+        this.snapToScene = snapToScene;
+    }
+
+    public boolean isSnapToScene() {
+        return snapToScene;
+    }
+
+    public boolean selectTerrain() {
+        return selectTerrain;
+    }
+
+    public void setSelectTerrain(boolean selectTerrain) {
+        this.selectTerrain = selectTerrain;
+    }
     
     /**
      * A marker on the screen that shows where a point light or
@@ -440,5 +476,12 @@ public class SceneComposerToolController extends SceneToolController {
         }
         
     }
+
+    public JmeNode getRootNode() {
+        return rootNode;
+    }
     
+    public void setSelectedSpatial(JmeSpatial selectedSpatial) {
+        this.selectedSpatial = selectedSpatial;
+    }
 }

+ 62 - 5
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneComposerTopComponent.form

@@ -443,13 +443,24 @@
               <Group type="102" alignment="0" attributes="0">
                   <EmptySpace max="-2" attributes="0"/>
                   <Group type="103" groupAlignment="0" attributes="0">
+                      <Component id="jSeparator6" alignment="0" pref="357" max="32767" attributes="0"/>
                       <Group type="102" alignment="0" attributes="0">
                           <EmptySpace min="10" pref="10" max="10" attributes="0"/>
-                          <Component id="jLabel5" min="-2" max="-2" attributes="0"/>
-                          <EmptySpace max="-2" attributes="0"/>
-                          <Component id="emitButton" pref="302" max="32767" attributes="0"/>
+                          <Group type="103" groupAlignment="0" attributes="0">
+                              <Group type="102" alignment="0" attributes="0">
+                                  <Component id="snapToSceneCheckbox" min="-2" max="-2" attributes="0"/>
+                                  <EmptySpace type="separate" max="-2" attributes="0"/>
+                                  <Component id="snapToGridCheckbox" min="-2" max="-2" attributes="0"/>
+                                  <EmptySpace type="separate" max="-2" attributes="0"/>
+                                  <Component id="selectTerrainCheckbox" min="-2" max="-2" attributes="0"/>
+                              </Group>
+                              <Group type="102" alignment="0" attributes="0">
+                                  <Component id="jLabel5" min="-2" max="-2" attributes="0"/>
+                                  <EmptySpace max="-2" attributes="0"/>
+                                  <Component id="emitButton" pref="302" max="32767" attributes="0"/>
+                              </Group>
+                          </Group>
                       </Group>
-                      <Component id="jSeparator6" alignment="0" pref="357" max="32767" attributes="0"/>
                   </Group>
                   <EmptySpace max="-2" attributes="0"/>
               </Group>
@@ -468,7 +479,13 @@
                       <Component id="jLabel5" alignment="3" min="-2" max="-2" attributes="0"/>
                       <Component id="emitButton" alignment="3" min="-2" max="-2" attributes="0"/>
                   </Group>
-                  <EmptySpace pref="40" max="32767" attributes="0"/>
+                  <EmptySpace max="-2" attributes="0"/>
+                  <Group type="103" groupAlignment="3" attributes="0">
+                      <Component id="snapToSceneCheckbox" alignment="3" min="-2" max="-2" attributes="0"/>
+                      <Component id="snapToGridCheckbox" alignment="3" min="-2" max="-2" attributes="0"/>
+                      <Component id="selectTerrainCheckbox" alignment="3" min="-2" max="-2" attributes="0"/>
+                  </Group>
+                  <EmptySpace pref="15" max="32767" attributes="0"/>
               </Group>
           </Group>
         </DimensionLayout>
@@ -601,6 +618,46 @@
             <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="emitButtonActionPerformed"/>
           </Events>
         </Component>
+        <Component class="javax.swing.JCheckBox" name="snapToSceneCheckbox">
+          <Properties>
+            <Property name="selected" type="boolean" value="true"/>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.snapToSceneCheckbox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+            <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.snapToSceneCheckbox.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="snapToSceneCheckboxActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JCheckBox" name="snapToGridCheckbox">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.snapToGridCheckbox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+            <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.snapToGridCheckbox.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="snapToGridCheckboxActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JCheckBox" name="selectTerrainCheckbox">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.selectTerrainCheckbox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+            <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/scenecomposer/Bundle.properties" key="SceneComposerTopComponent.selectTerrainCheckbox.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="selectTerrainCheckboxActionPerformed"/>
+          </Events>
+        </Component>
       </SubComponents>
     </Container>
   </SubComponents>

+ 62 - 5
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneComposerTopComponent.java

@@ -132,6 +132,9 @@ public final class SceneComposerTopComponent extends TopComponent implements Sce
         jSeparator6 = new javax.swing.JSeparator();
         jLabel5 = new javax.swing.JLabel();
         emitButton = new javax.swing.JButton();
+        snapToSceneCheckbox = new javax.swing.JCheckBox();
+        snapToGridCheckbox = new javax.swing.JCheckBox();
+        selectTerrainCheckbox = new javax.swing.JCheckBox();
 
         setBackground(new java.awt.Color(204, 204, 204));
 
@@ -421,6 +424,31 @@ public final class SceneComposerTopComponent extends TopComponent implements Sce
             }
         });
 
+        snapToSceneCheckbox.setSelected(true);
+        org.openide.awt.Mnemonics.setLocalizedText(snapToSceneCheckbox, org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.snapToSceneCheckbox.text")); // NOI18N
+        snapToSceneCheckbox.setToolTipText(org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.snapToSceneCheckbox.toolTipText")); // NOI18N
+        snapToSceneCheckbox.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                snapToSceneCheckboxActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(snapToGridCheckbox, org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.snapToGridCheckbox.text")); // NOI18N
+        snapToGridCheckbox.setToolTipText(org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.snapToGridCheckbox.toolTipText")); // NOI18N
+        snapToGridCheckbox.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                snapToGridCheckboxActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(selectTerrainCheckbox, org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.selectTerrainCheckbox.text")); // NOI18N
+        selectTerrainCheckbox.setToolTipText(org.openide.util.NbBundle.getMessage(SceneComposerTopComponent.class, "SceneComposerTopComponent.selectTerrainCheckbox.toolTipText")); // NOI18N
+        selectTerrainCheckbox.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                selectTerrainCheckboxActionPerformed(evt);
+            }
+        });
+
         javax.swing.GroupLayout jPanel4Layout = new javax.swing.GroupLayout(jPanel4);
         jPanel4.setLayout(jPanel4Layout);
         jPanel4Layout.setHorizontalGroup(
@@ -430,12 +458,20 @@ public final class SceneComposerTopComponent extends TopComponent implements Sce
             .addGroup(jPanel4Layout.createSequentialGroup()
                 .addContainerGap()
                 .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(jSeparator6, javax.swing.GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE)
                     .addGroup(jPanel4Layout.createSequentialGroup()
                         .addGap(10, 10, 10)
-                        .addComponent(jLabel5)
-                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
-                        .addComponent(emitButton, javax.swing.GroupLayout.DEFAULT_SIZE, 302, Short.MAX_VALUE))
-                    .addComponent(jSeparator6, javax.swing.GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE))
+                        .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                            .addGroup(jPanel4Layout.createSequentialGroup()
+                                .addComponent(snapToSceneCheckbox)
+                                .addGap(18, 18, 18)
+                                .addComponent(snapToGridCheckbox)
+                                .addGap(18, 18, 18)
+                                .addComponent(selectTerrainCheckbox))
+                            .addGroup(jPanel4Layout.createSequentialGroup()
+                                .addComponent(jLabel5)
+                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                                .addComponent(emitButton, javax.swing.GroupLayout.DEFAULT_SIZE, 302, Short.MAX_VALUE)))))
                 .addContainerGap())
         );
         jPanel4Layout.setVerticalGroup(
@@ -450,7 +486,12 @@ public final class SceneComposerTopComponent extends TopComponent implements Sce
                 .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                     .addComponent(jLabel5)
                     .addComponent(emitButton))
-                .addContainerGap(40, Short.MAX_VALUE))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(snapToSceneCheckbox)
+                    .addComponent(snapToGridCheckbox)
+                    .addComponent(selectTerrainCheckbox))
+                .addContainerGap(15, Short.MAX_VALUE))
         );
 
         javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
@@ -572,6 +613,18 @@ private void scaleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-F
         toolController.showEditTool(tool);
     }//GEN-LAST:event_rotateButtonActionPerformed
 
+    private void snapToSceneCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_snapToSceneCheckboxActionPerformed
+        toolController.setSnapToScene(snapToSceneCheckbox.isSelected());
+    }//GEN-LAST:event_snapToSceneCheckboxActionPerformed
+
+    private void snapToGridCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_snapToGridCheckboxActionPerformed
+        toolController.setSnapToGrid(snapToGridCheckbox.isSelected());
+    }//GEN-LAST:event_snapToGridCheckboxActionPerformed
+
+    private void selectTerrainCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectTerrainCheckboxActionPerformed
+        toolController.setSelectTerrain(selectTerrainCheckbox.isSelected());
+    }//GEN-LAST:event_selectTerrainCheckboxActionPerformed
+
     // Variables declaration - do not modify//GEN-BEGIN:variables
     private javax.swing.JButton camToCursorSelectionButton;
     private javax.swing.JButton createPhysicsMeshButton;
@@ -611,8 +664,11 @@ private void scaleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-F
     private javax.swing.JLabel sceneInfoLabel2;
     private javax.swing.JPanel sceneInfoPanel;
     private javax.swing.JToggleButton selectButton;
+    private javax.swing.JCheckBox selectTerrainCheckbox;
     private javax.swing.JToggleButton showGridToggleButton;
     private javax.swing.JToggleButton showSelectionToggleButton;
+    private javax.swing.JCheckBox snapToGridCheckbox;
+    private javax.swing.JCheckBox snapToSceneCheckbox;
     private javax.swing.ButtonGroup spatialModButtonGroup;
     // End of variables declaration//GEN-END:variables
 
@@ -862,6 +918,7 @@ private void scaleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-F
             return;
         } else {
             if (toolController != null) {
+                toolController.setSelectedSpatial(spatial);
                 toolController.updateSelection(spatial.getLookup().lookup(Spatial.class));
             }
         }

+ 61 - 4
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java

@@ -18,8 +18,10 @@ import com.jme3.collision.CollisionResult;
 import com.jme3.collision.CollisionResults;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.undoredo.AbstractUndoableSceneEdit;
 import com.jme3.gde.core.undoredo.SceneUndoRedoManager;
+import com.jme3.input.event.KeyInputEvent;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState.BlendMode;
 import com.jme3.material.RenderState.FaceCullMode;
@@ -39,6 +41,7 @@ import com.jme3.scene.Spatial;
 import com.jme3.scene.debug.Arrow;
 import com.jme3.scene.debug.WireBox;
 import com.jme3.scene.shape.Quad;
+import java.util.Iterator;
 import java.util.concurrent.Callable;
 import org.openide.loaders.DataObject;
 import org.openide.util.Lookup;
@@ -178,7 +181,7 @@ public abstract class SceneEditTool {
     /**
      * Called when the mouse is moved but not dragged (ie no buttons are pressed)
      */
-    public abstract void mouseMoved(Vector2f screenCoord);
+    public abstract void mouseMoved(Vector2f screenCoord, JmeNode rootNode, DataObject dataObject, JmeSpatial selectedSpatial);
 
     /**
      * Called when the mouse is moved while the primary button is down
@@ -190,6 +193,8 @@ public abstract class SceneEditTool {
      */
     public abstract void draggedSecondary(Vector2f screenCoord, boolean pressed, JmeNode rootNode, DataObject currentDataObject);
 
+    public void keyPressed(KeyInputEvent kie) {}
+    
     /**
      * Call when an action is performed that requires the scene to be saved
      * and an undo can be performed
@@ -223,11 +228,23 @@ public abstract class SceneEditTool {
      */
     protected Vector3f pickWorldLocation(Camera cam, Vector2f mouseLoc, JmeNode jmeRootNode) {
         Node rootNode = jmeRootNode.getLookup().lookup(Node.class);
-        return pickWorldLocation(cam, mouseLoc, rootNode);
+        return pickWorldLocation(cam, mouseLoc, rootNode, null);
+    }
+    
+    /**
+     * Pick anything except the excluded spatial
+     * @param excludeSpat to not pick
+     */
+    protected Vector3f pickWorldLocation(Camera cam, Vector2f mouseLoc, JmeNode jmeRootNode, JmeSpatial excludeSpat) {
+        Node rootNode = jmeRootNode.getLookup().lookup(Node.class);
+        return pickWorldLocation(cam, mouseLoc, rootNode, excludeSpat);
     }
 
-    protected Vector3f pickWorldLocation(Camera cam, Vector2f mouseLoc, Node rootNode) {
-        CollisionResult cr = pick(cam, mouseLoc, rootNode);
+    protected Vector3f pickWorldLocation(Camera cam, Vector2f mouseLoc, Node rootNode, JmeSpatial excludeSpat) {
+        Spatial exclude = null;
+        if (excludeSpat != null)
+            exclude = excludeSpat.getLookup().lookup(Spatial.class);
+        CollisionResult cr = doPick(cam, mouseLoc, rootNode, exclude);
         if (cr != null) {
             return cr.getContactPoint();
         } else {
@@ -235,6 +252,46 @@ public abstract class SceneEditTool {
         }
     }
 
+    private CollisionResult doPick(Camera cam, Vector2f mouseLoc, Node node, Spatial exclude) {
+        CollisionResults results = new CollisionResults();
+        Ray ray = new Ray();
+        Vector3f pos = cam.getWorldCoordinates(mouseLoc, 0).clone();
+        Vector3f dir = cam.getWorldCoordinates(mouseLoc, 0.1f).clone();
+        dir.subtractLocal(pos).normalizeLocal();
+        ray.setOrigin(pos);
+        ray.setDirection(dir);
+        node.collideWith(ray, results);
+        CollisionResult result = null;
+        if (exclude == null)
+            result = results.getClosestCollision();
+        else {
+            Iterator<CollisionResult> it = results.iterator();
+            while (it.hasNext()) {
+                CollisionResult cr = it.next();
+                if (isExcluded(cr.getGeometry(), exclude))
+                    continue;
+                else
+                    return cr;
+            }
+            
+        }
+        return result;
+    }
+    
+    /**
+     * Is the selected spatial the one we want to exclude from the picking?
+     * Recursively looks up the parents to find out.
+     */
+    private boolean isExcluded(Spatial s, Spatial exclude) {
+        if (s.equals(exclude))
+            return true;
+        
+        if (s.getParent() != null) {
+            return isExcluded(s.getParent(), exclude);
+        }
+        return false;
+    }
+    
     /**
      * Pick a part of the axis marker. The result is a Vector3f that represents
      * what part of the axis was selected.

+ 3 - 2
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/MoveTool.java

@@ -8,6 +8,7 @@ import com.jme3.asset.AssetManager;
 import com.jme3.bullet.control.CharacterControl;
 import com.jme3.bullet.control.RigidBodyControl;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.undoredo.AbstractUndoableSceneEdit;
 import com.jme3.gde.scenecomposer.SceneComposerToolController;
 import com.jme3.gde.scenecomposer.SceneEditTool;
@@ -87,7 +88,7 @@ public class MoveTool extends SceneEditTool {
     }
 
     @Override
-    public void mouseMoved(Vector2f screenCoord) {
+    public void mouseMoved(Vector2f screenCoord, JmeNode rootNode, DataObject currentDataObject, JmeSpatial selectedSpatial) {
         if (pickedPlane == null) {
             highlightAxisMarker(camera, screenCoord, axisPickType);
         }
@@ -127,7 +128,7 @@ public class MoveTool extends SceneEditTool {
             plane.setLocalTranslation(startLoc);
         }
         
-        Vector3f planeHit = pickWorldLocation(camera, screenCoord, plane);
+        Vector3f planeHit = pickWorldLocation(camera, screenCoord, plane, null);
         if (planeHit == null)
             return;
 

+ 2 - 1
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/RotateTool.java

@@ -6,6 +6,7 @@ package com.jme3.gde.scenecomposer.tools;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.undoredo.AbstractUndoableSceneEdit;
 import com.jme3.gde.scenecomposer.SceneComposerToolController;
 import com.jme3.gde.scenecomposer.SceneEditTool;
@@ -58,7 +59,7 @@ public class RotateTool extends SceneEditTool {
     }
  
     @Override
-    public void mouseMoved(Vector2f screenCoord) {
+    public void mouseMoved(Vector2f screenCoord, JmeNode rootNode, DataObject currentDataObject, JmeSpatial selectedSpatial) {
         if (pickedPlane == null) {
             highlightAxisMarker(camera, screenCoord, axisPickType);
         }

+ 2 - 1
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/ScaleTool.java

@@ -6,6 +6,7 @@ package com.jme3.gde.scenecomposer.tools;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.undoredo.AbstractUndoableSceneEdit;
 import com.jme3.gde.scenecomposer.SceneComposerToolController;
 import com.jme3.gde.scenecomposer.SceneEditTool;
@@ -57,7 +58,7 @@ public class ScaleTool extends SceneEditTool {
     }
 
     @Override
-    public void mouseMoved(Vector2f screenCoord) {
+    public void mouseMoved(Vector2f screenCoord, JmeNode rootNode, DataObject currentDataObject, JmeSpatial selectedSpatial) {
         if (pickedPlane == null) {
             highlightAxisMarker(camera, screenCoord, axisPickType, true);
         }

+ 751 - 34
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/SelectTool.java

@@ -6,77 +6,794 @@ package com.jme3.gde.scenecomposer.tools;
 
 import com.jme3.gde.core.sceneexplorer.SceneExplorerTopComponent;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.sceneviewer.SceneViewerTopComponent;
+import com.jme3.gde.core.undoredo.AbstractUndoableSceneEdit;
 import com.jme3.gde.scenecomposer.SceneEditTool;
+import com.jme3.input.KeyInput;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Quaternion;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
+import com.jme3.terrain.Terrain;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import org.openide.loaders.DataObject;
 
 /**
- *
+ * This duplicates the Blender manipulate tool.
+ * It supports quick access to Grab, Rotate, and Scale operations
+ * by typing one of the following keys: 'g', 'r', or 's'
+ * Those keys can be followed by an axis key to specify what axis
+ * to perform the transformation: x, y, z
+ * Then, after the operation and axis are selected, you can type in a
+ * number and then hit 'enter' to complete the transformation.
+ * 
+ * Ctrl+Shift+D will duplicate an object
+ * X will delete an object
+ * 
+ * ITEMS TO FINISH:
+ * 1) fixed scale and rotation values by holding Ctrl and dragging mouse
+ * BUGS:
+ * 1) window always needs focus from primary click when it should focus from secondary and middle mouse
+ * 2) ESC will not reset currentState (ESC get hijacked)
+ * 3) rotation towards screen is busted (wrong axis)
+ * 
  * @author Brent Owens
  */
 public class SelectTool extends SceneEditTool {
 
     protected Spatial selected;
-    private boolean wasDragging = false;
+    
+    private enum State {translate, rotate, scale};
+    private State currentState = null;
+    
+    private enum Axis {x, y, z};
+    private Axis currentAxis = null;
+    
+    private StringBuilder numberBuilder = new StringBuilder(); // gets appended with numbers
+    
+    private Quaternion startRot;
+    private Vector3f startTrans;
+    private Vector3f startScale;
+    
+    private boolean wasDraggingL = false;
+    private boolean wasDraggingR = false;
+    private boolean wasDownR = false;
+    private boolean ctrlDown = false;
+    private boolean shiftDown = false;
+    private boolean altDown = false;
+    
+    private MoveUndo moving;
+    private ScaleUndo scaling;
+    private RotateUndo rotating;
+    private Vector2f startMouseCoord; // for scaling and rotation
+    private Vector2f startSelectedCoord; // for scaling and rotation
+    private float lastRotAngle; // used for rotation
 
+    /**
+     * This is stateful:
+     * First it checks for a command (rotate, translate, delete, etc..)
+     * Then it checks for an axis (x,y,z)
+     * Then it checks for a number (user typed a number
+     * Then, finally, it checks if Enter was hit.
+     * 
+     * If either of the commands was actioned, the preceeding states/axis/amount
+     * will be reset. For example if the user types: G Y 2 R
+     * Then it will:
+     * 1) Set state as 'Translate' for the G (grab)
+     * 2) Set the axis as 'Y'; it will translate along the Y axis
+     * 3) Distance will be 2, when the 2 key is hit
+     * 4) Distance, Axis, and state are then reset because a new state was set: Rotate
+     * it won't actually translate because 'Enter' was not hit and 'R' reset the state.
+     * 
+     */
+    @Override
+    public void keyPressed(KeyInputEvent kie) {
+        
+        checkModificatorKeys(kie); // alt,shift,ctrl
+        
+        if (selected == null)
+            return; // only do anything if a spatial is selected
+        
+        // key released
+        if (kie.isReleased()) {
+            boolean commandUsed = checkCommandKey(kie);
+            boolean stateChange = checkStateKey(kie);
+            boolean axisChange = checkAxisKey(kie);
+            boolean numberChange = checkNumberKey(kie);
+            boolean enterHit = checkEnterHit(kie);
+            boolean escHit = checkEscHit(kie);
+            
+            if (commandUsed)
+                return; // commands take priority
+            
+            if (stateChange) {
+                currentAxis = null;
+                numberBuilder = new StringBuilder();
+                recordInitialState(selected);
+            }
+            else if (axisChange) {}
+            else if (numberChange) {}
+            else if (enterHit) {
+                if (currentState != null && numberBuilder.length() > 0) {
+                    applyKeyedChangeState(selected);
+                    clearState(false);
+                } 
+            }
+            
+            
+            // -----------------------
+            // reset conditions below:
+            
+            if (escHit) {
+                if (moving != null)
+                    moving.sceneUndo();
+                
+                moving = null;
+                clearState();
+            }
+            
+            if (!stateChange && !axisChange && !numberChange && !enterHit && !escHit) {
+                // nothing valid was hit, reset the state
+                //clearState(); // this will be 
+            }
+        }
+    }
+    
+    /**
+     * Abort any manipulations
+     */
+    private void clearState() {
+        clearState(true);
+    }
+    
+    private void clearState(boolean resetSelected) {
+        if (resetSelected && selected != null) {
+            // reset the transforms
+            if (startRot != null)
+                selected.setLocalRotation(startRot);
+            if (startTrans != null)
+                selected.setLocalTranslation(startTrans);
+            if (startScale != null)
+                selected.setLocalScale(startScale);
+        }
+        currentState = null;
+        currentAxis = null;
+        numberBuilder = new StringBuilder();
+        startRot = null;
+        startTrans = null;
+        startScale = null;
+        startMouseCoord = null;
+        startSelectedCoord = null;
+    }
+    
+    
+    private void recordInitialState(Spatial selected) {
+        startRot = selected.getLocalRotation().clone();
+        startTrans = selected.getLocalTranslation().clone();
+        startScale = selected.getLocalScale().clone();
+    }
+    
+    /**
+     * Applies the changes entered by a number, not by mouse.
+     * Translate: adds the value to the current local translation
+     * Rotate: rotates by X degrees
+     * Scale: scale the current scale by X amount
+     */
+    private void applyKeyedChangeState(Spatial selected) {
+        Float value = null;
+        try {
+            value = new Float(numberBuilder.toString());
+        } catch (NumberFormatException e){
+            return;
+        }
+        
+        if (currentState == State.translate) {
+            float x = 0, y= 0, z = 0;
+            if (currentAxis == Axis.x)
+                x = value;
+            else if (currentAxis == Axis.y)
+                y = value;
+            else if (currentAxis == Axis.z)
+                z = value;
+            Vector3f before = selected.getLocalTranslation().clone();
+            Vector3f after = selected.getLocalTranslation().addLocal(x, y, z);
+            selected.setLocalTranslation(after);
+            actionPerformed(new MoveUndo(selected, before, after));
+        } else if (currentState == State.scale) {
+            float x = 1, y= 1, z = 1;
+            if (currentAxis == Axis.x)
+                x = value;
+            else if (currentAxis == Axis.y)
+                y = value;
+            else if (currentAxis == Axis.z)
+                z = value;
+            else if (currentAxis == null) {
+                x = value;
+                y = value;
+                z = value;
+            }
+            Vector3f before = selected.getLocalScale().clone();
+            Vector3f after = selected.getLocalScale().multLocal(x, y, z);
+            selected.setLocalScale(after);
+            actionPerformed(new ScaleUndo(selected, before, after));
+        } else if (currentState == State.rotate) {
+            float x = 0, y= 0, z = 0;
+            if (currentAxis == Axis.x)
+                x = 1;
+            else if (currentAxis == Axis.y)
+                y = 1;
+            else if (currentAxis == Axis.z)
+                z = 1;
+            Vector3f axis = new Vector3f(x,y,z);
+            Quaternion initialRot = selected.getLocalRotation().clone();
+            Quaternion rot = new Quaternion();
+            rot = rot.fromAngleAxis(value*FastMath.DEG_TO_RAD, axis);
+            selected.setLocalRotation(selected.getLocalRotation().mult(rot));
+            RotateUndo undo = new RotateUndo(selected, initialRot, rot);
+            actionPerformed(undo);
+            toolController.updateSelection(null);// force a re-draw of the bbox shape
+            toolController.updateSelection(selected);
+        }
+    }
+    
+    private void checkModificatorKeys(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_LCONTROL || kie.getKeyCode() == KeyInput.KEY_RCONTROL)
+            ctrlDown = kie.isPressed();
+        
+        if (kie.getKeyCode() == KeyInput.KEY_LSHIFT || kie.getKeyCode() == KeyInput.KEY_RSHIFT)
+            shiftDown = kie.isPressed();
+        
+        if (kie.getKeyCode() == KeyInput.KEY_LMENU || kie.getKeyCode() == KeyInput.KEY_RMENU)
+            altDown = kie.isPressed();
+    }
+    
+    private boolean checkCommandKey(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_D) {
+            if (ctrlDown && shiftDown) {
+                duplicateSelected();
+                return true;
+            }
+        }
+        // X will only delete if the user isn't already transforming
+        if (currentState == null && kie.getKeyCode() == KeyInput.KEY_X) {
+            if (!ctrlDown && !shiftDown) {
+                deleteSelected();
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    private boolean checkStateKey(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_G) {
+            currentState = State.translate;
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_R) {
+            currentState = State.rotate;
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_S) {
+            currentState = State.scale;
+            return true;
+        }
+        return false;
+    }
+    
+    private boolean checkAxisKey(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_X) {
+            currentAxis = Axis.x;
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_Y) {
+            currentAxis = Axis.y;
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_Z) {
+            currentAxis = Axis.z;
+            return true;
+        }
+        return false;
+    }
+    
+    private boolean checkNumberKey(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_MINUS) {
+            if (numberBuilder.length() > 0) {
+                if (numberBuilder.charAt(0) == '-') {
+                    numberBuilder.replace(0, 1, "");
+                } else {
+                    numberBuilder.insert(0, '-');
+                }
+            } else
+                numberBuilder.append('-');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_0) {
+            numberBuilder.append('0');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_1) {
+            numberBuilder.append('1');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_2) {
+            numberBuilder.append('2');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_3) {
+            numberBuilder.append('3');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_4) {
+            numberBuilder.append('4');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_5) {
+            numberBuilder.append('5');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_6) {
+            numberBuilder.append('6');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_7) {
+            numberBuilder.append('7');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_8) {
+            numberBuilder.append('8');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_9) {
+            numberBuilder.append('9');
+            return true;
+        } else if (kie.getKeyCode() == KeyInput.KEY_PERIOD) {
+            if (numberBuilder.indexOf(".") == -1){ // if it doesn't exist yet
+                if (numberBuilder.length() == 0 ||
+                    (numberBuilder.length() == 1 && numberBuilder.charAt(0) == '-'))
+                    numberBuilder.append("0.");
+                else
+                    numberBuilder.append(".");
+            }
+            return true;
+        }
+        
+        return false;
+    }
+    
+    private boolean checkEnterHit(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_RETURN) {
+            return true;
+        }
+        return false;
+    }
+    
+    private boolean checkEscHit(KeyInputEvent kie) {
+        if (kie.getKeyCode() == KeyInput.KEY_ESCAPE) {
+            return true;
+        }
+        return false;
+    }
+    
     @Override
     public void actionPrimary(Vector2f screenCoord, boolean pressed, final JmeNode rootNode, DataObject dataObject) {
-        if (!pressed && !wasDragging) {
-            // mouse released and wasn't dragging, select a new spatial
-            final Spatial result = pickWorldSpatial(getCamera(), screenCoord, rootNode);
-
-            java.awt.EventQueue.invokeLater(new Runnable() {
-
-                public void run() {
+        if (!pressed) {
+            // left mouse released
+            if (!wasDraggingL) {
+                // left mouse pressed
+                if (currentState != null) {
+                    // finish manipulating the spatial
+                    if (moving != null) {
+                        moving.after = selected.getLocalTranslation().clone();
+                        actionPerformed(moving);
+                        moving = null;
+                        clearState(false);
+                    } else if (scaling != null) {
+                        scaling.after = selected.getLocalScale().clone();
+                        actionPerformed(scaling);
+                        scaling = null;
+                        clearState(false);
+                    } else if (rotating != null) {
+                        rotating.after = selected.getLocalRotation().clone();
+                        actionPerformed(rotating);
+                        rotating = null;
+                        clearState(false);
+                    }
+                } else {
+                    // mouse released and wasn't dragging, place cursor
+                    final Vector3f result = pickWorldLocation(getCamera(), screenCoord, rootNode);
                     if (result != null) {
-//                        System.out.println(rootNode.getChild(result).getName());
-                        SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode.getChild(result)});
-                        SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode.getChild(result));
-
-                    } else {
-                        SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode});
-                        SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode);
+                        if (toolController.isSnapToGrid())
+                            result.set(Math.round(result.x), result.y, Math.round(result.z));
+                        toolController.doSetCursorLocation(result);
                     }
                 }
-            });
-
-            if (result != null) {
-                updateToolsTransformation();
             }
-        }
-
-        if (!pressed) {
-            wasDragging = false;
+            wasDraggingL = false;
         }
     }
 
     @Override
     public void actionSecondary(final Vector2f screenCoord, boolean pressed, final JmeNode rootNode, DataObject dataObject) {
-        if (!pressed && !wasDragging) {
-            final Vector3f result = pickWorldLocation(getCamera(), screenCoord, rootNode);
-            if (result != null) {
-                toolController.doSetCursorLocation(result);
+        if (pressed) {
+            // mouse down
+            
+            if (!wasDraggingR && !wasDownR) { // wasn't dragging and was not down already
+                // pick on the spot
+                Spatial s = pickWorldSpatial(camera, screenCoord, rootNode);
+                if (!toolController.selectTerrain() && isTerrain(s) ) {
+                    // only select non-terrain
+                    selected = null;
+                    return;
+                } else {
+                    
+                    // climb up and find the Model Node (parent) and select that, don't select the geom
+                    Spatial linkNodeParent = findModelNodeParent(s);
+                    if (linkNodeParent != null)
+                        s = linkNodeParent;
+                    else
+                        return;
+                    final Spatial selec = s;
+                    selected = selec;
+                    java.awt.EventQueue.invokeLater(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (selec != null) {
+                                SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode.getChild(selec)});
+                                SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode.getChild(selec));
+                            } else {
+                                SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode});
+                                SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode);
+                            }
+                        }
+                    });
+                }
+                
+                toolController.updateSelection(selected);
             }
-        }
-        if (!pressed) {
-            wasDragging = false;
+            wasDownR = true;
+        } else {
+            // mouse up, stop everything
+            wasDownR = false;
+            wasDraggingR = false;
         }
     }
+    
+    /**
+     * Climb up the spatial until we find the first node parent.
+     * TODO: use userData to determine the actual model's parent.
+     */
+    private Spatial findModelNodeParent(Spatial child) {
+        if (child instanceof Node)
+            return child;
+        
+        if (child.getParent() != null)
+            return findModelNodeParent(child.getParent());
+        
+        return null;
+    }
 
     @Override
-    public void mouseMoved(Vector2f screenCoord) {
+    public void mouseMoved(Vector2f screenCoord, JmeNode rootNode, DataObject currentDataObject, JmeSpatial selectedSpatial) {
+        if (currentState != null) {
+            //if (currentAxis != null) {
+                // manipulate from specific axis
+                handleMouseManipulate(screenCoord, currentState, currentAxis, rootNode, currentDataObject, selectedSpatial);
+            //} else {
+                // manipulate from screen coordinates
+            //}
+        }
     }
 
     @Override
     public void draggedPrimary(Vector2f screenCoord, boolean pressed, JmeNode rootNode, DataObject currentDataObject) {
-        wasDragging = pressed;
+        wasDraggingL = pressed;
     }
 
     @Override
     public void draggedSecondary(Vector2f screenCoord, boolean pressed, JmeNode rootNode, DataObject currentDataObject) {
-        wasDragging = pressed;
+        wasDraggingR = pressed;
+    }
+    
+    /**
+     * Manipulate the spatial
+     */
+    private void handleMouseManipulate( Vector2f screenCoord, 
+                                        State state, 
+                                        Axis axis, 
+                                        JmeNode rootNode, 
+                                        DataObject currentDataObject, 
+                                        JmeSpatial selectedSpatial) 
+    {
+        if (state == State.translate) {
+            doMouseTranslate(axis, screenCoord, rootNode, selectedSpatial);
+        } 
+        else if (state == State.scale) {
+            doMouseScale(axis, screenCoord, rootNode, selectedSpatial);
+        } 
+        else if (state == State.rotate) {
+            doMouseRotate(axis, screenCoord, rootNode, selectedSpatial);
+        }
+            
+    }
+    
+    private void doMouseTranslate(Axis axis, Vector2f screenCoord, JmeNode rootNode, JmeSpatial selectedSpatial) {
+        // free form translation
+        if (axis == null) {
+            if (toolController.isSnapToScene()) {
+                if (moving == null)
+                    moving = new MoveUndo(selected, selected.getLocalTranslation().clone(), null);
+                Vector3f loc = pickWorldLocation(camera, screenCoord, rootNode, selectedSpatial);
+                if (loc != null) {
+                    //Node sel = selectedSpatial.getLookup().lookup(Node.class);
+                    if (toolController.isSnapToGrid()) {
+                        selected.setLocalTranslation(loc.set(Math.round(loc.x), loc.y, Math.round(loc.z)));
+                    } else {
+                        selected.setLocalTranslation(loc);
+                    }
+                }
+            } else {
+                //TODO drag alone a camera-oriented axis
+            }
+        } else {
+            //snap to specific axis, ignoring snapToScene
+            if (moving == null)
+                moving = new MoveUndo(selected, selected.getLocalTranslation().clone(), null);
+            Vector3f loc = pickWorldLocation(camera, screenCoord, rootNode, selectedSpatial);
+            if (loc != null) {
+                if (toolController.isSnapToGrid())
+                    loc.set(Math.round(loc.x), Math.round(loc.y), Math.round(loc.z)); // round the values
+                
+                //Node sel = selectedSpatial.getLookup().lookup(Node.class);
+                Vector3f prev = selected.getLocalTranslation().clone();
+                Vector3f newLoc = new Vector3f(prev);
+                if (axis == Axis.x)
+                    newLoc.x = loc.x;
+                else if (axis == Axis.y)
+                    newLoc.y = loc.y;
+                if (axis == Axis.z)
+                    newLoc.z = loc.z;
+                
+                selected.setLocalTranslation(newLoc);
+            }
+        }
+    }
+    
+    private void doMouseScale(Axis axis, Vector2f screenCoord, JmeNode rootNode, JmeSpatial selectedSpatial) {
+        // scale based on the original mouse position and original model-to-screen position
+        // and compare that to the distance from the new mouse position and the original distance
+        if (startMouseCoord == null)
+            startMouseCoord = screenCoord.clone();
+        if (startSelectedCoord == null) {
+            Vector3f screen = getCamera().getScreenCoordinates(selected.getWorldTranslation());
+            startSelectedCoord = new Vector2f(screen.x, screen.y);
+        }
+
+        if (scaling == null)
+            scaling = new ScaleUndo(selected, selected.getLocalScale().clone(), null);
+
+        float origDist = startMouseCoord.distanceSquared(startSelectedCoord);
+        float newDist = screenCoord.distanceSquared(startSelectedCoord);
+        if (origDist == 0)
+            origDist = 1;
+        float ratio = newDist/origDist;
+        Vector3f prev = selected.getLocalScale();
+        if (axis == Axis.x)
+            selected.setLocalScale(ratio, prev.y, prev.z);
+        else if (axis == Axis.y)
+            selected.setLocalScale(prev.x, ratio, prev.z);
+        else if (axis == Axis.z)
+            selected.setLocalScale(prev.x, prev.y, ratio);
+        else
+            selected.setLocalScale(ratio, ratio, ratio);
+    }
+    
+    private void doMouseRotate(Axis axis, Vector2f screenCoord, JmeNode rootNode, JmeSpatial selectedSpatial) {
+        if (startMouseCoord == null)
+            startMouseCoord = screenCoord.clone();
+        if (startSelectedCoord == null) {
+            Vector3f screen = getCamera().getScreenCoordinates(selected.getWorldTranslation());
+            startSelectedCoord = new Vector2f(screen.x, screen.y);
+        }
+
+        if (rotating == null)
+            rotating = new RotateUndo(selected, selected.getLocalRotation().clone(), null);
+        
+        Vector2f origRot = startMouseCoord.subtract(startSelectedCoord);
+        Vector2f newRot = screenCoord.subtract(startSelectedCoord);
+        float newRotAngle = origRot.angleBetween(newRot);
+        float temp = newRotAngle;
+        
+        if (lastRotAngle != 0)
+            newRotAngle -= lastRotAngle;
+        
+        lastRotAngle = temp;
+        
+        Quaternion rotate = new Quaternion();
+        if (axis == Axis.x) {
+            rotate = rotate.fromAngleAxis(newRotAngle, Vector3f.UNIT_X);
+        } else if (axis == Axis.y) {
+            rotate = rotate.fromAngleAxis(newRotAngle, Vector3f.UNIT_Y);
+        } else if (axis == Axis.z) {
+            rotate = rotate.fromAngleAxis(newRotAngle, Vector3f.UNIT_Z);
+        } else {
+            Vector3f screen = getCamera().getScreenCoordinates(selected.getWorldTranslation());
+            rotate = rotate.fromAngleAxis(newRotAngle, screen.normalize());
+        }
+        selected.setLocalRotation(selected.getLocalRotation().mult(rotate));
+        
+        
+    }
+    
+    private void duplicateSelected() {
+        if (selected == null)
+            return;
+        Spatial clone = selected.clone();
+        clone.move(1, 0, 1);
+    
+        selected.getParent().attachChild(clone);
+        actionPerformed(new DuplicateUndo(clone, selected.getParent()));
+        selected = clone;
+        final Spatial cloned = clone;
+        final JmeNode rootNode = toolController.getRootNode();
+        refreshSelected(rootNode, selected.getParent());
+        
+        java.awt.EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (cloned != null) {
+                    SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode.getChild(cloned)});
+                    SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode.getChild(cloned));
+                } else {
+                    SceneViewerTopComponent.findInstance().setActivatedNodes(new org.openide.nodes.Node[]{rootNode});
+                    SceneExplorerTopComponent.findInstance().setSelectedNode(rootNode);
+                }
+            }
+        });
+        
+        // set to automatically 'grab'/'translate' the new cloned model
+        toolController.updateSelection(selected);
+        currentState = State.translate;
+        currentAxis = null;
+    }
+    
+    private void deleteSelected() {
+        if (selected == null)
+            return;
+        Node parent = selected.getParent();
+        selected.removeFromParent();
+        actionPerformed(new DeleteUndo(selected, parent));
+        
+        selected = null;
+        toolController.updateSelection(selected);
+        
+        final JmeNode rootNode = toolController.getRootNode();
+        refreshSelected(rootNode, parent);
+    }
+    
+    private void refreshSelected(final JmeNode jmeRootNode, final Node parent) {
+        java.awt.EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                jmeRootNode.getChild(parent).refresh(false);
+            }
+        });
+    }
+    
+    private class MoveUndo extends AbstractUndoableSceneEdit {
+
+        private Spatial spatial;
+        private Vector3f before,after;
+        
+        MoveUndo(Spatial spatial, Vector3f before, Vector3f after) {
+            this.spatial = spatial;
+            this.before = before;
+            this.after = after;
+        }
+        
+        @Override
+        public void sceneUndo() {
+            spatial.setLocalTranslation(before);
+        }
+
+        @Override
+        public void sceneRedo() {
+            spatial.setLocalTranslation(after);
+        }
+    }
+    
+    private class ScaleUndo extends AbstractUndoableSceneEdit {
+
+        private Spatial spatial;
+        private Vector3f before,after;
+        
+        ScaleUndo(Spatial spatial, Vector3f before, Vector3f after) {
+            this.spatial = spatial;
+            this.before = before;
+            this.after = after;
+        }
+        
+        @Override
+        public void sceneUndo() {
+            spatial.setLocalScale(before);
+        }
+
+        @Override
+        public void sceneRedo() {
+            spatial.setLocalScale(after);
+        }
+    }
+    
+    private class RotateUndo extends AbstractUndoableSceneEdit {
+
+        private Spatial spatial;
+        private Quaternion before,after;
+        
+        RotateUndo(Spatial spatial, Quaternion before, Quaternion after) {
+            this.spatial = spatial;
+            this.before = before;
+            this.after = after;
+        }
+        
+        @Override
+        public void sceneUndo() {
+            spatial.setLocalRotation(before);
+        }
+
+        @Override
+        public void sceneRedo() {
+            spatial.setLocalRotation(after);
+        }
+    }
+    
+    private class DeleteUndo extends AbstractUndoableSceneEdit {
+
+        private Spatial spatial;
+        private Node parent;
+        
+        DeleteUndo(Spatial spatial, Node parent) {
+            this.spatial = spatial;
+            this.parent = parent;
+        }
+        
+        @Override
+        public void sceneUndo() {
+            parent.attachChild(spatial);
+        }
+
+        @Override
+        public void sceneRedo() {
+            spatial.removeFromParent();
+        }
+    }
+    
+    private class DuplicateUndo extends AbstractUndoableSceneEdit {
+
+        private Spatial spatial;
+        private Node parent;
+        
+        DuplicateUndo(Spatial spatial, Node parent) {
+            this.spatial = spatial;
+            this.parent = parent;
+        }
+        
+        @Override
+        public void sceneUndo() {
+            spatial.removeFromParent();
+        }
+
+        @Override
+        public void sceneRedo() {
+            parent.attachChild(spatial);
+        }
+    }
+    
+    /**
+     * Check if the selected item is a Terrain
+     * It will climb up the parent tree to see if
+     * a parent is terrain too.
+     * Recursive call.
+     */
+    protected boolean isTerrain(Spatial s) {
+        if (s instanceof Terrain)
+            return true;
+        
+        if (s.getParent() != null) {
+            return isTerrain(s.getParent());
+        }
+        return false;
     }
+    
 }