Question on DragAndDropDemoState example

Hi,
I’m having problems setting up Paul’s DragAndDropDemoState in my own application: the drag and drop only works as long as I move the Draggable inside the respective container. However, once I want to drag the Draggable out, the DragSession is terminated and the icon picked returns to its orginial place. I’d like to be able to move the icon from one container to another.

I’ve mostly copied Paul’s code (except for some simplifications) and the only real difference I see is that in onDragDetected I’m giving the dragged item a name with
drag.setName("draggedItem");
so that I can update its position in onDragOver with
guiNode.getChild("draggedItem").setLocalTranslation(new Vector3f(event.getX(), event.getY(), 0));
Any help on how to get a drag&drop done between the containers is appreciated.
Here is my code:

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.debug.WireBox;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Line;
import com.jme3.scene.shape.Sphere;
import com.simsilica.lemur.*;
import com.simsilica.lemur.core.GuiMaterial;
import com.simsilica.lemur.dnd.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;

public class DragTest extends SimpleApplication {

    private static final Logger LOGGER = LoggerFactory.getLogger(DragTest.class);

    private static final int GRID_SIZE = 3;
    private static final float LOCAL_SCALE = 13.5f;
    private static final ColorRGBA INVENTORY_BORDER_COLOR = ColorRGBA.Red;

    private ContainerNode container1;
    private ContainerNode container2;

    @Override
    public void simpleInitApp() {

        flyCam.setEnabled(false);
        // Initialize the globals access so that the default
        // components can find what they need.
        GuiGlobals.initialize(this);
        // hide display stats
        setDisplayStatView(false);
        setDisplayFps(false);

        container1 = new ContainerNode("container2");
        container1.setSize(GRID_SIZE, GRID_SIZE, 0);
        container1.setLocalTranslation(100f, 200f, 0.5f);
        container1.setLocalScale(LOCAL_SCALE);
        container1.addControl(new GridControl(GRID_SIZE));
        container1.addControl(new DragAndDropControl(new GridContainerListener(container1)));
        guiNode.attachChild(container1);

        // Add some random items to our MVC grid 'model' control
        container1.getControl(GridControl.class).setCell(0, 0, createItem());
        container1.getControl(GridControl.class).setCell(2, 1, createItem());

        // Setup a grid based container
        container2 = new ContainerNode("container2");
        container2.setSize(GRID_SIZE, GRID_SIZE, 0);
        container2.setLocalTranslation(300f, 200f, 0.5f);
        container2.setLocalScale(LOCAL_SCALE);
        container2.addControl(new GridControl(GRID_SIZE));
        container2.addControl(new DragAndDropControl(new GridContainerListener(container2)));
        guiNode.attachChild(container2);

        // Add some random items to our MVC grid 'model' control
        container2.getControl(GridControl.class).setCell(0, 0, createItem());
        container2.getControl(GridControl.class).setCell(2, 1, createItem());
    }

