Browse Source

Merge pull request #316 from neph1/animcomposer

Animcomposer and external change refactor
Rickard Edén 3 years ago
parent
commit
de897e084a

+ 173 - 72
jme3-core/src/com/jme3/gde/core/assets/ExternalChangeScanner.java

@@ -1,22 +1,22 @@
 /*
 /*
  * Copyright (c) 2003-2012 jMonkeyEngine
  * Copyright (c) 2003-2012 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
- * 
+ *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
  * modification, are permitted provided that the following conditions are
  * met:
  * met:
- * 
+ *
  * * Redistributions of source code must retain the above copyright
  * * Redistributions of source code must retain the above copyright
  *   notice, this list of conditions and the following disclaimer.
  *   notice, this list of conditions and the following disclaimer.
- * 
+ *
  * * Redistributions in binary form must reproduce the above copyright
  * * Redistributions in binary form must reproduce the above copyright
  *   notice, this list of conditions and the following disclaimer in the
  *   notice, this list of conditions and the following disclaimer in the
  *   documentation and/or other materials provided with the distribution.
  *   documentation and/or other materials provided with the distribution.
- * 
+ *
  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
  *   may be used to endorse or promote products derived from this software
  *   may be used to endorse or promote products derived from this software
  *   without specific prior written permission.
  *   without specific prior written permission.
- * 
+ *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
@@ -34,12 +34,23 @@ package com.jme3.gde.core.assets;
 import com.jme3.export.Savable;
 import com.jme3.export.Savable;
 import com.jme3.gde.core.scene.ApplicationLogHandler;
 import com.jme3.gde.core.scene.ApplicationLogHandler;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.scene.SceneApplication;
+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.util.SpatialUtil;
 import com.jme3.gde.core.util.SpatialUtil;
+import com.jme3.gde.core.util.TaggedSpatialFinder;
+import com.jme3.gde.core.util.datatransfer.CopyAnimationDataFromOriginal;
+import com.jme3.gde.core.util.datatransfer.CopyMaterialDataFromOriginal;
+import com.jme3.gde.core.util.datatransfer.CopyMeshDataFromOriginal;
+import com.jme3.gde.core.util.datatransfer.CopyTransformDataFromOriginal;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.Spatial;
+import java.io.IOException;
+
 import java.util.concurrent.Callable;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.Logger;
+
 import org.netbeans.api.progress.ProgressHandle;
 import org.netbeans.api.progress.ProgressHandle;
 import org.openide.DialogDisplayer;
 import org.openide.DialogDisplayer;
 import org.openide.NotifyDescriptor;
 import org.openide.NotifyDescriptor;
@@ -51,6 +62,7 @@ import org.openide.filesystems.FileObject;
 import org.openide.filesystems.FileRenameEvent;
 import org.openide.filesystems.FileRenameEvent;
 import org.openide.loaders.DataObject;
 import org.openide.loaders.DataObject;
 import org.openide.loaders.DataObjectNotFoundException;
 import org.openide.loaders.DataObjectNotFoundException;
+import org.openide.nodes.Node;
 import org.openide.util.Exceptions;
 import org.openide.util.Exceptions;
 
 
 /**
 /**
@@ -59,16 +71,19 @@ import org.openide.util.Exceptions;
  *
  *
  * @author normenhansen
  * @author normenhansen
  */
  */
