SpringGridLayout grid size problem

I’m trying to make a hot bar like in minecraft and I’m having trouble making my block meshes line up with my image. I’ve tried lots of things and I’m stuck. I’ve created a demo app and I’ll include the code and a screenshot. It should be a quick fix but I’m banging my head against the wall at this point.

The code below involves 3 classes and my custom styles at the end. HUDDemo is the main app. It attaches a HUDAppState which extends the BaseHudState which is just a slightly modified version of HudState in the lemur project.

Any help is appreciated.

import com.chappelle.jcraft.CubesSettings;
import com.chappelle.jcraft.blocks.*;
import com.chappelle.jcraft.jme3.HUDAppState;
import com.chappelle.jcraft.world.chunk.ChunkMaterial;
import com.jme3.app.*;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles;

public class HUDDemo extends SimpleApplication
{
	@Override
	public void simpleInitApp()
	{
		//Initialize Lemur GUI
		GuiGlobals.initialize(this);
		BaseStyles.loadGlassStyle();
		BaseStyles.loadStyleResources("com/chappelle/jcraft/jme3/ui/styles/jcraft-styles.groovy");
		GuiGlobals.getInstance().getStyles().setDefaultStyle("glass");

        stateManager.detach(stateManager.getState(StatsAppState.class));
        
        Block block = Blocks.bedrock;//HACK: Need to statically reference Blocks class to initialize blocks
        new CubesSettings(assetManager, new ChunkMaterial(assetManager, "Textures/FaithfulBlocks.png"));//HACK: load up CubesSettings
        
        stateManager.attach(new HUDAppState());
	}
	
	public static void main(String[] args)
	{
		HUDDemo app = new HUDDemo();
		app.showSettings = false;
		app.start();
	}
}

package com.chappelle.jcraft.jme3;

import com.chappelle.jcraft.blocks.*;
import com.chappelle.jcraft.world.chunk.ChunkMaterial;
import com.jme3.app.Application;
import com.jme3.math.*;
import com.jme3.scene.*;
import com.simsilica.lemur.*;
import com.simsilica.lemur.component.BorderLayout.Position;
import com.simsilica.lemur.component.SpringGridLayout;
import com.simsilica.lemur.core.GuiControl;
import com.simsilica.lemur.style.ElementId;

public class HUDAppState extends BaseHudState
{
	private Container hotbarContainer;
	
	public HUDAppState(){}
	
	@Override
	protected void initialize(Application app)
	{
		super.initialize(app);
		
		hotbarContainer = new Container(new ElementId("hud.hotbar"));
		hotbarContainer.setLayout(new SpringGridLayout(Axis.X, Axis.Y));
		hotbarContainer.setPreferredSize(new Vector3f(500, 50, 0));
		hotbarContainer.setInsets(new Insets3f(0, 80, 0, 80));
		ChunkMaterial material = new ChunkMaterial(app.getAssetManager(), "Textures/FaithfulBlocks.png");
		for(int i = 0; i < 10; i++)
		{
			Block block = Block.blocksList[i+1];
			if(block == Blocks.door || block == Blocks.torch || block == Blocks.ladder)//these blocks don't work yet
			{
				block = Blocks.cactus;
			}
			Node node = new Node("huditem" + i);
			Geometry blockItemGeometry = new Geometry("", MeshGenerator.generateIndividualMesh(block));
			blockItemGeometry.rotate(new Quaternion().fromAngleAxis(toRadians(25), Vector3f.UNIT_X));
			blockItemGeometry.rotate(new Quaternion().fromAngleAxis(toRadians(-45), Vector3f.UNIT_Y));
			blockItemGeometry.setMaterial(material);
			node.scale(20.0f);
	        node.attachChild(blockItemGeometry);
	        GuiControl control = new GuiControl(Panel.LAYER_INSETS, Panel.LAYER_BORDER, Panel.LAYER_BACKGROUND);
	        control.setPreferredSize(new Vector3f(50.0f, 10.0f, 10.0f));
	        node.addControl(control);
	        
			hotbarContainer.addChild(node);
		}
		getSouth().addChild(hotbarContainer, Position.Center);
	}

	private float toRadians(float degrees)
	{
	    return (degrees / 180) * FastMath.PI;
	}
}

package com.chappelle.jcraft.jme3;

import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.math.Vector3f;
import com.jme3.renderer.*;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Node;
import com.simsilica.lemur.*;
import com.simsilica.lemur.component.*;
import com.simsilica.lemur.component.BorderLayout.Position;
import com.simsilica.lemur.event.MouseAppState;
import com.simsilica.lemur.input.InputMapper;
import com.simsilica.lemur.style.ElementId;

