Browse Source

Merge pull request #612 from neph1/AnimLayers

Create AnimLayers in the SceneComposer and play animations
Rickard Edén 10 months ago
parent
commit
e5b33e8ffd

+ 12 - 24
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/JmeControl.java

@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2009-2016 jMonkeyEngine
+ *  Copyright (c) 2009-2024jMonkeyEngine
  *  All rights reserved.
  * 
  *  Redistribution and use in source and binary forms, with or without
@@ -37,7 +37,6 @@ import com.jme3.scene.Spatial;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
 import java.io.IOException;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import javax.swing.Action;
 import org.openide.actions.DeleteAction;
@@ -97,17 +96,12 @@ public abstract class JmeControl extends AbstractSceneExplorerNode {
         final Spatial spat = getParentNode().getLookup().lookup(Spatial.class);
         try {
             fireSave(true);
-            SceneApplication.getApplication().enqueue(new Callable<Void>() {
-
-                public Void call() throws Exception {
-                    spat.removeControl(control);
-                    return null;
-                }
+            SceneApplication.getApplication().enqueue(() -> {
+                spat.removeControl(control);
+                return null;
             }).get();
             ((AbstractSceneExplorerNode) getParentNode()).refresh(true);
-        } catch (InterruptedException ex) {
-            Exceptions.printStackTrace(ex);
-        } catch (ExecutionException ex) {
+        } catch (InterruptedException | ExecutionException ex) {
             Exceptions.printStackTrace(ex);
         }
     }
@@ -116,9 +110,8 @@ public abstract class JmeControl extends AbstractSceneExplorerNode {
     @Override
     public void fireSave(boolean modified) {
         super.fireSave(true);
-        Node parent = getParentNode();
-        if (parent instanceof AbstractSceneExplorerNode) {
-            AbstractSceneExplorerNode par=(AbstractSceneExplorerNode)parent;
+        final Node parent = getParentNode();
+        if (parent instanceof AbstractSceneExplorerNode par) {
             par.fireSave(modified);
         }
     }
@@ -128,23 +121,18 @@ public abstract class JmeControl extends AbstractSceneExplorerNode {
      * This only works for extended AbstractControls!!
      * Also see: {@link #isEnabled() }
      * @param enabled Whether the Control should be enabled or disabled
-     * @return If we had success (false when an Exception occured or no {@link Control} assigned or not of type {@link AbstractControl} )
+     * @return If we had success (false when an Exception occurred or no {@link Control} assigned or not of type {@link AbstractControl} )
      */
     public boolean setEnabled(final boolean enabled) {
         if (!isEnableable())
             return false;
         try {
-            SceneApplication.getApplication().enqueue(new Callable<Void>() {
-                public Void call() throws Exception {
-                    ((AbstractControl)control).setEnabled(enabled);
-                    return null;
-                }
+            SceneApplication.getApplication().enqueue(() -> {
+                ((AbstractControl)control).setEnabled(enabled);
+                return null;
             }).get();
            
-        } catch (InterruptedException ex) {
-            Exceptions.printStackTrace(ex);
-            return false;
-        } catch (ExecutionException ex) {
+        } catch (InterruptedException | ExecutionException ex) {
             Exceptions.printStackTrace(ex);
             return false;
         }

+ 45 - 26
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeAnimClip.java

@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2009-2020 jMonkeyEngine
+ *  Copyright (c) 2009-2024 jMonkeyEngine
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -43,6 +43,8 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.beans.PropertyChangeEvent;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 import javax.swing.Action;
 import javax.swing.SwingUtilities;
@@ -58,7 +60,7 @@ import org.openide.util.actions.SystemAction;
 
 /**
  * Visual representation of the AnimClip Class in the Scene Explorer
- * @author MeFisto94
+ * @author MeFisto94, neph1
  */
 @org.openide.util.lookup.ServiceProvider(service = SceneExplorerNode.class)
 @SuppressWarnings({"unchecked", "rawtypes"})
@@ -67,7 +69,6 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
     private AnimClip animClip;
     private Image icon;
     private JmeAnimComposer jmeControl;
-    private boolean playing = false;
 
     public JmeAnimClip() {
     }
@@ -103,7 +104,7 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
     }
 
     public void toggleIcon(boolean enabled) {
-        if (!playing) {
+        if (!enabled) {
             icon = IconList.animation.getImage();
         } else {
             icon = IconList.animationPlay.getImage();
@@ -113,13 +114,12 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
 
     @Override
     public Action getPreferredAction() {
-        return Actions.alwaysEnabled(new PlayAction(), "Play", "", false);
+        return Actions.alwaysEnabled(new PlayAction(AnimComposer.DEFAULT_LAYER), "Play", "", false);
     }
 
-    private void play() {
-        playing = !playing;
-        toggleIcon(playing);
-        jmeControl.setAnimClip(this);
+    private void play(String layer) {
+        toggleIcon(true);
+        jmeControl.setAnimClip(layer, this);
     }
 
     @Override
@@ -142,13 +142,25 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
 
     @Override
     public Action[] getActions(boolean context) {
-        return new Action[]{Actions.alwaysEnabled(new PlayAction(), playing ? "Stop" : "Play", "", false),
-            SystemAction.get(RenameAction.class),
-            SystemAction.get(DeleteAction.class),
-            //Actions.alwaysEnabled(new EffectTrackWizardAction(jmeControl.getLookup().lookup(AnimComposer.class).getSpatial(), this), "Add Effect Track", "", false),
-            //Actions.alwaysEnabled(new AudioTrackWizardAction(jmeControl.getLookup().lookup(AnimComposer.class).getSpatial(), this), "Add Audio Track", "", false),
-            // @TODO: not working yet, Actions.alwaysEnabled(new ExtractAnimationAction(), "Extract sub-animation", "", true)
-        };
+        final AnimComposer control = jmeControl.getLookup().lookup(AnimComposer.class);
+        if(control == null) {
+            return new Action[]{
+                Actions.alwaysEnabled(new PlayAction(AnimComposer.DEFAULT_LAYER), jmeControl.getPlaying(AnimComposer.DEFAULT_LAYER) == this ? "Stop" : "Play", "", false),
+                SystemAction.get(RenameAction.class),
+                SystemAction.get(DeleteAction.class),
+            };
+        }
+        final String[] layers = control.getLayerNames().stream().toArray(String[] ::new);
+        
+        List<Action> playActions = new ArrayList<>();
+        
+        for(String layer: layers) {
+            playActions.add(Actions.alwaysEnabled(new PlayAction(layer), jmeControl.getPlaying(layer) == this ? "Stop " + layer : "Play " + layer, "", false));
+        }
+        playActions.add(SystemAction.get(RenameAction.class));
+        playActions.add(SystemAction.get(DeleteAction.class));
+        final Action[] actions = new Action[playActions.size()];
+        return playActions.toArray(actions);
     }
 
     @Override
@@ -157,19 +169,19 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
     }
 
     public void stop() {
-        playing = false;
-        toggleIcon(playing);
+        toggleIcon(false);
     }
 
     @Override
     public void destroy() throws IOException {
         super.destroy();  
         final AnimComposer control = jmeControl.getLookup().lookup(AnimComposer.class);
-        if (playing) {
+
+        if (jmeControl.getPlaying(AnimComposer.DEFAULT_LAYER) == this) {
             control.removeCurrentAction(AnimComposer.DEFAULT_LAYER);
-            jmeControl.setAnimClip(null);
-            
+            jmeControl.setAnimClip(AnimComposer.DEFAULT_LAYER, null);  
         }
+        
         lookupContents.remove(JmeAnimClip.this.animClip);
         lookupContents.remove(this);
         SceneApplication.getApplication().enqueue( () -> {
@@ -202,6 +214,13 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
     }
 
     class PlayAction implements ActionListener {
+        
+        private final String layer;
+        
+        public PlayAction(String layer) {
+            this.layer = layer;
+        }
+        
         @Override
         public void actionPerformed(ActionEvent e) {
             final AnimComposer control = jmeControl.getLookup().lookup(AnimComposer.class);
@@ -211,14 +230,14 @@ public class JmeAnimClip extends AbstractSceneExplorerNode {
 
             try {
                 SceneApplication.getApplication().enqueue(() -> {
-                    if (playing) { // Stop Playing
-                        control.removeCurrentAction(AnimComposer.DEFAULT_LAYER);
-                        jmeControl.setAnimClip(null);
+                    if (jmeControl.getPlaying(layer) == JmeAnimClip.this) { // Stop Playing
+                        control.removeCurrentAction(layer);
+                        jmeControl.setAnimClip(layer, null);
                         return null;
                     } else {
-                        control.setCurrentAction(animClip.getName());
+                        control.setCurrentAction(animClip.getName(), layer);
                         java.awt.EventQueue.invokeLater(() -> {
-                            play();
+                            play(layer);
                         });
                         return null;
                     }

+ 38 - 12
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeAnimComposer.java

@@ -34,19 +34,29 @@ package com.jme3.gde.core.sceneexplorer.nodes.animation;
 import com.jme3.anim.AnimComposer;
 import com.jme3.gde.core.icons.IconList;
 import com.jme3.gde.core.scene.SceneApplication;
+import com.jme3.gde.core.sceneexplorer.SceneExplorerTopComponent;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeControl;
 import com.jme3.gde.core.sceneexplorer.nodes.SceneExplorerNode;
 import com.jme3.gde.core.sceneexplorer.nodes.actions.ControlsPopup;
 import com.jme3.gde.core.sceneexplorer.nodes.actions.animation.AnimClipProperty;
 import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
+import javax.swing.AbstractAction;
 import javax.swing.Action;
 import org.openide.actions.DeleteAction;
+import org.openide.awt.Actions;
+import org.openide.explorer.ExplorerManager;
 import org.openide.loaders.DataObject;
 import org.openide.nodes.Node;
 import org.openide.nodes.Sheet;
 import org.openide.util.Exceptions;
 import org.openide.util.actions.SystemAction;
+import org.openide.windows.TopComponent;
 
 /**
  * Visual representation of the AnimComposer Class in the Scene Explorer
@@ -56,13 +66,13 @@ import org.openide.util.actions.SystemAction;
 @SuppressWarnings({"unchecked", "rawtypes"})
 public class JmeAnimComposer extends JmeControl {
     private AnimComposer animComposer;
-    private JmeAnimClip playingAnimation = null;
+    private final Map<String, JmeAnimClip> playingAnimation = new HashMap<>();
     private static Image smallImage = IconList.animControl.getImage();
 
     public JmeAnimComposer() {
     }
 
-    public JmeAnimComposer(AnimComposer animComposer, JmeAnimClipChildren children, DataObject obj) {
+    public JmeAnimComposer(AnimComposer animComposer, JmeAnimComposerChildren children, DataObject obj) {
         super(children);
         dataObject = obj;
         children.setDataObject(dataObject);
@@ -100,15 +110,15 @@ public class JmeAnimComposer extends JmeControl {
         return sheet;
     }
 
-    public boolean isPlaying() {
-        return playingAnimation != null;
+    public JmeAnimClip getPlaying(String layer) {
+        return playingAnimation.get(layer);
     }
-
-    public void setAnimClip(JmeAnimClip anim) {
-        if (playingAnimation != null) {
-            playingAnimation.stop();
+    
+    public void setAnimClip(String layer, JmeAnimClip anim) {
+        if (playingAnimation.get(layer) != null) {
+            playingAnimation.get(layer).stop();
         }
-        playingAnimation = anim;
+        playingAnimation.put(layer, anim);
     }
     
     public float getGlobalSpeed() {
@@ -130,7 +140,8 @@ public class JmeAnimComposer extends JmeControl {
     public Action[] getActions(boolean context) {
         return new Action[]{
             new ControlsPopup(this),
-            SystemAction.get(DeleteAction.class)
+            new StopAllAction(),
+            SystemAction.get(DeleteAction.class),
         };
     }
 
@@ -146,13 +157,28 @@ public class JmeAnimComposer extends JmeControl {
 
     @Override
     public Node[] createNodes(Object key, DataObject key2, boolean cookie) {
-        JmeAnimClipChildren children = new JmeAnimClipChildren(this);
+        JmeAnimComposerChildren children = new JmeAnimComposerChildren(this);
         return new Node[]{ new JmeAnimComposer((AnimComposer)key, children, key2)};
     }
     
     @Override
     public void refresh(boolean immediate) {
-        ((JmeAnimClipChildren) jmeChildren).refreshChildren(immediate);
+        ((JmeAnimComposerChildren) jmeChildren).refreshChildren(immediate);
         super.refresh(immediate);
     }
+   
+    private class StopAllAction extends AbstractAction {
+
+        public StopAllAction() {
+            super("Stop animations");
+        }
+                
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            for(JmeAnimClip layer: JmeAnimComposer.this.playingAnimation.values()) {
+                layer.stop();
+            }
+        }
+    }
+    
 }

+ 18 - 6
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeAnimClipChildren.java → jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeAnimComposerChildren.java

@@ -33,10 +33,13 @@ package com.jme3.gde.core.sceneexplorer.nodes.animation;
 
 import com.jme3.anim.AnimClip;
 import com.jme3.anim.AnimComposer;
+import com.jme3.anim.AnimLayer;
 import com.jme3.gde.core.scene.SceneApplication;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import org.openide.loaders.DataObject;
 import org.openide.nodes.Children;
@@ -44,19 +47,19 @@ import org.openide.nodes.Node;
 import org.openide.util.Exceptions;
 
 /**
- * Representation of multiple Animations in the Scene Explorer
- * @author MeFisto94
+ * Representation of an AnimComposers AnimClips and AnimLayers in the Scene Explorer
+ * @author MeFisto94, neph1
  */
-public class JmeAnimClipChildren extends Children.Keys<Object> {
+public class JmeAnimComposerChildren extends Children.Keys<Object> {
     protected JmeAnimComposer jmeAnimComposer;
     protected boolean readOnly = true;
     protected HashMap<Object, Node> map = new HashMap<>();
     private DataObject dataObject;
 
-    public JmeAnimClipChildren() {
+    public JmeAnimComposerChildren() {
     }
 
-    public JmeAnimClipChildren(JmeAnimComposer jmeAnimComposer) {
+    public JmeAnimComposerChildren(JmeAnimComposer jmeAnimComposer) {
         this.jmeAnimComposer = jmeAnimComposer;
     }
 
@@ -82,6 +85,12 @@ public class JmeAnimClipChildren extends Children.Keys<Object> {
                 AnimComposer composer = jmeAnimComposer.getLookup().lookup(AnimComposer.class);
                 if (composer != null) {
                     keys.addAll(composer.getAnimClips());
+                    final Set<String> layerNames = composer.getLayerNames();
+                    final List<AnimLayer> layers = new ArrayList<>();
+                    for(String s: layerNames) {
+                        layers.add(composer.getLayer(s));
+                    }
+                    keys.addAll(layers);
                 }
                 
                 return keys;
@@ -96,7 +105,10 @@ public class JmeAnimClipChildren extends Children.Keys<Object> {
     protected Node[] createNodes(Object key) {
         if (key instanceof AnimClip animClip) {
             return new Node[]{ new JmeAnimClip(jmeAnimComposer, animClip, dataObject).setReadOnly(readOnly)};
-        } else {
+        } else if (key instanceof AnimLayer animLayer) {
+            return new Node[]{ new JmeAnimLayer(jmeAnimComposer, animLayer, dataObject).setReadOnly(readOnly)};
+        } 
+        else {
             return new Node[]{ Node.EMPTY };
         }
     }

+ 145 - 0
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeAnimLayer.java

@@ -0,0 +1,145 @@
+/*
+ *  Copyright (c) 2009-2024 jMonkeyEngine
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.core.sceneexplorer.nodes.animation;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.AnimLayer;
+import com.jme3.gde.core.icons.IconList;
+import com.jme3.gde.core.scene.SceneApplication;
+import com.jme3.gde.core.sceneexplorer.nodes.AbstractSceneExplorerNode;
+import com.jme3.gde.core.sceneexplorer.nodes.SceneExplorerNode;
+import java.awt.Image;
+import java.io.IOException;
+import javax.swing.Action;
+import javax.swing.SwingUtilities;
+import org.openide.actions.DeleteAction;
+import org.openide.loaders.DataObject;
+import org.openide.nodes.Node;
+import org.openide.nodes.Sheet;
+import org.openide.util.actions.SystemAction;
+
+
+/**
+ * Representation of an AnimComposers AnimLayer
+ * @author rickard
+ */
[email protected](service = SceneExplorerNode.class)
+public class JmeAnimLayer extends AbstractSceneExplorerNode {
+    
+    private Image icon;
+    private JmeAnimComposer jmeControl;
+    private AnimLayer layer;
+    
+    public JmeAnimLayer() {
+        
+    }
+
+    public JmeAnimLayer(JmeAnimComposer animComposer, AnimLayer layer, DataObject dataObject) {
+        super();
+        this.jmeControl = animComposer;
+        this.layer = layer;
+        this.dataObject = dataObject;
+        lookupContents.add(this);
+        lookupContents.add(layer);
+        setName(layer.getName());
+        icon = IconList.important.getImage();
+    }
+    
+    @Override
+    public Image getIcon(int type) {
+        return icon;
+    }
+
+    @Override
+    public Image getOpenedIcon(int type) {
+        return icon;
+    }
+    
+    @Override
+    protected Sheet createSheet() {
+        Sheet sheet = Sheet.createDefault();
+        Sheet.Set set = Sheet.createPropertiesSet();
+        set.setDisplayName("AnimLayer");
+        set.setName(AnimLayer.class.getName());
+        if (layer != null) {
+            sheet.put(set);
+        } // else: Empty Sheet
+        
+        return sheet;
+    }
+    
+    @Override
+    public Action[] getActions(boolean context) {
+        return new Action[]{
+            SystemAction.get(DeleteAction.class),
+        };
+    }
+
+    @Override
+    public boolean canDestroy() {
+         return !getName().equals((AnimComposer.DEFAULT_LAYER)) && !jmeControl.isReadOnly();
+    }
+    
+    @Override
+    public void destroy() throws IOException {
+        super.destroy();  
+        final AnimComposer control = jmeControl.getLookup().lookup(AnimComposer.class);
+
+        lookupContents.remove(JmeAnimLayer.this.layer);
+        lookupContents.remove(this);
+        SceneApplication.getApplication().enqueue( () -> {
+            control.removeLayer(this.getName());
+            SwingUtilities.invokeLater(() -> jmeControl.refresh(false));
+        });
+        setChanged();
+    }
+    
+    public void setChanged() {
+        fireSave(true);
+    }
+    
+    @Override
+    public Class getExplorerObjectClass() {
+        return AnimLayer.class;
+    }
+    
+    @Override
+    public Class getExplorerNodeClass() {
+        return JmeAnimLayer.class;
+    }
+    
+    @Override
+    public Node[] createNodes(Object key, DataObject key2, boolean cookie) {
+        JmeAnimLayer jsc = new JmeAnimLayer(jmeControl, (AnimLayer)key, key2);
+        return new Node[]{jsc};
+    }
+}

+ 1 - 0
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeBone.java

@@ -55,6 +55,7 @@ import org.openide.util.Exceptions;
  */
 @org.openide.util.lookup.ServiceProvider(service = SceneExplorerNode.class)
 @SuppressWarnings({"unchecked", "rawtypes"})
+@Deprecated
 public class JmeBone extends AbstractSceneExplorerNode {
 
     private static Image smallImage = IconList.bone.getImage();

+ 1 - 0
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeBoneChildren.java

@@ -48,6 +48,7 @@ import org.openide.util.Exceptions;
  *
  * @author normenhansen
  */
+@Deprecated
 public class JmeBoneChildren extends Children.Keys<Object> {
 
     protected Bone rootBone;

+ 43 - 17
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeJoint.java

@@ -31,6 +31,8 @@
  */
 package com.jme3.gde.core.sceneexplorer.nodes.animation;
 
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.ArmatureMask;
 import com.jme3.anim.Joint;
 import com.jme3.gde.core.icons.IconList;
 import com.jme3.gde.core.scene.SceneApplication;
@@ -43,6 +45,7 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.util.concurrent.ExecutionException;
 import javax.swing.Action;
+import javax.swing.JOptionPane;
 import org.openide.awt.Actions;
 import org.openide.loaders.DataObject;
 import org.openide.nodes.Sheet;
@@ -99,26 +102,11 @@ public class JmeJoint extends AbstractSceneExplorerNode {
     @Override
     public Action[] getActions(boolean context) {
         return new Action[]{
-            Actions.alwaysEnabled(new AttachementNodeActionListener(), "Get attachement Node", "", false)
+            Actions.alwaysEnabled(new AttachementNodeActionListener(), "Get attachement Node", "", false),
+            Actions.alwaysEnabled(new ArmatureMaskActionListener(), "Create armature mask", "", false)
         };
     }
 
-    private class AttachementNodeActionListener implements ActionListener {
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            fireSave(true);
-            try {
-                SceneApplication.getApplication().enqueue(() -> 
-                    jmeSkinningControl.getSkinningControl().getAttachmentsNode(joint.getName())
-                ).get();
-                
-                ((AbstractSceneExplorerNode)jmeSkinningControl.getParentNode()).refresh(false);
-            } catch (InterruptedException | ExecutionException ex) {
-                Exceptions.printStackTrace(ex);
-            }
-        }
-    }
-
     @Override
     public Class getExplorerObjectClass() {
         return Joint.class;
@@ -136,4 +124,42 @@ public class JmeJoint extends AbstractSceneExplorerNode {
         children.setDataObject(key2);
         return new org.openide.nodes.Node[]{new JmeJoint(jmeSkinningControl, (Joint)key, children).setReadOnly(cookie)};
     }
+    
+    
+    private class AttachementNodeActionListener implements ActionListener {
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            fireSave(true);
+            try {
+                SceneApplication.getApplication().enqueue(() -> 
+                    jmeSkinningControl.getSkinningControl().getAttachmentsNode(joint.getName())
+                ).get();
+                
+                ((AbstractSceneExplorerNode)jmeSkinningControl.getParentNode()).refresh(false);
+            } catch (InterruptedException | ExecutionException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+    
+    private class ArmatureMaskActionListener implements ActionListener {
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            String name = JOptionPane.showInputDialog("Enter a name for the armature mask"); 
+            if(name == null) {
+                return;
+            }
+            fireSave(true);
+            SceneApplication.getApplication().enqueue(() -> {
+                final AnimComposer composer = jmeSkinningControl.getSkinningControl().getSpatial().getControl(AnimComposer.class);
+                composer.makeLayer(name, ArmatureMask.createMask(jmeSkinningControl.getSkinningControl().getArmature(), joint.getName()));
+                }
+            );
+            final JmeAnimComposer animComposer = (JmeAnimComposer) ((AbstractSceneExplorerNode)jmeSkinningControl.getParentNode()).getChildren().findChild("AnimComposer");
+            animComposer.refresh(true);
+            animComposer.fireSave(true);
+        }
+        
+    }
 }

+ 1 - 0
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeSkeletonControl.java

@@ -46,6 +46,7 @@ import org.openide.nodes.Sheet;
  */
 @org.openide.util.lookup.ServiceProvider(service = SceneExplorerNode.class)
 @SuppressWarnings({"unchecked", "rawtypes"})
+@Deprecated
 public class JmeSkeletonControl extends JmeControl {
 
     private SkeletonControl skeletonControl;

+ 3 - 3
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/animation/JmeSkinningControl.java

@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2009-2020 jMonkeyEngine
+ *  Copyright (c) 2009-2024 jMonkeyEngine
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -33,7 +33,7 @@ package com.jme3.gde.core.sceneexplorer.nodes.animation;
 
 import com.jme3.anim.SkinningControl;
 import com.jme3.gde.core.icons.IconList;
-import com.jme3.gde.core.sceneexplorer.nodes.AbstractSceneExplorerNode;
+import com.jme3.gde.core.sceneexplorer.nodes.JmeControl;
 import com.jme3.gde.core.sceneexplorer.nodes.SceneExplorerNode;
 import java.awt.Image;
 import org.openide.loaders.DataObject;
@@ -46,7 +46,7 @@ import org.openide.nodes.Sheet;
  */
 @org.openide.util.lookup.ServiceProvider(service = SceneExplorerNode.class)
 @SuppressWarnings({"unchecked", "rawtypes"})
-public class JmeSkinningControl extends AbstractSceneExplorerNode {
+public class JmeSkinningControl extends JmeControl {
     private SkinningControl skinningControl;
     private static Image smallImage = IconList.skeletonControl.getImage();