    private Spatial createItem() {
        Sphere sphere = new Sphere(12, 24, 0.9f);
        Geometry geom = new Geometry("item", sphere);

        // Create a random color
        float r = (float)(Math.random() * 0.4 + 0.2);
        float g = (float)(Math.random() * 0.6 + 0.2);
        float b = (float)(Math.random() * 0.6 + 0.2);

        geom.setMaterial(new Material( assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
        return geom;
    }

    /**
     *  Just to encapsulate the visuals needed to have both a wireframe
     *  view but an actual box for picking.
     */
    private class ContainerNode extends Node {

        private GuiMaterial material;
        private WireBox wire;
        private Geometry wireGeom;
        private Box box;
        private Geometry boxGeom;

        public ContainerNode( String name ) {
            super(name);
            material = GuiGlobals.getInstance().createMaterial(INVENTORY_BORDER_COLOR, false);

            List<javafx.util.Pair<Vector3f, Vector3f>> lines = new ArrayList<>();
            // horizontal lines
            for (float column = 2 - GRID_SIZE; column < GRID_SIZE; column += 2) {
                lines.add(new javafx.util.Pair<>(new Vector3f(-GRID_SIZE, column, 0f), new Vector3f(GRID_SIZE, column, 0f)));
            }
            for (float row = 2 - GRID_SIZE; row < GRID_SIZE; row += 2) {
                lines.add(new javafx.util.Pair<>(new Vector3f(row, -GRID_SIZE,  0f), new Vector3f(row, GRID_SIZE, 0f)));
            }
            drawLines(lines);

            wire = new WireBox(1, 1, 0);
            wireGeom = new Geometry(name + ".wire", wire);
            wireGeom.setMaterial(material.getMaterial());
            attachChild(wireGeom);

            box = new Box(1, 1, 0);
            boxGeom = new Geometry(name + ".box", box);
            boxGeom.setMaterial(material.getMaterial()); // might as well reuse it

            boxGeom.setCullHint(CullHint.Always); // invisible
            attachChild(boxGeom);
        }

        private void drawLines(List<javafx.util.Pair<Vector3f, Vector3f>> lines ) {

            for (javafx.util.Pair<Vector3f, Vector3f> line : lines) {
                Line l = new Line(line.getKey(), line.getValue());
                Geometry lineGeometry = new Geometry("line", l);
                Material lineMaterial = assetManager.loadMaterial("Common/Materials/RedColor.j3m");
                lineGeometry.setMaterial(lineMaterial);
                attachChild(lineGeometry);
            }
        }

        private void setSize( float x, float y, float z ) {
            wire.updatePositions(x, y, z);
            box.updateGeometry(Vector3f.ZERO, x, y, z);
            box.clearCollisionData();

            wireGeom.updateModelBound();
            boxGeom.updateModelBound();
        }
    }

    private class GridControl extends AbstractControl {

        private ContainerNode node;
        private int gridSize;
        private Spatial[][] grid;

        public GridControl( int gridSize ) {
            this.gridSize = gridSize;
            this.grid = new Spatial[gridSize][gridSize];
        }

        @Override
        public void setSpatial( Spatial s ) {
            super.setSpatial(s);
            this.node = (ContainerNode)s;
            updateLayout();
        }

        public Spatial getCell( int x, int y ) {
            return grid[x][y];
        }

        public void setCell( int x, int y, Spatial child ) {
            if( grid[x][y] != null ) {
                grid[x][y].removeFromParent();
            }
            grid[x][y] = child;
            if( child != null ) {
                node.attachChild(child);
            }
            updateLayout();
        }

        public Spatial removeCell( int x, int y ) {
            Spatial result = grid[x][y];
            node.detachChild(result);
            grid[x][y] = null;
            if( result != null ) {
                updateLayout();
            }
            return result;
        }

        public void addChild( Spatial child ) {
            // Find the first valid cell
            for( int x = 0; x < gridSize; x++ ) {
                for( int y = 0; y < gridSize; y++ ) {
                    // just in case the child is already in the grid
                    if( grid[x][y] == child ) {
                        return;
                    }
                    if( grid[x][y] == null ) {
                        setCell(x, y, child);
                        return;
                    }
                }
            }
        }

        public void removeChild( Spatial child ) {
            for( int x = 0; x < gridSize; x++ ) {
                for( int y = 0; y < gridSize; y++ ) {
                    if( child == grid[x][y] ) {
                        if( child.getParent() == node ) {
                            child.removeFromParent();
                        }
                        grid[x][y] = null;
                    }
                }
            }
            updateLayout();
        }

        protected void updateLayout() {
            node.setSize(gridSize, gridSize, 0);
            for( int x = 0; x < gridSize; x++ ) {
                for( int y = 0; y < gridSize; y++ ) {
                    Spatial child = grid[x][y];
                    if( child != null ) {
                        child.setLocalTranslation(-(gridSize - 1) + x * 2, (gridSize - 1) - y * 2, 0);
                    }
                }
            }
        }

        @Override
        protected void controlUpdate( float tpf ) {
        }

        @Override
        protected void controlRender(RenderManager rm, ViewPort vp ) {
        }
    }

    private class GridContainerListener implements DragAndDropListener {

        private Spatial container;

        public GridContainerListener( Spatial container ) {
            this.container = container;
        }

        /**
         *  Returns the container 'model' (in the MVC sense) for this
         *  container listener.
         */
        public GridControl getModel() {
            return container.getControl(GridControl.class);
        }

        private Vector2f getCellLocation( Vector3f world ) {
            Vector3f local = container.worldToLocal(world, null);

            // Calculate the cell location
            float x = (GRID_SIZE + local.x) / 2;
            float y = (GRID_SIZE - local.y) / 2;

            int xCell = (int)x;
            int yCell = (int)y;

            return new Vector2f(xCell, yCell);
        }



        public Draggable onDragDetected(DragEvent event ) {
            // Find the child we collided with
            GridControl control = getModel();

            // See where we hit
            Vector2f hit = getCellLocation(event.getCollision().getContactPoint());

            // Remove the item from the grid if it exists.
            Spatial item = control.removeCell((int)hit.x, (int)hit.y);
            if( item != null ) {
                // Save the item in the session so the other containers (and ourselves)
                // know what we are dragging.
                event.getSession().set(DragSession.ITEM, item);

                // We'll keep track of the grid cell in case the drag is
                // canceled and we have to put it back.
                event.getSession().set("gridLocation", hit);

                // Clone the dragged item to use in our draggable and stick the
                // clone in the root at the same world location.
                Spatial drag = item.clone();
                drag.setLocalTranslation(new Vector3f(event.getX(), event.getY(), 0));
                drag.setLocalRotation(item.getWorldRotation());
                drag.setLocalScale(LOCAL_SCALE * 1.2f);
                drag.setName("draggedItem");
                guiNode.attachChild(drag);

                return new ColoredDraggable(event.getViewPort(), drag, event.getLocation());
            }
            return null;
        }

        public void onDragEnter( DragEvent event ) {
        }

        public void onDragExit( DragEvent event ) {
            System.out.println("onDragExit");
        }

        public void onDragOver( DragEvent event ) {
            LOGGER.info("onDragOver");
            Vector2f hit = getCellLocation(event.getCollision().getContactPoint());
            Spatial item = getModel().getCell((int)hit.x, (int)hit.y);
            guiNode.getChild("draggedItem").setLocalTranslation(new Vector3f(event.getX(), event.getY(), 0));
            if( item == null ) {
                // An empty cell is a valid target
                event.getSession().setDragStatus(DragStatus.ValidTarget);
            } else {
                // A filled slot is not
                event.getSession().setDragStatus(DragStatus.InvalidTarget);
            }
        }

        // Target specific
        public void onDrop( DragEvent event ) {
            LOGGER.info("onDrop");
            Spatial draggedItem = event.getSession().get(DragSession.ITEM, null);

            Vector2f hit = getCellLocation(event.getCollision().getContactPoint());

            // One last check to see if the drop location is available
            Spatial item = getModel().getCell((int)hit.x, (int)hit.y);
            if( item == null ) {
                // Then we can stick the new child right in
                getModel().setCell((int)hit.x, (int)hit.y, draggedItem);
            } else {
                // It wasn't really a valid drop
                event.getSession().setDragStatus(DragStatus.InvalidTarget);
            }
        }

        // Source specific
        public void onDragDone( DragEvent event ) {
            LOGGER.info("onDragDone");
            DragSession session = event.getSession();

            // Check to see if drop target was null as this indicates
            // that the drag operation didn't finish and we need to
            // put the item back.
            if( session.getDropTarget() == null ) {

                // Grab the payload we stored during drag start
                Spatial draggedItem = session.get(DragSession.ITEM, null);

                // Grab the original slot of the item.  We tucked this away
                // during drag start just for this case.
                Vector2f slot = session.get("gridLocation", null);
                if( slot != null ) {
                    getModel().setCell((int)slot.x, (int)slot.y, draggedItem);
                } else {
                    System.out.println("Error, missing gridLocation for dragged item");
                    // This should not ever happen but if it does we'll at least
                    // try to deal with it
                    getModel().addChild(draggedItem);
                }
            }
        }
    }

    private class ColoredDraggable extends DefaultDraggable {

        private Material originalMaterial;
        private Geometry geom;

        public ColoredDraggable( ViewPort view, Spatial spatial, Vector2f start ) {
            super(view, spatial, start);
            this.geom = (Geometry)spatial;
            this.originalMaterial = geom.getMaterial();
        }

        @Override
        public void updateDragStatus( DragStatus status ) {
            switch( status ) {
                case ValidTarget:
                    geom.setMaterial(originalMaterial);
                    break;
                default:
                    break;
            }
        }
    }




    @Override
    public void simpleUpdate(float tpf) {
    }

    @Override
    public void destroy() {
    }
}

Why would you do that when you already have the spatial in the drag event?

It’s not your problem but it’s very strange to do a buggy error-prone search for “any random spatial that happens to have this name” from the root.

As to your actual problem, since you have a working version and a non-working version, I guess the trick will be to roll back the customizations you’ve made one at a time until you find the one that caused the issue.

Thanks for your quick feedback. I’ll post if I can find the error from the transition from your class extending BaseAppState to mine extending SimpleApplication.

I’ve retraced the developement steps and found the following: It seems that the handling of a Draggable is different when working on the guiNode of a SimpleApplication: the Draggable disappears while moving and only reappears when dropping it.
I’ve created a fork of Paul’s repo to document what I mean:
https://github.com/tomreineke/Lemur/commit/2f1d18cf1b670015ac2d3148dd7c2d8e3d096653
still works fine as I want it to, whereas the head of

contains the bug of the sphere disappearing. The differences are really minimal:
https://github.com/tomreineke/Lemur/commit/676ca7ab42376736f7924a154372731b7302b6a0
I’m somewhat lost here and would appreciate feedback.

Not sure why that would happen and I’m 99% sure I’ve run this in a 2D gui environment with icons instead of spheres.

One thing to check would be what the dragged spatial looks like as it’s being dragged… I mean in code. Like it’s world bounds, location, etc… Perhaps there is something weird going on with the scaling or something (like it’s rendered back at 2 pixels high or whatever).

I’ve now also tried it with icons. When I add another debug output to the onDragOver method I see that the localTranslation of the Draggable is not changing when I move the cursor (apart from some decimals that stem from float rounding issues I suppose):

Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[200.0, 126.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(200.0, 126.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.89624, y: 135.10333, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[202.0, 126.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(202.0, 126.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.89374, y: 135.10333, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[204.0, 127.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(204.0, 127.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.89124, y: 135.105, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[206.0, 127.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(206.0, 127.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.88873, y: 135.105, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[207.0, 127.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(207.0, 127.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.88748, y: 135.105, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[207.0, 127.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(207.0, 127.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.88748, y: 135.105, z: 0.5
Grid.onDragOver(DragEvent{session=com.simsilica.lemur.dnd.DefaultDragSession@331acdad, location=[208.0, 127.0], collision=CollisionResult[geometry=container1.box (Geometry), contactPoint=(208.0, 127.0, 0.5), contactNormal=(0.0, 0.0, -1.0), distance=1.0, triangleIndex=1], viewPort=com.jme3.renderer.ViewPort@41d426b5, target=container1 (ContainerNode)})
draggable position x: 191.88623, y: 135.105, z: 0.5

This is why I tried to update the localTranslation of the Draggable in onDragOver in the first place. But that led to the issue that an item couldn’t be dragged outside of its starting container.

The only other differences I see when I debug / compare the Draggables in the working and non-working version are:

  1. In the working version the frustrumIntersects of the dragged Panel is Inside as opposed to Intersetcs in the non-working version
  2. In the working version there is some userData added to Panel. In the non-working version userData is null

It’s very strange. I’m not sure what’s happening.

Thanks for your feedback and your work on Lemur, pspeed. I’ll extend from BaseAppState to avoid the issue.