public abstract class BaseHudState extends BaseAppState
{
	public static final ElementId ID_HUD = new ElementId("hud");
	public static final ElementId ID_NORTH = ID_HUD.child("north");
	public static final ElementId ID_SOUTH = ID_HUD.child("south");
	public static final ElementId ID_EAST = ID_HUD.child("east");
	public static final ElementId ID_WEST = ID_HUD.child("west");
	
	private ViewPort view;
	private Node main;
	private Container container;
	private Container east;
	private Container west;
	private Container north;
	private Container south;

	public BaseHudState()
	{
	}

	public Node getRoot()
	{
		return main;
	}

	public void toggleHud()
	{
		setEnabled(!isEnabled());
	}

	public Container getEast()
	{
		if(east == null)
		{
			east = new Container(ID_EAST);
			east.setLayout(new SpringGridLayout(Axis.Y, Axis.X, FillMode.None, FillMode.Even));
			east.setInsets(new Insets3f(5, 5, 5, 5));
			container.addChild(east, Position.East);
		}
		return east;
	}

	public Container getWest()
	{
		if(west == null)
		{
			west = new Container(ID_WEST);
			west.setLayout(new SpringGridLayout(Axis.Y, Axis.X, FillMode.None, FillMode.Even));
			west.setInsets(new Insets3f(5, 5, 5, 5));
			container.addChild(west, Position.West);
		}
		return west;
	}

	public Container getNorth()
	{
		if(north == null)
		{
			north = new Container(ID_NORTH);
			north.setLayout(new BorderLayout());
			container.addChild(north, Position.North);
		}
		return north;
	}

	public Container getSouth()
	{
		if(south == null)
		{
			south = new Container(ID_SOUTH);
			south.setLayout(new BorderLayout());
			container.addChild(south, Position.South);
		}
		return south;
	}

	@Override
	protected void initialize(Application app)
	{
		InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
		// inputMapper.addDelegate( MainFunctions.F_HUD, this, "toggleHud" );

		Camera cam = app.getCamera().clone();
		cam.setParallelProjection(true);

		main = new Node("HUD");
		main.setQueueBucket(Bucket.Gui);

		view = app.getRenderManager().createPostView("Hud ViewPort", cam);
		view.setEnabled(isEnabled());
		view.setClearFlags(false, true, true);
		view.attachScene(main);

		// Make sure our viewport is setup properly
		GuiGlobals.getInstance().setupGuiComparators(view);

		// Make sure this viewport gets mouse events
		getState(MouseAppState.class).addCollisionRoot(main, view);

		// Setup a basic container for standard layout... for anything
		// that cares to use it.
		container = new Container(new BorderLayout(), ID_HUD);
		container.setPreferredSize(new Vector3f(cam.getWidth(), cam.getHeight(), 0));
		container.setLocalTranslation(0, cam.getHeight(), 0);
		main.attachChild(container);

		// Have to add an empty geometry to the HUD because JME has
		// a bug in the online versions and I'd rather not go directly to
		// source.
//		Label temp = new Label("");
//		getNorth().addChild(temp);

		main.updateLogicalState(1);
		main.updateGeometricState();
	}

	@Override
	protected void cleanup(Application app)
	{
		InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
		// inputMapper.removeDelegate( MainFunctions.F_HUD, this, "toggleHud" );

		app.getRenderManager().removePostView(view);
	}

	@Override
	protected void onEnable()
	{
		view.setEnabled(true);
	}

	@Override
	protected void onDisable()
	{
		view.setEnabled(false);
		main.updateGeometricState();
	}

	@Override
	public void update(float tpf)
	{
		main.updateLogicalState(tpf);
	}

	@Override
	public void render(RenderManager rm)
	{
		main.updateGeometricState();
	}

}


import com.simsilica.lemur.*
import com.simsilica.lemur.component.*
import com.jme3.material.RenderState.BlendMode

def gradient = TbtQuadBackgroundComponent.create(
		texture( name:"/com/simsilica/lemur/icons/bordered-gradient.png",
		generateMips:false ),
		1, 1, 1, 126, 126,
		1f, false )

def border = TbtQuadBackgroundComponent.create(
		texture( name:"/com/simsilica/lemur/icons/border.png",
		generateMips:false ),
		1, 2, 2, 6, 6,
		1f, false )
def hotbar = new QuadBackgroundComponent(texture( name:"/Textures/gui/hotbar.png"), 25f, 25f)