-public class ExternalChangeScanner implements AssetDataPropertyChangeListener, FileChangeListener {
+public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
+        FileChangeListener {
 
 
-    private static final Logger logger = Logger.getLogger(ExternalChangeScanner.class.getName());
+    private static final Logger LOGGER =
+            Logger.getLogger(ExternalChangeScanner.class.getName());
     private static final AtomicBoolean userNotified = new AtomicBoolean(false);
     private static final AtomicBoolean userNotified = new AtomicBoolean(false);
-    protected final AssetDataObject assetDataObject;
-    protected final AssetData assetData;
-    protected FileObject originalObject;
+    private final AssetDataObject assetDataObject;
+    private final AssetData assetData;
+    private FileObject originalObject;
 
 
     public ExternalChangeScanner(AssetDataObject assetDataObject) {
     public ExternalChangeScanner(AssetDataObject assetDataObject) {
         this.assetDataObject = assetDataObject;
         this.assetDataObject = assetDataObject;
+
         assetData = assetDataObject.getLookup().lookup(AssetData.class);
         assetData = assetDataObject.getLookup().lookup(AssetData.class);
         if (assetData != null) {
         if (assetData != null) {
             String path = assetData.getProperty("ORIGINAL_PATH");
             String path = assetData.getProperty("ORIGINAL_PATH");
@@ -80,88 +95,151 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener, F
             assetDataObject.getPrimaryFile().addFileChangeListener(new FileChangeAdapter() {
             assetDataObject.getPrimaryFile().addFileChangeListener(new FileChangeAdapter() {
                 @Override
                 @Override
                 public void fileDeleted(FileEvent fe) {
                 public void fileDeleted(FileEvent fe) {
-                    logger.log(Level.INFO, "File {0} deleted, remove!", new Object[]{fe.getFile()});
+                    LOGGER.log(Level.INFO, "File {0} deleted, remove!",
+                            new Object[]{fe.getFile()});
                     assetData.removePropertyChangeListener(main);
                     assetData.removePropertyChangeListener(main);
                     fe.getFile().removeFileChangeListener(this);
                     fe.getFile().removeFileChangeListener(this);
                     if (originalObject != null) {
                     if (originalObject != null) {
-                        logger.log(Level.INFO, "Remove file change listener for {0}", originalObject);
+                        LOGGER.log(Level.INFO, "Remove file change listener "
+                                + "for {0}", originalObject);
                         originalObject.removeFileChangeListener(main);
                         originalObject.removeFileChangeListener(main);
                         originalObject = null;
                         originalObject = null;
                     }
                     }
                 }
                 }
             });
             });
         } else {
         } else {
-            logger.log(Level.WARNING, "Trying to observer changes for asset {0} which has no AssetData in Lookup.", assetDataObject.getName());
+            LOGGER.log(Level.WARNING, "Trying to observer changes for asset "
+                            + "{0} which has no AssetData in Lookup.",
+                    assetDataObject.getName());
         }
         }
     }
     }
 
 
     private void notifyUser() {
     private void notifyUser() {
         if (!userNotified.getAndSet(true)) {
         if (!userNotified.getAndSet(true)) {
             //TODO: execute on separate thread?
             //TODO: execute on separate thread?
-            java.awt.EventQueue.invokeLater(new Runnable() {
-                public void run() {
-                    NotifyDescriptor.Confirmation mesg = new NotifyDescriptor.Confirmation("Original file for " + assetDataObject.getName() + " changed\nTry and reapply mesh data to j3o file?",
-                            "Original file changed",
-                            NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.QUESTION_MESSAGE);
-                    DialogDisplayer.getDefault().notify(mesg);
-                    if (mesg.getValue() != NotifyDescriptor.Confirmation.YES_OPTION) {
-                        userNotified.set(false);
-                        return;
-                    }
-                    SceneApplication.getApplication().enqueue(new Callable<Void>() {
-                        public Void call() throws Exception {
-                            applyExternalData();
-                            return null;
-                        }
-                    });
+            java.awt.EventQueue.invokeLater(() -> {
+                final String noOption = "No";
+                final String allOption = "All";
+                final String meshOption = "Only mesh data";
+                final String animOption = "Only animation data";
+                final NotifyDescriptor.Confirmation message =
+                        new NotifyDescriptor.Confirmation("Original file for "
+                                + assetDataObject.getName() + " changed\nTry "
+                                + "and reapply data to j3o file?",
+                                "Original file changed",
+                                NotifyDescriptor.YES_NO_CANCEL_OPTION,
+                                NotifyDescriptor.QUESTION_MESSAGE);
+                message.setOptions(new Object[]{allOption, meshOption, animOption,
+                        noOption});
+                DialogDisplayer.getDefault().notify(message);
+                if (message.getValue().equals(noOption)) {
                     userNotified.set(false);
                     userNotified.set(false);
+                    return;
                 }
                 }
+                SceneApplication.getApplication().enqueue((Callable<Void>) () -> {
+                    applyExternalData(message.getValue().equals(meshOption), 
+                            message.getValue().equals(animOption));
+                    return null;
+                });
+                userNotified.set(false);
             });
             });
         } else {
         } else {
-            logger.log(Level.INFO, "User already notified about change in {0}", assetDataObject.getName());
+            LOGGER.log(Level.INFO, "User already notified about change in "
+                    + "{0}", assetDataObject.getName());
         }
         }
     }
     }
-
-    private void applyExternalData() {
-        ProgressHandle handle = ProgressHandle.createHandle("Updating file data");
+    
+    private void applyExternalData(final boolean onlyMeshData, 
+            final boolean onlyAnimData) {
+        final ProgressHandle handle = ProgressHandle.createHandle("Updating "
+                + "file "
+                + "data");
         handle.start();
         handle.start();
         try {
         try {
-            Spatial original = loadOriginalSpatial();
-            Spatial spat = (Spatial) assetDataObject.loadAsset();
-
-            SpatialUtil.updateMeshDataFromOriginal(spat, original);
-            SpatialUtil.updateMaterialDataFromOriginal(spat, original);
-            if (SpatialUtil.hasAnimations(original)) {
-                NotifyDescriptor.Confirmation mesg = new NotifyDescriptor.Confirmation("Model appears to have animations, try to import as well?\nCurrently this will unlink attachment Nodes and clear\nadded effects tracks.",
-                        "Animations Available",
-                        NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.QUESTION_MESSAGE);
-                DialogDisplayer.getDefault().notify(mesg);
-                if (mesg.getValue() == NotifyDescriptor.Confirmation.YES_OPTION) {
-                    SpatialUtil.updateAnimControlDataFromOriginal(spat, original);
-                }
+            final Spatial original = loadOriginalSpatial();
+            final Spatial spat = (Spatial) assetDataObject.loadAsset();
+            final TaggedSpatialFinder finder = new TaggedSpatialFinder();
+
+            if(!onlyMeshData && SpatialUtil.hasAnimations(original)) {
+                new CopyAnimationDataFromOriginal(finder).update(spat,
+                            original);
+                
             }
             }
+            if(!onlyAnimData){
+                new CopyMeshDataFromOriginal(finder).update(spat, original);
+            }
+            if (!onlyMeshData && !onlyAnimData) {
+                new CopyTransformDataFromOriginal(finder).update(spat, original);
+                new CopyMaterialDataFromOriginal(finder).update(spat, original);
+            }
+            // Do a complicated recurse refresh since AbstractSceneExplorerNode:refresh() isn't working
+            SceneApplication.getApplication().enqueue((Runnable) () -> {
+                Node rootNode = SceneExplorerTopComponent.findInstance().getExplorerManager().getRootContext();
+                if (rootNode instanceof JmeNode) {
+                    refreshNamedSpatial((JmeNode) rootNode, spat.getName());
+                }
+            });
             closeOriginalSpatial();
             closeOriginalSpatial();
             assetDataObject.saveAsset();
             assetDataObject.saveAsset();
-        } catch (Exception e) {
-            logger.log(Level.SEVERE, "Exception when trying to update external data.", e);
+        } catch (IOException e) {
+            LOGGER.log(Level.SEVERE, "Exception when trying to update "
+                    + "external data.", e);
         } finally {
         } finally {
             handle.finish();
             handle.finish();
         }
         }
     }
     }
+    
+    /**
+     * Look for the spatial to update using the name of the asset
+     * @param spatial
+     * @param name 
+     */
+    private void refreshNamedSpatial(JmeSpatial spatial, String name){
+        if(spatial.getName().equals(name)){
+            recurseRefresh(spatial);
+        } else {
+            for(Node s: spatial.getChildren().getNodes()){
+                if(s instanceof JmeSpatial){
+                    refreshNamedSpatial((JmeSpatial) s, name);
+                }
+                
+            }
+        }
+    }
+    
+    /**
+     * Refreshes the spatial and all children
+     * @param spatial 
+     */
+    private void recurseRefresh(JmeSpatial spatial){
+        spatial.refresh(false);
+        for(Node s: spatial.getChildren().getNodes()){
+            if(s instanceof JmeSpatial){
+                recurseRefresh((JmeSpatial) s);
+            }
+        }
+    }
 
 
     private Spatial loadOriginalSpatial() {
     private Spatial loadOriginalSpatial() {
         try {
         try {
-            DataObject dobj = DataObject.find(originalObject);
-            AssetData originalAssetData = dobj.getLookup().lookup(AssetData.class);
+            final DataObject dobj = DataObject.find(originalObject);
+            final AssetData originalAssetData =
+                    dobj.getLookup().lookup(AssetData.class);
             if (originalAssetData != null) {
             if (originalAssetData != null) {
-                Savable sav = originalAssetData.loadAsset();
+                final Savable sav = originalAssetData.loadAsset();
                 if (sav instanceof Spatial) {
                 if (sav instanceof Spatial) {
                     return (Spatial) sav;
                     return (Spatial) sav;
                 } else {
                 } else {
-                    logger.log(Level.SEVERE, "Trying to load original for {0} but it is not a Spatial: {1}", new Object[]{assetDataObject.getName(), originalObject});
+                    LOGGER.log(Level.SEVERE, "Trying to load original for {0}"
+                                    + " but it is not a Spatial: {1}",
+                            new Object[]{assetDataObject.getName(),
+                                    originalObject});
                 }
                 }
             } else {
             } else {
-                logger.log(Level.WARNING, "Could not get AssetData for {0}, original file {1}", new Object[]{assetDataObject.getName(), originalObject});
+                LOGGER.log(Level.WARNING, "Could not get AssetData for {0}, "
+                                + "original file {1}",
+                        new Object[]{assetDataObject.getName(),
+                                originalObject});
             }
             }
         } catch (DataObjectNotFoundException ex) {
         } catch (DataObjectNotFoundException ex) {
             Exceptions.printStackTrace(ex);
             Exceptions.printStackTrace(ex);
@@ -171,12 +249,16 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener, F
 
 
     private Spatial closeOriginalSpatial() {
     private Spatial closeOriginalSpatial() {
         try {
         try {
-            DataObject dobj = DataObject.find(originalObject);
-            AssetData originalAssetData = dobj.getLookup().lookup(AssetData.class);
+            final DataObject dobj = DataObject.find(originalObject);
+            final AssetData originalAssetData =
+                    dobj.getLookup().lookup(AssetData.class);
             if (originalAssetData != null) {
             if (originalAssetData != null) {
                 originalAssetData.closeAsset();
                 originalAssetData.closeAsset();
             } else {
             } else {
-                logger.log(Level.WARNING, "Could not get AssetData for {0}, original file {1}", new Object[]{assetDataObject.getName(), originalObject});
+                LOGGER.log(Level.WARNING, "Could not get AssetData for {0}, "
+                                + "original file {1}",
+                        new Object[]{assetDataObject.getName(),
+                                originalObject});
             }
             }
         } catch (DataObjectNotFoundException ex) {
         } catch (DataObjectNotFoundException ex) {
             Exceptions.printStackTrace(ex);
             Exceptions.printStackTrace(ex);
@@ -184,36 +266,49 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener, F
         return null;
         return null;
     }
     }
 
 
-    private void setObservedFilePath(String assetName) {
-        ProjectAssetManager mgr = assetDataObject.getLookup().lookup(ProjectAssetManager.class);
+    private void setObservedFilePath(final String assetName) {
+        final ProjectAssetManager mgr =
+                assetDataObject.getLookup().lookup(ProjectAssetManager.class);
         if (mgr == null) {
         if (mgr == null) {
-            logger.log(Level.WARNING, "File is not part of a jME project but tries to find original model...");
+            LOGGER.log(Level.WARNING, "File is not part of a jME project but "
+                    + "tries to find original model...");
             return;
             return;
         }
         }
-        FileObject fileObject = mgr.getAssetFileObject(assetName);
+        final FileObject fileObject = mgr.getAssetFileObject(assetName);
         //ignoring same file -> old properties files
         //ignoring same file -> old properties files
         if (fileObject != null) {
         if (fileObject != null) {
             if (!fileObject.equals(assetDataObject.getPrimaryFile())) {
             if (!fileObject.equals(assetDataObject.getPrimaryFile())) {
                 if (originalObject != null) {
                 if (originalObject != null) {
                     originalObject.removeFileChangeListener(this);
                     originalObject.removeFileChangeListener(this);
-                    logger.log(Level.INFO, "{0} stops listening for external changes on {1}", new Object[]{assetDataObject.getName(), originalObject});
+                    LOGGER.log(Level.INFO, "{0} stops listening for external "
+                                    + "changes on {1}",
+                            new Object[]{assetDataObject.getName(),
+                                    originalObject});
                 }
                 }
                 fileObject.addFileChangeListener(this);
                 fileObject.addFileChangeListener(this);
-                logger.log(Level.INFO, "{0} listening for external changes on {1}", new Object[]{assetDataObject.getName(), fileObject});
+                LOGGER.log(Level.INFO, "{0} listening for external changes on"
+                        + " {1}", new Object[]{assetDataObject.getName(),
+                        fileObject});
                 originalObject = fileObject;
                 originalObject = fileObject;
             } else {
             } else {
-                logger.log(Level.FINE, "Ignoring old reference to self for {0}", assetDataObject.getName());
+                LOGGER.log(Level.FINE, "Ignoring old reference to self for "
+                        + "{0}", assetDataObject.getName());
             }
             }
         } else {
         } else {
-            logger.log(Level.INFO, "Could not get FileObject for {0} when trying to update original data for {1}. Possibly deleted.", new Object[]{assetName, assetDataObject.getName()});
+            LOGGER.log(Level.INFO, "Could not get FileObject for {0} when "
+                    + "trying to update original data for {1}. Possibly deleted"
+                    + ".", new Object[]{assetName, assetDataObject.getName()});
             //TODO: add folder listener for when recreated
             //TODO: add folder listener for when recreated
         }
         }
     }
     }
 
 
     @Override
     @Override
-    public void assetDataPropertyChanged(String property, String before, String after) {
-        if ("ORIGINAL_PATH".equals(property)) {
-            logger.log(Level.INFO, "Notified about change in AssetData properties for {0}", assetDataObject.getName());
+    public void assetDataPropertyChanged(final String property,
+                                         final String before,
+                                         final String after) {
+        if (SpatialUtil.ORIGINAL_PATH.equals(property)) {
+            LOGGER.log(Level.INFO, "Notified about change in AssetData "
+                    + "properties for {0}", assetDataObject.getName());
             setObservedFilePath(after);
             setObservedFilePath(after);
         }
         }
     }
     }
@@ -225,14 +320,18 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener, F
     }
     }
 
 
     public void fileChanged(FileEvent fe) {
     public void fileChanged(FileEvent fe) {
-        logger.log(Level.INFO, "External file {0} for {1} changed!", new Object[]{fe.getFile(), assetDataObject.getName()});
+        LOGGER.log(Level.INFO, "External file {0} for {1} changed!",
+                new Object[]{fe.getFile(), assetDataObject.getName()});
         notifyUser();
         notifyUser();
     }
     }
 
 
     public void fileDeleted(FileEvent fe) {
     public void fileDeleted(FileEvent fe) {
-        logger.log(Level.INFO, "External file {0} for {1} deleted!", new Object[]{fe.getFile(), assetDataObject.getName()});
+        LOGGER.log(Level.INFO, "External file {0} for {1} deleted!",
+                new Object[]{fe.getFile(), assetDataObject.getName()});
         if (originalObject != null) {
         if (originalObject != null) {
-            logger.log(ApplicationLogHandler.LogLevel.INFO, "Remove file change listener for deleted object on {0}", assetDataObject.getName());
+            LOGGER.log(ApplicationLogHandler.LogLevel.INFO, "Remove file "
+                            + "change listener for deleted object on {0}",
+                    assetDataObject.getName());
             originalObject.removeFileChangeListener(this);
             originalObject.removeFileChangeListener(this);
             originalObject = null;
             originalObject = null;
         }
         }
@@ -240,9 +339,11 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener, F
     }
     }
 
 
     public void fileRenamed(FileRenameEvent fe) {
     public void fileRenamed(FileRenameEvent fe) {
-        logger.log(Level.INFO, "External file {0} for {1} renamed!", new Object[]{fe.getFile(), assetDataObject.getName()});
+        LOGGER.log(Level.INFO, "External file {0} for {1} renamed!",
+                new Object[]{fe.getFile(), assetDataObject.getName()});
         if (originalObject != null) {
         if (originalObject != null) {
-            logger.log(Level.INFO, "Remove file change listener for renamed object on {0}", assetDataObject.getName());
+            LOGGER.log(Level.INFO, "Remove file change listener for renamed "
+                    + "object on {0}", assetDataObject.getName());
             originalObject.removeFileChangeListener(this);
             originalObject.removeFileChangeListener(this);
             originalObject = null;
             originalObject = null;
         }
         }

+ 42 - 285
jme3-core/src/com/jme3/gde/core/util/SpatialUtil.java

@@ -1,22 +1,22 @@
 /*
 /*
  * Copyright (c) 2003-2012 jMonkeyEngine
  * Copyright (c) 2003-2012 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
- * 
+ *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
  * modification, are permitted provided that the following conditions are
  * met:
  * met:
- * 
+ *
  * * Redistributions of source code must retain the above copyright
  * * Redistributions of source code must retain the above copyright
  *   notice, this list of conditions and the following disclaimer.
  *   notice, this list of conditions and the following disclaimer.
- * 
+ *
  * * Redistributions in binary form must reproduce the above copyright
  * * Redistributions in binary form must reproduce the above copyright
  *   notice, this list of conditions and the following disclaimer in the
  *   notice, this list of conditions and the following disclaimer in the
  *   documentation and/or other materials provided with the distribution.
  *   documentation and/or other materials provided with the distribution.
- * 
+ *
  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
  *   may be used to endorse or promote products derived from this software
  *   may be used to endorse or promote products derived from this software
  *   without specific prior written permission.
  *   without specific prior written permission.
- * 
+ *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
@@ -31,14 +31,9 @@
  */
  */
 package com.jme3.gde.core.util;
 package com.jme3.gde.core.util;
 
 
-import com.jme3.animation.AnimControl;
-import com.jme3.animation.SkeletonControl;
-import com.jme3.gde.core.scene.ApplicationLogHandler.LogLevel;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.SceneGraphVisitor;
-import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.anim.AnimComposer;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.Spatial;
+
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Level;
@@ -51,329 +46,91 @@ import java.util.logging.Logger;
  *
  *
  * @author normenhansen
  * @author normenhansen
  */
  */
-@SuppressWarnings({"unchecked", "rawtypes"})
 public class SpatialUtil {
 public class SpatialUtil {
 
 
-    private static final Logger logger = Logger.getLogger(SpatialUtil.class.getName());
-    //TODO: use these variables
     public static final String ORIGINAL_NAME = "ORIGINAL_NAME";
     public static final String ORIGINAL_NAME = "ORIGINAL_NAME";
     public static final String ORIGINAL_PATH = "ORIGINAL_PATH";
     public static final String ORIGINAL_PATH = "ORIGINAL_PATH";
+    
+    private static final Logger LOGGER =
+            Logger.getLogger(SpatialUtil.class.getName());
 
 
     /**
     /**
      * Gets a "pathname" for the given Spatial, combines the Spatials and
      * Gets a "pathname" for the given Spatial, combines the Spatials and
      * parents names to make a long name. This "path" is stored in geometry
      * parents names to make a long name. This "path" is stored in geometry
      * after the first import for example.
      * after the first import for example.
      *
      *
-     * @param spat
-     * @return
+     * @param spat Spatial
+     * @return id of spatial
      */
      */
     public static String getSpatialPath(Spatial spat) {
     public static String getSpatialPath(Spatial spat) {
         StringBuilder geometryIdentifier = new StringBuilder();
         StringBuilder geometryIdentifier = new StringBuilder();
         while (spat != null) {
         while (spat != null) {
             String name = spat.getName();
             String name = spat.getName();
             if (name == null) {
             if (name == null) {
-                logger.log(Level.WARNING, "Null spatial name!");
+                LOGGER.log(Level.WARNING, "Null spatial name!");
                 name = "null";
                 name = "null";
             }
             }
             geometryIdentifier.insert(0, name);
             geometryIdentifier.insert(0, name);
             geometryIdentifier.insert(0, '/');
             geometryIdentifier.insert(0, '/');
             spat = spat.getParent();
             spat = spat.getParent();
         }
         }
-        String id = geometryIdentifier.toString();
-        return id;
+        return geometryIdentifier.toString();
     }
     }
 
 
     /**
     /**
      * Stores ORIGINAL_NAME and ORIGINAL_PATH UserData to given Spatial and all
      * Stores ORIGINAL_NAME and ORIGINAL_PATH UserData to given Spatial and all
      * sub-Spatials.
      * sub-Spatials.
      *
      *
-     * @param spat
+     * @param spat spatial
      */
      */
     public static void storeOriginalPathUserData(Spatial spat) {
     public static void storeOriginalPathUserData(Spatial spat) {
         //TODO: only stores for geometry atm
         //TODO: only stores for geometry atm
-        final ArrayList<String> geomMap = new ArrayList<String>();
+        final ArrayList<String> geomMap = new ArrayList<>();
         if (spat != null) {
         if (spat != null) {
-            spat.depthFirstTraversal(new SceneGraphVisitor() {
-                @Override
-                public void visit(Spatial geom) {
-                    Spatial curSpat = geom;
-                    String geomName = curSpat.getName();
-                    if (geomName == null) {
-                        logger.log(Level.WARNING, "Null Spatial name!");
-                        geomName = "null";
-                    }
-                    geom.setUserData("ORIGINAL_NAME", geomName);
-                    logger.log(Level.FINE, "Set ORIGINAL_NAME for {0}", geomName);
-                    String id = SpatialUtil.getSpatialPath(curSpat);
-                    if (geomMap.contains(id)) {
-                        logger.log(Level.WARNING, "Cannot create unique name for Spatial {0}: {1}", new Object[]{geom, id});
-                    }
-                    geomMap.add(id);
-                    geom.setUserData("ORIGINAL_PATH", id);
-                    logger.log(Level.FINE, "Set ORIGINAL_PATH for {0}", id);
-                }
-            });
-        } else {
-            logger.log(Level.SEVERE, "No Spatial available when trying to add Spatial paths.");
-        }
-    }
-
-    /**
-     * Finds a previously marked spatial in the supplied root Spatial, creates
-     * the name and path to be looked for from the given needle Spatial.
-     *
-     * @param root
-     * @param needle
-     * @return
-     */
-    public static Spatial findTaggedSpatial(final Spatial root, final Spatial needle) {
-        if (needle == null) {
-            logger.log(Level.WARNING, "Trying to find null needle for {0}", root);
-            return null;
-        }
-        final String name = needle.getName();
-        final String path = getSpatialPath(needle);
-        if (name == null || path == null) {
-            logger.log(Level.WARNING, "Trying to find tagged Spatial with null name spatial for {0}.", root);
-            return null;
-        }
-        final Class clazz = needle.getClass();
-        String rootName = root.getUserData("ORIGINAL_NAME");
-        String rootPath = root.getUserData("ORIGINAL_PATH");
-        if (name.equals(rootName) && path.equals(rootPath)) {
-            return root;
-        }
-        final SpatialHolder holder = new SpatialHolder();
-        root.depthFirstTraversal(new SceneGraphVisitor() {
-            @Override
-            public void visit(Spatial spatial) {
-                String spName = spatial.getUserData("ORIGINAL_NAME");
-                String spPath = spatial.getUserData("ORIGINAL_PATH");
-                if (name.equals(spName) && path.equals(spPath) && clazz.isInstance(spatial)) {
-                    if (holder.spatial == null) {
-                        holder.spatial = spatial;
-                    } else {
-                        logger.log(Level.WARNING, "Found Spatial {0} twice in {1}", new Object[]{path, root});
-                    }
-                }
-            }
-        });
-        return holder.spatial;
-    }
-
-    /**
-     * Finds a spatial in the given Spatial tree with the specified name and
-     * path, the path and name are constructed from the given (sub-)spatial(s)
-     * and is not read from the UserData of the objects. This is mainly used to
-     * check if the original spatial still exists in the original file.
-     *
-     * @param root
-     * @param name
-     * @param path
-     */
-    public static Spatial findSpatial(final Spatial root, final String name, final String path) {
-        if (name == null || path == null) {
-            logger.log(Level.WARNING, "Trying to find Spatial with null name spatial for {0}.", root);
-            return null;
-        }
-        if (name.equals(root.getName()) && path.equals(getSpatialPath(root))) {
-            return root;
-        }
-        final SpatialHolder holder = new SpatialHolder();
-        root.depthFirstTraversal(new SceneGraphVisitor() {
-            @Override
-            public void visit(Spatial spatial) {
-                if (name.equals(spatial.getName()) && path.equals(getSpatialPath(spatial))) {
-                    if (holder.spatial == null) {
-                        holder.spatial = spatial;
-                    } else {
-                        logger.log(Level.WARNING, "Found Spatial {0} twice in {1}", new Object[]{path, root});
-                    }
-                }
-            }
-        });
-        return holder.spatial;
-    }
-
-    /**
-     * Updates the mesh data of existing objects from an original file, adds new
-     * nonexisting geometry objects to the root, including their parents if they
-     * don't exist.
-     *
-     * @param root
-     * @param original
-     */
-    public static void updateMeshDataFromOriginal(final Spatial root, final Spatial original) {
-        //loop through original to also find new geometry
-        original.depthFirstTraversal(new SceneGraphVisitorAdapter() {
-            @Override
-            public void visit(Geometry geom) {
-                //will always return same class type as 2nd param, so casting is safe
-                Geometry spat = (Geometry) findTaggedSpatial(root, geom);
-                if (spat != null) {
-                    spat.setMesh(geom.getMesh());
-                    logger.log(LogLevel.USERINFO, "Updated mesh for Geometry {0}", geom.getName());
-                } else {
-                    addLeafWithNonExistingParents(root, geom);
-                }
-            }
-        });
-    }
+            spat.depthFirstTraversal(geom -> {
 
 
-    /**
-     * Updates material of existing objects from an original file.
-     *
-     * @param root
-     * @param original
-     */
-    public static void updateMaterialDataFromOriginal(final Spatial root,
-                                                      final Spatial original) {
-        //loop through original to also find new geometry
-        original.depthFirstTraversal(new SceneGraphVisitorAdapter() {
-            @Override
-            public void visit(Geometry geom) {
-                //will always return same class type as 2nd param, so casting is safe
-                Geometry spat = (Geometry) findTaggedSpatial(root, geom);
-                if (spat != null && spat.getMaterial() != null
-                        && geom.getMaterial() != null
-                        && !spat.getMaterial().equals(geom.getMaterial())) {
-                    spat.setMaterial(geom.getMaterial().clone());
-                    logger.log(LogLevel.USERINFO,
-                            "Updated material for Geometry {0}", geom.getName()
-                    );
+                String geomName = geom.getName();
+                if (geomName == null) {
+                    LOGGER.log(Level.WARNING, "Null Spatial name!");
+                    geomName = "null";
                 }
                 }
-            }
-        });
-    }
-
-    /**
-     * Adds a leaf to a spatial, including all nonexisting parents.
-     *
-     * @param root
-     * @param original
-     */
-    private static void addLeafWithNonExistingParents(Spatial root, Spatial leaf) {
-        if (!(root instanceof Node)) {
-            logger.log(Level.WARNING, "Cannot add new Leaf {0} to {1}, is not a Node!", new Object[]{leaf.getName(), root.getName()});
-            return;
-        }
-        for (Spatial s = leaf; s.getParent() != null; s = s.getParent()) {
-            Spatial parent = s.getParent();
-            Spatial other = findTaggedSpatial(root, parent);
-            if (other == null) {
-                continue;
-            }
-            if (other instanceof Node) {
-                logger.log(Level.INFO, "Attaching {0} to {1} in root {2} to add leaf {3}", new Object[]{s, other, root, leaf});
-                //set original path data to leaf and new parents
-                for (Spatial spt = leaf; spt != parent; spt = spt.getParent()) {
-                    if (spt == null) {
-                        // this is to avoid a crash when changing mesh names
-                        return;
-                    }
-                    spt.setUserData(ORIGINAL_NAME, spt.getName());
-                    spt.setUserData(ORIGINAL_PATH, getSpatialPath(spt));
-                    spt = spt.getParent();
+                geom.setUserData(SpatialUtil.ORIGINAL_NAME, geomName);
+                LOGGER.log(Level.FINE, "Set ORIGINAL_NAME for {0}",
+                        geomName);
+                final Spatial curSpat = geom;
+                String id = SpatialUtil.getSpatialPath(curSpat);
+                if (geomMap.contains(id)) {
+                    LOGGER.log(Level.WARNING, "Cannot create unique name "
+                            + "for Spatial {0}: {1}", new Object[]{geom, id});
                 }
                 }
-                //attach to new node in own root
-                Node otherNode = (Node) other;
-                otherNode.attachChild(s);
-                logger.log(LogLevel.USERINFO, "Attached Node {0} with leaf {0}", new Object[]{other.getName(), leaf.getName()});
-                return;
-            } else {
-                logger.log(Level.WARNING, "Cannot attach leaf {0} to found spatial {1} in root {2}, not a node.", new Object[]{leaf, other, root});
-            }
+                geomMap.add(id);
+                geom.setUserData(SpatialUtil.ORIGINAL_PATH, id);
+                LOGGER.log(Level.FINE, "Set ORIGINAL_PATH for {0}", id);
+            });
+        } else {
+            LOGGER.log(Level.SEVERE, "No Spatial available when trying to add"
+                    + " Spatial paths.");
         }
         }
-        logger.log(Level.WARNING, "Could not attach new Leaf {0}, no root node found.", leaf.getName());
-    }
-
-    public static void updateAnimControlDataFromOriginal(final Spatial root, final Spatial original) {
-        //loop through original to also find new AnimControls, we expect all nodes etc. to exist
-        //TODO: can (blender) AnimControls end up in other nodes that are not a parent of the geometry they modify?
-        removeAnimData(root);
-        original.depthFirstTraversal(new SceneGraphVisitor() {
-            @Override
-            public void visit(Spatial spat) {
-                AnimControl animControl = spat.getControl(AnimControl.class);
-                if (animControl != null) {
-                    Spatial mySpatial = findTaggedSpatial(root, spat);
-                    if (mySpatial != null) {
-                        //TODO: move attachments: have to scan through all nodes and find the ones
-                        //where UserData "AttachedBone" == Bone and move it to new Bone
-                        AnimControl myAnimControl = mySpatial.getControl(AnimControl.class);
-                        SkeletonControl mySkeletonControl = spat.getControl(SkeletonControl.class);
-                        if (mySkeletonControl != null) {
-                            mySpatial.removeControl(mySkeletonControl);
-                        }
-                        if (myAnimControl != null) {
-                            mySpatial.removeControl(myAnimControl);
-                        }
-                        AnimControl newControl = (AnimControl) animControl.cloneForSpatial(mySpatial);
-                        if (mySpatial.getControl(SkeletonControl.class) == null) {
-                            logger.log(Level.INFO, "Adding control for {0}", mySpatial.getName());
-                            mySpatial.addControl(newControl);
-                        } else {
-                            logger.log(Level.INFO, "Control for {0} was added automatically", mySpatial.getName());
-                        }
-                        if (mySpatial.getControl(SkeletonControl.class) == null) {
-                            if (animControl.getSkeleton() == null) {
-                                logger.log(Level.INFO, "Could not add a SkeletonControl for {0}, because animControl.getSkeleton() return null. Broken file? Gltf?", mySpatial.getName());
-                            } else {
-                                mySpatial.addControl(new SkeletonControl(animControl.getSkeleton()));
-                            }
-                        } else {
-                            logger.log(Level.INFO, "SkeletonControl for {0} was added by AnimControl already", mySpatial.getName());
-                        }
-                        logger.log(LogLevel.USERINFO, "Updated animation for {0}", mySpatial.getName());
-                    } else {
-                        logger.log(Level.WARNING, "Could not find sibling for {0} in root {1} when trying to apply AnimControl data", new Object[]{spat, root});
-                    }
-                }
-            }
-        });
-        //TODO: remove old AnimControls?
     }
     }
 
 
-    public static void removeAnimData(Spatial root) {
-        root.depthFirstTraversal(new SceneGraphVisitor() {
-            @Override
-            public void visit(Spatial spat) {
-                AnimControl animControl = spat.getControl(AnimControl.class);
-                if (animControl != null) {
-                    spat.removeControl(animControl);
-                    SkeletonControl skeletonControl = spat.getControl(SkeletonControl.class);
-                    if (skeletonControl != null) {
-                        spat.removeControl(skeletonControl);
-                    }
-                }
-            }
-        });
-    }
-
-    public static void clearRemovedOriginals(final Spatial root, final Spatial original) {
+    public static void clearRemovedOriginals(final Spatial root,
+                                             final Spatial original) {
         //TODO: Clear old stuff at all?
         //TODO: Clear old stuff at all?
-        return;
     }
     }
 
 
     /**
     /**
      * Finds out if a spatial has animations.
      * Finds out if a spatial has animations.
      *
      *
-     * @param root
+     * @param root root spatial
      */
      */
     public static boolean hasAnimations(final Spatial root) {
     public static boolean hasAnimations(final Spatial root) {
         final AtomicBoolean animFound = new AtomicBoolean(false);
         final AtomicBoolean animFound = new AtomicBoolean(false);
-        root.depthFirstTraversal(new SceneGraphVisitor() {
-            public void visit(Spatial spatial) {
-                if (spatial.getControl(AnimControl.class) != null) {
-                    animFound.set(true);
-                }
+        root.depthFirstTraversal(spatial -> {
+            if (spatial.getControl(AnimComposer.class) != null) {
+                animFound.set(true);
             }
             }
         });
         });
         return animFound.get();
         return animFound.get();
     }
     }
 
 
-    private static class SpatialHolder {
-
-        Spatial spatial;
-    }
 }
 }

+ 64 - 0
jme3-core/src/com/jme3/gde/core/util/TaggedSpatialFinder.java

@@ -0,0 +1,64 @@
+package com.jme3.gde.core.util;
+
+import com.jme3.scene.Spatial;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Finds a previously marked spatial in the supplied root Spatial, creates
+ * the name and path to be looked for from the given needle Spatial.
+ */
+public class TaggedSpatialFinder {
+    
+    private static final Logger LOGGER =
+            Logger.getLogger(TaggedSpatialFinder.class.getName());
+    
+    /**
+     * Finds a previously marked spatial in the supplied root Spatial, creates
+     * the name and path to be looked for from the given needle Spatial.
+     *
+     * @param root   supplied root Spatial
+     * @param needle spatial to look for
+     * @return found spatial
+     */
+    public Spatial find(final Spatial root, final Spatial needle) {
+        if (needle == null) {
+            LOGGER.log(Level.WARNING, "Trying to find null needle for {0}",
+                    root);
+            return null;
+        }
+        final String name = needle.getName();
+        final String path = SpatialUtil.getSpatialPath(needle);
+        if (name == null) {
+            LOGGER.log(Level.WARNING, "Trying to find tagged Spatial with "
+                    + "null name spatial for {0}.", root);
+            return null;
+        }
+        final Class<? extends Spatial> clazz = needle.getClass();
+        final String rootName = root.getUserData(SpatialUtil.ORIGINAL_NAME);
+        final String rootPath = root.getUserData(SpatialUtil.ORIGINAL_PATH);
+        if (name.equals(rootName) && path.equals(rootPath)) {
+            return root;
+        }
+        final SpatialHolder holder = new SpatialHolder();
+        root.depthFirstTraversal(spatial -> {
+            final String spatialName =
+                    spatial.getUserData(SpatialUtil.ORIGINAL_NAME);
+            final String spPath =
+                    spatial.getUserData(SpatialUtil.ORIGINAL_PATH);
+            if (name.equals(spatialName) && path.equals(spPath) && clazz.isInstance(spatial)) {
+                if (holder.spatial == null) {
+                    holder.spatial = spatial;
+                } else {
+                    LOGGER.log(Level.WARNING, "Found Spatial {0} twice in"
+                            + " {1}", new Object[]{path, root});
+                }
+            }
+        });
+        return holder.spatial;
+    }
+
+    private static class SpatialHolder {
+        private Spatial spatial;
+    }
+}

+ 121 - 0
jme3-core/src/com/jme3/gde/core/util/datatransfer/CopyAnimationDataFromOriginal.java

@@ -0,0 +1,121 @@
+package com.jme3.gde.core.util.datatransfer;
+
+import com.jme3.anim.AnimClip;
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.Armature;
+import com.jme3.anim.SkinningControl;
+import com.jme3.gde.core.scene.ApplicationLogHandler;
+import com.jme3.gde.core.util.TaggedSpatialFinder;
+import com.jme3.scene.SceneGraphVisitor;
+import com.jme3.scene.Spatial;
+
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Copies AnimComposer and AnimClips from an updated spatial to the original.
+ */
+public final class CopyAnimationDataFromOriginal implements SpatialDataTransferInterface {
+
+    private static final Logger LOGGER
+            = Logger.getLogger(CopyAnimationDataFromOriginal.class.getName());
+
+    private final TaggedSpatialFinder finder;
+
+    public CopyAnimationDataFromOriginal(final TaggedSpatialFinder finder) {
+        this.finder = finder;
+    }
+
+    @Override
+    public void update(final Spatial root, final Spatial original) {
+        //loop through original to also find new AnimControls, we expect all 
+        // nodes etc. to exist
+        removeAnimData(root);
+        original.depthFirstTraversal(new SceneGraphVisitor() {
+
+            @Override
+            public void visit(final Spatial spatial) {
+                final AnimComposer animComposer
+                        = spatial.getControl(AnimComposer.class);
+                if (animComposer != null) {
+                    final Spatial mySpatial = finder.find(root, spatial);
+                    if (mySpatial != null) {
+                        //TODO: move attachments: have to scan through all
+                        // nodes and find the ones
+                        //where UserData "AttachedBone" == Bone and move it
+                        // to new Bone
+
+                        updateAndAddControl(mySpatial,
+                                animComposer,
+                                spatial.getControl(SkinningControl.class));
+
+                        LOGGER.log(ApplicationLogHandler.LogLevel.FINE,
+                                "Updated animation for {0}",
+                                mySpatial.getName());
+                    } else {
+                        LOGGER.log(Level.WARNING, "Could not find sibling for"
+                                + " {0} in root {1} when trying to apply "
+                                + "AnimControl data", new Object[]{spatial,
+                                root});
+                    }
+                }
+            }
+        });
+    }
+
+    private void updateAndAddControl(final Spatial spatial,
+                                     final AnimComposer originalAnimComposer,
+                                     final SkinningControl originalSkinningControl) {
+
+        final AnimComposer newControl = new AnimComposer();
+        copyAnimClips(newControl, originalAnimComposer);
+        if (spatial.getControl(AnimComposer.class) == null) {
+            LOGGER.log(Level.INFO, "Adding AnimComposer for {0}",
+                    spatial.getName());
+            spatial.addControl(newControl);
+        } else {
+            LOGGER.log(Level.INFO, "Control for {0} was added"
+                    + " automatically", spatial.getName());
+        }
+        if (spatial.getControl(SkinningControl.class) == null) {
+            if (originalSkinningControl == null) {
+                LOGGER.log(Level.INFO, "Could not add a SkinningControl. "
+                        + "Broken file?");
+            } else {
+                final SkinningControl skinningControl =
+                        new SkinningControl((Armature) originalSkinningControl.getArmature().jmeClone());
+                spatial.addControl(skinningControl);
+                LOGGER.log(Level.INFO, "Adding SkinningControl for {0}",
+                        spatial.getName());
+            }
+        }
+
+    }
+
+    private void copyAnimClips(final AnimComposer control,
+                               final AnimComposer original) {
+        final Collection<AnimClip> clips = original.getAnimClips();
+        for (final AnimClip c : clips) {
+            control.addAnimClip((AnimClip) c.jmeClone());
+            control.makeAction(c.getName());
+            LOGGER.log(Level.INFO, "Copied clip {0}",
+                    c.getName());
+        }
+    }
+
+    private void removeAnimData(final Spatial root) {
+        root.depthFirstTraversal(spatial -> {
+            final AnimComposer animControl = spatial.getControl(AnimComposer.class);
+            if (animControl != null) {
+                spatial.removeControl(animControl);
+            }
+            final SkinningControl skinningControl =
+                    spatial.getControl(SkinningControl.class);
+            if (skinningControl != null) {
+                spatial.removeControl(skinningControl);
+            }
+        });
+    }
+
+}

+ 42 - 0
jme3-core/src/com/jme3/gde/core/util/datatransfer/CopyMaterialDataFromOriginal.java

@@ -0,0 +1,42 @@
+package com.jme3.gde.core.util.datatransfer;
+
+import com.jme3.gde.core.util.TaggedSpatialFinder;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.scene.Spatial;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Copies material data from an updated model to the original.
+ */
+public final class CopyMaterialDataFromOriginal implements SpatialDataTransferInterface {
+
+    private static final Logger LOGGER =
+            Logger.getLogger(CopyMaterialDataFromOriginal.class.getName());
+
+    private final TaggedSpatialFinder finder;
+
+    public CopyMaterialDataFromOriginal(final TaggedSpatialFinder finder) {
+        this.finder = finder;
+    }
+
+    @Override
+    public void update(final Spatial root, final Spatial original) {
+        //loop through original to also find new geometry
+        original.depthFirstTraversal(new SceneGraphVisitorAdapter() {
+            @Override
+            public void visit(Geometry geom) {
+                //will always return same class type as 2nd param, so casting
+                // is safe
+                final Geometry spat = (Geometry) finder.find(root, geom);
+                if (spat != null && spat.getMaterial() != null && geom.getMaterial() != null) {
+                    spat.setMaterial(geom.getMaterial());
+                    LOGGER.log(Level.FINE,
+                            "Updated material for Geometry {0}",
+                            geom.getName());
+                }
+            }
+        });
+    }
+}

+ 95 - 0
jme3-core/src/com/jme3/gde/core/util/datatransfer/CopyMeshDataFromOriginal.java

@@ -0,0 +1,95 @@
+package com.jme3.gde.core.util.datatransfer;
+
+import com.jme3.gde.core.util.SpatialUtil;
+import com.jme3.gde.core.util.TaggedSpatialFinder;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.scene.Spatial;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Copies mesh data from an updated external file to the spatial.
+ */
+public class CopyMeshDataFromOriginal implements SpatialDataTransferInterface {
+
+    private static final Logger LOGGER =
+            Logger.getLogger(CopyMeshDataFromOriginal.class.getName());
+
+    private final TaggedSpatialFinder finder;
+
+    public CopyMeshDataFromOriginal(final TaggedSpatialFinder finder) {
+        this.finder = finder;
+    }
+
+    @Override
+    public void update(final Spatial root, final Spatial original) {
+        //loop through original to also find new geometry
+        original.depthFirstTraversal(new SceneGraphVisitorAdapter() {
+            @Override
+            public void visit(Geometry geom) {
+                //will always return same class type as 2nd param, so casting
+                // is safe
+                final Geometry spat = (Geometry) finder.find(root, geom);
+                if (spat != null) {
+                    spat.setMesh(geom.getMesh());
+                    LOGGER.log(Level.INFO, "Updated mesh for Geometry "
+                            + "{0}", geom.getName());
+                } else {
+                    addLeafWithNonExistingParents(root, geom);
+                }
+            }
+        });
+    }
+
+    /**
+     * Adds a leaf to a spatial, including all non-existing parents.
+     *
+     * @param root
+     * @param leaf
+     */
+    private void addLeafWithNonExistingParents(final Spatial root, 
+            final Spatial leaf) {
+        if (!(root instanceof Node)) {
+            LOGGER.log(Level.WARNING, "Cannot add new Leaf {0} to {1}, is not"
+                    + " a Node!", new Object[]{leaf.getName(), root.getName()});
+            return;
+        }
+        for (Spatial s = leaf; s.getParent() != null; s = s.getParent()) {
+            final Spatial parent = s.getParent();
+            final Spatial other = finder.find(root, parent);
+            if (other == null) {
+                continue;
+            }
+            if (other instanceof Node) {
+                LOGGER.log(Level.INFO, "Attaching {0} to {1} in root {2} to "
+                        + "add leaf {3}", new Object[]{s, other, root, leaf});
+                //set original path data to leaf and new parents
+                for (Spatial spt = leaf; spt != parent; spt = spt.getParent()) {
+                    // this is to avoid a crash when changing
+                    // names of meshes externally
+                    if (spt == null) {
+                        return;
+                    }
+                    spt.setUserData(SpatialUtil.ORIGINAL_NAME, spt.getName());
+                    spt.setUserData(SpatialUtil.ORIGINAL_PATH,
+                            SpatialUtil.getSpatialPath(spt));
+                    spt = spt.getParent();
+                }
+                //attach to new node in own root
+                Node otherNode = (Node) other;
+                otherNode.attachChild(s);
+                LOGGER.log(Level.INFO, "Attached Node {0} with leaf "
+                        + "{0}", new Object[]{other.getName(), leaf.getName()});
+                return;
+            } else {
+                LOGGER.log(Level.WARNING, "Cannot attach leaf {0} to found "
+                                + "spatial {1} in root {2}, not a node.",
+                        new Object[]{leaf, other, root});
+            }
+        }
+        LOGGER.log(Level.WARNING, "Could not attach new Leaf {0}, no root "
+                + "node found.", leaf.getName());
+    }
+}

+ 54 - 0
jme3-core/src/com/jme3/gde/core/util/datatransfer/CopyTransformDataFromOriginal.java

@@ -0,0 +1,54 @@
+package com.jme3.gde.core.util.datatransfer;
+
+import com.jme3.gde.core.util.TaggedSpatialFinder;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.scene.Spatial;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Copies Transform data (translation, rotation, scale) from an updated spatial
+ * to the original.
+ */
+public final class CopyTransformDataFromOriginal implements 
+        SpatialDataTransferInterface {
+
+    private static final Logger LOGGER
+            = Logger.getLogger(CopyAnimationDataFromOriginal.class.getName());
+
+    private final TaggedSpatialFinder finder;
+
+    public CopyTransformDataFromOriginal(final TaggedSpatialFinder finder) {
+        this.finder = finder;
+    }
+
+    @Override
+    public void update(final Spatial root, final Spatial original) {
+        original.depthFirstTraversal(new SceneGraphVisitorAdapter() {
+
+            @Override
+            public void visit(final Geometry geom) {
+                final Geometry spat = (Geometry) finder.find(root, geom);
+                if (spat != null) {
+                    spat.setLocalTransform(geom.getLocalTransform());
+                    LOGGER.log(Level.FINE,
+                            "Updated transform for Geometry {0}",
+                            geom.getName());
+                }
+            }
+
+            @Override
+            public void visit(final Node node) {
+                final Node spat = (Node) finder.find(root, node);
+                if (spat != null) {
+                    spat.setLocalTransform(node.getLocalTransform());
+                    LOGGER.log(Level.FINE,
+                            "Updated transform for Node {0}", node.getName());
+                }
+            }
+        });
+    }
+
+}

+ 19 - 0
jme3-core/src/com/jme3/gde/core/util/datatransfer/SpatialDataTransferInterface.java

@@ -0,0 +1,19 @@
+package com.jme3.gde.core.util.datatransfer;
+
+import com.jme3.scene.Spatial;
+
+/**
+ * Generic interface for data transfer from files updated externally
+ *
+ * @author rickard
+ */
+public interface SpatialDataTransferInterface {
+
+    /**
+     * Performs the data transfer.
+     *
+     * @param root     the node containing the spatial to update
+     * @param original spatial to update data from
+     */
+    abstract void update(Spatial root, Spatial original);
+}