def transparent = new QuadBackgroundComponent(color(0, 0, 0, 0))

selector( "container", "glass" )
{
	background = gradient.clone()
	background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
selector( "hud", "glass" )
{ 
	background = transparent 
}
selector( "hud", "south", "glass" )
{
	background = border.clone()
}

selector("hud", "hotbar", "glass")
{
	background = hotbar.clone()
}

Well, from a quick glance at the code it seems like your block menu will be stretched to the full width of the screen. It has insets but even still the rest will try to stretch the children to meet whatever space is left over after screenWidth-preferredSize.x-insets.

If you don’t want your block components to stretch in the X direction then you can turn off fill in the direction by specifying the fill mode on your SpringGridLayout’s constructor. This may have other side effects but then there are ways to fix that, too… but at least your children will be behaving. (The side effects will be due to trying to stretch the container you don’t really want stretched… indicative that you should just be placing it instead of forcing it to screen width or whatever.)

I’m closer. I made the fill modes be none for both axes and set a preferedSize on my GuiControl. I also commented out my preferredSize and insets on my hotbarContainer just to make sure that isn’t causing any problems. I’m just not understanding this at a fundamental level. It kinda makes sense to me that making the FillMode.None means I take control of the sizing of each element. However, each of my meshes may not be the same size. I see that there is a Mesh.getBounds method that I could use but it seems to be getting pretty hacky when I considered doing that.

My concern here is that I get it just right for this resolution and as soon as it changes it will be off again.

		hotbarContainer = new Container(new ElementId("hud.hotbar"));
		hotbarContainer.setLayout(new SpringGridLayout(Axis.X, Axis.Y, FillMode.None, FillMode.None));
//		hotbarContainer.setPreferredSize(new Vector3f(500, 50, 0));
//		hotbarContainer.setInsets(new Insets3f(0, 80, 0, 80));
		ChunkMaterial material = new ChunkMaterial(app.getAssetManager(), "Textures/FaithfulBlocks.png");
		for(int i = 0; i < 10; i++)
		{
			Block block = Block.blocksList[i+1];
			if(block == Blocks.door || block == Blocks.torch || block == Blocks.ladder)//these blocks don't work yet
			{
				block = Blocks.cactus;
			}
			Node node = new Node("huditem" + i);
			Geometry blockItemGeometry = new Geometry("", MeshGenerator.generateIndividualMesh(block));
			blockItemGeometry.rotate(new Quaternion().fromAngleAxis(toRadians(25), Vector3f.UNIT_X));
			blockItemGeometry.rotate(new Quaternion().fromAngleAxis(toRadians(-45), Vector3f.UNIT_Y));
			blockItemGeometry.setMaterial(material);
			node.scale(20.0f);
			
	        node.attachChild(blockItemGeometry);
	        GuiControl control = new GuiControl(Panel.LAYER_INSETS, Panel.LAYER_BORDER, Panel.LAYER_BACKGROUND);
	        control.setPreferredSize(new Vector3f(70.0f, 0, 0));
	        node.addControl(control);
	        
			hotbarContainer.addChild(node);
		}
		getSouth().addChild(hotbarContainer, Position.Center);

Well, another alternative is to let the layout stretch but then give your blocks a DynamicInsetsComponent so that they just center themselves in whatever space they’re given.

In the end, mixing 3D this way with a manually created GuiControl is going to be tough unless that GuiControl knows the size of the 3D object… ie: its bounds.

Do you recommend not using a background image this way and just creating my control with code? At first I didn’t know how to do borders but I’ve since seen how you are doing that in groovy.

The key, I think, will be keeping the hacking as low as possible. I feel like most of your issues stem from a partially specified GuiControl holding the 3D child.

Taking a step back, the “right” solution would be to implement your own ModelComponent that is similar to things like IconComponent except that it has a 3D child instead of creating a quad. It could then provide a proper preferred size, participate in resize(), etc… then it would act like any other component and you could just plop it in as the Icon layer of a label/panel/whatever. It could take backgrounds, etc… and act just like any other proper layer.

Short of that, I think you’ll need to add a listener to the GuiControl so that you can still move your geometry around on resize.

If it were me, I’d just roll a new GuiComponent… but I already know how to write them. :slight_smile:

In case it’s not clear, how the spring grid layout works is that it first calculates a preferred size by adding up all of the child preferred sizes. Then if you set a bigger size, it needs to move and reset the size of all of those children. It divides up the extra space depending on the FillMode… so, FillMode.None means it just positions everyone and doesn’t make them any bigger. All of the left over space will be at the end.

I figure you either want your children to stretch or you don’t want any stretching at all and then you can just center the bar and let empty space open up to either side. Either one would be fine with multiple resolutions.

Edit: just remember that if you write your own component, Lemur thinks of things in terms of upper left and your component growing down and right.

That gives me something to digest. IconComponent looks straightforward and there really aren’t that many methods to implement. I may go that route and see what I can come up with.

Thanks for your help!

This javadoc just saved me from asking you a bunch of questions. Good job!

/**
 *  A member of a component stack that provides sizing or
 *  rendering as part of that stack.  A GuiControl manages a stack
 *  of GuiComponents.  Each component can contribute
 *  to the overall preferred size of the stack/control and each component
 *  can adjust the position of the next layer in the stack.
 *
 *  <p>Most GuiComponent implementations will manage actual scene
 *  graph elements.  Some may simply provide extra sizing adjustments
 *  like the InsetsComponent.</p>
 *
 *  <p>See package com.simsilica.lemur.component for base GuiComponent
 *  implementations.</p>
 *
 *  @author    Paul Speed
 */
public interface GuiComponent {
    public void calculatePreferredSize( Vector3f size );
    public void reshape( Vector3f pos, Vector3f size );
    public void attach( GuiControl parent );
    public void detach( GuiControl parent );
    public boolean isAttached();
    public GuiControl getGuiControl();
    public GuiComponent clone();
}

Thanks. If you liked that… then this might blow your mind: :slight_smile:

I have read that early on. I’d probably understand it more now that I have used the code a bit so I’ll go back and read it.

This is the one I was hoping for: Creating Custom Components · jMonkeyEngine-Contributions/Lemur Wiki · GitHub :slight_smile:

Yeah… but it would refer heavily to the other section with all of the pretty pictures. :slight_smile:

Not perfect yet but now I feel as though I have control over it. Writing the component was easy.The work left has nothing to do with lemur, just my sizing logic.

It’s not working on other resolutions but I’ll save that one for another day. Thanks again for the help. I learned a lot about lemur tonight and I like it even more.

1 Like

Yay on both counts. :slight_smile:

I’m exploring the pure 3D way and not trying to match up to the background image.

I have a custom GuiComponent that creates the block mesh and positions my items. The Container that has the SpringGridLayout has insets with 80 on the left and right. I would have expected my position.x to start after the insets and the border apply their positioning but it doesn’t seem to be the case.

My guess is that my GuiComponent layer is getting called before the other layers but I don’t know how to change the ordering.

I’m sure I can get around this by passing in the insets or traversing the tree to find the parent component and discovering the Insets but that seems like the wrong thing to do. It just seems like the whole benefit of layers is that one layer doesn’t have to know about the other.

Anyway, a nudge in the right direction would be greatly appreciated.

public class HUDAppState extends BaseHudState
{
	private Container hotbarContainer;
	
	public HUDAppState(){}
	
	@Override
	protected void initialize(Application app)
	{
		super.initialize(app);
		
		hotbarContainer = new Container(new ElementId("hud.hotbar"));
		hotbarContainer.setLayout(new SpringGridLayout(Axis.X, Axis.Y, FillMode.None, FillMode.None));
		hotbarContainer.setInsets(new Insets3f(0, 80, 0, 80));
		hotbarContainer.setPreferredSize(new Vector3f(0, 50, 0));
		ChunkMaterial material = new ChunkMaterial(app.getAssetManager(), "Textures/FaithfulBlocks.png");
		for(int i = 0; i < 9; i++)
		{
			Block block = Block.blocksList[i+1];
			if(block == Blocks.door || block == Blocks.torch || block == Blocks.ladder)//these blocks don't work yet
			{
				block = Blocks.cactus;
			}
			Node node = new Node("huditem" + i);
			node.scale(22.0f);
	        GuiControl control = new GuiControl(Panel.LAYER_INSETS, Panel.LAYER_BORDER, Panel.LAYER_BACKGROUND);
	        control.addComponent(new HudItemComponent(i, block, material));
			node.addControl(control);
			hotbarContainer.addChild(node);
		}
		getSouth().addChild(hotbarContainer, Position.Center);
	}


package com.chappelle.jcraft.jme3;

import com.chappelle.jcraft.blocks.*;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.scene.*;
import com.simsilica.lemur.component.AbstractGuiComponent;
import com.simsilica.lemur.core.GuiControl;

public class HudItemComponent extends AbstractGuiComponent
{
	private Geometry geometry;
	private Block block;
	private Material material;
	private int itemIndex;

	public HudItemComponent(int itemIndex, Block block, Material material)
	{
		this.itemIndex = itemIndex;
		this.block = block;
		this.material = material;

		createItem();
	}

	private void createItem()
	{
		geometry = new Geometry("item: " + block.getDisplayName(), MeshGenerator.generateIndividualMesh(block));
		geometry.rotate(new Quaternion().fromAngleAxis(toRadians(25), Vector3f.UNIT_X));
		geometry.rotate(new Quaternion().fromAngleAxis(toRadians(-45), Vector3f.UNIT_Y));
		geometry.setMaterial(material);
	}

	@Override
	public void calculatePreferredSize(Vector3f size)
	{
		size.x += 50;
	}

	@Override
	public void reshape(Vector3f pos, Vector3f size)
	{
		float spacing = 0.12f;
		if(itemIndex == 0)
		{
			pos.x += 0.1;
		}
		else
		{
			pos.x += itemIndex * spacing;
		}
		pos.y -= 1f;
		geometry.setLocalTranslation(pos);
	}

    @Override
    public HudItemComponent clone() 
    {
        HudItemComponent result = (HudItemComponent)super.clone();
        result.geometry = null;
        result.material = material.clone();
        result.createItem();
        return result;
    }

	@Override
	public void attach(GuiControl parent)
	{
		super.attach(parent);
		if(geometry != null)
		{
			getNode().attachChild(geometry);
		}
	}

	@Override
	public void detach(GuiControl parent)
	{
		if(geometry != null)
		{
			getNode().detachChild(geometry);
		}
		super.detach(parent);
	}

	private float toRadians(float degrees)
	{
		return (degrees / 180) * FastMath.PI;
	}

}

Well, you probably want to control your layers better… I’m not sure if the default gets added to the top or the bottom but it’s better to add it to an actual layer. (You have three layers defined in your GuiControl, you could always define a fourth.)

Also, not sure why you are still using a custom GuiControl instead of one of the default GUI elements at this point. If your new component is well behaved then you should be able to just set it as the icon of a label, for example.

…but anyway, your current approach should also work which is why I suspect a layer ordering problem.

Though do note that your component does not seem to take the advice I gave about growing right and down as my guess is that your geometry is centered over its own origin. Perhaps if not in y then at least in x? reshape() is effectively given a box at pos of size size. It’s up to you to position your geometry within that box properly and either carve out the space you took or just let it pass on.

Well, you probably want to control your layers better… I’m not sure if the default gets added to the top or the bottom but it’s better to add it to an actual layer. (You have three layers defined in your GuiControl, you could always define a fourth.)

So I found how to add the layer with a specific name. It seems to be behaving better now. When I change the insets, my starting position changes.

	        GuiControl control = new GuiControl(Panel.LAYER_INSETS, Panel.LAYER_BORDER, Panel.LAYER_BACKGROUND, "hud");
	        control.setComponent("hud", new HudItemComponent(i, block, material));

Also, not sure why you are still using a custom GuiControl instead of one of the default GUI elements at this point.

I’m not sure either. I’m just a user of Lemur, not the creator. :slight_smile: Seriously though, it’s because I don’t have a good grasp on the proper way to do that with a custom Mesh. Plus I almost have this working so I kinda want to stay with it.

Though do note that your component does not seem to take the advice I gave about growing right and down as my guess is that your geometry is centered over its own origin.

My reshape() is a mess because I don’t understand how my size is correlating to position. Lets take the x direction only for a moment. In my calculatePreferredSize I add 50 to size.x. In reshape I would think that my pos.x would be itemIndex*size.x + spacing. When I do that my items get rendered off screen. So that’s why my reshape is wacky looking.

Well, it’s whatever the layout decided… which will include borders, insets, or whatever.

Anyway, you have a lot of moving parts here that could all be contributing to strangeness.

My recommendation, take a step back. Create a container with some Labels in it, each with their own square background. Then set your custom component as the icon of each label. See what your reshape looks like then.

The part I don’t know and can’t really comment on is why the origin of your block geometry is. It looks to me like it’s centered on its own local 0,0,0… which means that each Geometry needs to be offset by at least half its own size… though it’s probably easier to just center it in what reshape gives you.

Much better! Now it works like I expect. Thank you! Now I just need to dig into effects to see how to highlight the border when an item is selected.

	@Override
	public void reshape(Vector3f pos, Vector3f size)
	{
		pos.x += size.x/2;
		pos.y -= size.y/2;
		geometry.setLocalTranslation(pos);
	}