Tech tree rendering + layout

This commit is contained in:
Anuken
2019-01-19 00:26:39 -05:00
parent 4ff1d0c2e1
commit 49253964d8
11 changed files with 1036 additions and 549 deletions

View File

@@ -0,0 +1,310 @@
package io.anuke.mindustry.ui;
import io.anuke.arc.collection.FloatArray;
import io.anuke.arc.math.geom.Rectangle;
/**
* Algorithm taken from <a href="https://github.com/abego/treelayout">TreeLayout</a>.
*/
public class TreeLayout{
public TreeLocation rootLocation = TreeLocation.top;
public TreeAlignment alignment = TreeAlignment.awayFromRoot;
public float gapBetweenLevels = 10;
public float gapBetweenNodes = 10f;
private final FloatArray sizeOfLevel = new FloatArray();
private float boundsLeft = Float.MAX_VALUE;
private float boundsRight = Float.MIN_VALUE;
private float boundsTop = Float.MAX_VALUE;
private float boundsBottom = Float.MIN_VALUE;
public void layout(TreeNode root){
firstWalk(root, null);
calcSizeOfLevels(root, 0);
secondWalk(root, -root.prelim, 0, 0);
}
private float getWidthOrHeightOfNode(TreeNode treeNode, boolean returnWidth){
return returnWidth ? treeNode.width : treeNode.height;
}
private float getNodeThickness(TreeNode treeNode){
return getWidthOrHeightOfNode(treeNode, !isLevelChangeInYAxis());
}
private float getNodeSize(TreeNode treeNode){
return getWidthOrHeightOfNode(treeNode, isLevelChangeInYAxis());
}
private boolean isLevelChangeInYAxis(){
return rootLocation == TreeLocation.top || rootLocation == TreeLocation.bottom;
}
private int getLevelChangeSign(){
return rootLocation == TreeLocation.bottom || rootLocation == TreeLocation.right ? -1 : 1;
}
private void updateBounds(TreeNode node, float centerX, float centerY){
float width = node.width;
float height = node.height;
float left = centerX - width / 2;
float right = centerX + width / 2;
float top = centerY - height / 2;
float bottom = centerY + height / 2;
if(boundsLeft > left){
boundsLeft = left;
}
if(boundsRight < right){
boundsRight = right;
}
if(boundsTop > top){
boundsTop = top;
}
if(boundsBottom < bottom){
boundsBottom = bottom;
}
}
public Rectangle getBounds(){
return new Rectangle(0, 0, boundsRight - boundsLeft, boundsBottom - boundsTop);
}
private void calcSizeOfLevels(TreeNode node, int level){
float oldSize;
if(sizeOfLevel.size <= level){
sizeOfLevel.add(0);
oldSize = 0;
}else{
oldSize = sizeOfLevel.get(level);
}
float size = getNodeThickness(node);
if(oldSize < size){
sizeOfLevel.set(level, size);
}
if(!node.isLeaf()){
for(TreeNode child : node.children){
calcSizeOfLevels(child, level + 1);
}
}
}
public int getLevelCount(){
return sizeOfLevel.size;
}
public float getGapBetweenNodes(TreeNode a, TreeNode b){
return gapBetweenNodes;
}
public float getSizeOfLevel(int level){
if(!(level >= 0)) throw new IllegalArgumentException("level must be >= 0");
if(!(level < getLevelCount())) throw new IllegalArgumentException("level must be < levelCount");
return sizeOfLevel.get(level);
}
private TreeNode getAncestor(TreeNode node){
return node.ancestor != null ? node.ancestor : node;
}
private float getDistance(TreeNode v, TreeNode w){
float sizeOfNodes = getNodeSize(v) + getNodeSize(w);
return sizeOfNodes / 2 + getGapBetweenNodes(v, w);
}
private TreeNode nextLeft(TreeNode v){
return v.isLeaf() ? v.thread : v.children[0];
}
private TreeNode nextRight(TreeNode v){
return v.isLeaf() ? v.thread : v.children[v.children.length - 1];
}
private int getNumber(TreeNode node, TreeNode parentNode){
if(node.number == -1){
int number = 1;
for(TreeNode child : parentNode.children){
child.number = number++;
}
}
return node.number;
}
private TreeNode ancestor(TreeNode vIMinus, TreeNode parentOfV, TreeNode defaultAncestor){
TreeNode ancestor = getAncestor(vIMinus);
return ancestor.parent == parentOfV ? ancestor : defaultAncestor;
}
private void moveSubtree(TreeNode wMinus, TreeNode wPlus, TreeNode parent, float shift){
int subtrees = getNumber(wPlus, parent) - getNumber(wMinus, parent);
wPlus.change = wPlus.change - shift / subtrees;
wPlus.shift = wPlus.shift + shift;
wMinus.change = wMinus.change + shift / subtrees;
wPlus.prelim = wPlus.prelim + shift;
wPlus.mode = wPlus.mode + shift;
}
private TreeNode apportion(TreeNode v, TreeNode defaultAncestor,
TreeNode leftSibling, TreeNode parentOfV){
if(leftSibling == null){
return defaultAncestor;
}
TreeNode vOPlus = v;
TreeNode vIPlus = v;
TreeNode vIMinus = leftSibling;
TreeNode vOMinus = parentOfV.children[0];
float sIPlus = (vIPlus).mode;
float sOPlus = (vOPlus).mode;
float sIMinus = (vIMinus).mode;
float sOMinus = (vOMinus).mode;
TreeNode nextRightVIMinus = nextRight(vIMinus);
TreeNode nextLeftVIPlus = nextLeft(vIPlus);
while(nextRightVIMinus != null && nextLeftVIPlus != null){
vIMinus = nextRightVIMinus;
vIPlus = nextLeftVIPlus;
vOMinus = nextLeft(vOMinus);
vOPlus = nextRight(vOPlus);
vOPlus.ancestor = v;
float shift = (vIMinus.prelim + sIMinus)
- (vIPlus.prelim + sIPlus)
+ getDistance(vIMinus, vIPlus);
if(shift > 0){
moveSubtree(ancestor(vIMinus, parentOfV, defaultAncestor),
v, parentOfV, shift);
sIPlus = sIPlus + shift;
sOPlus = sOPlus + shift;
}
sIMinus += vIMinus.mode;
sIPlus += vIPlus.mode;
sOMinus += vOMinus.mode;
sOPlus += vOPlus.mode;
nextRightVIMinus = nextRight(vIMinus);
nextLeftVIPlus = nextLeft(vIPlus);
}
if(nextRightVIMinus != null && nextRight(vOPlus) == null){
vOPlus.thread = nextRightVIMinus;
vOPlus.mode += sIMinus - sOPlus;
}
if(nextLeftVIPlus != null && nextLeft(vOMinus) == null){
vOMinus.thread = nextLeftVIPlus;
vOMinus.mode += sIPlus - sOMinus;
defaultAncestor = v;
}
return defaultAncestor;
}
private void executeShifts(TreeNode v){
float shift = 0;
float change = 0;
for(int i = v.children.length - 1; i >= 0; i --){
TreeNode w = v.children[i];
change = change + w.change;
w.prelim += shift;
w.mode += shift;
shift += w.shift + change;
}
}
private void firstWalk(TreeNode v, TreeNode leftSibling){
if(v.isLeaf()){
if(leftSibling != null){
v.prelim = leftSibling.prelim + getDistance(v, leftSibling);
}
}else{
TreeNode defaultAncestor = v.children[0];
TreeNode previousChild = null;
for(TreeNode w : v.children){
firstWalk(w, previousChild);
defaultAncestor = apportion(w, defaultAncestor, previousChild,
v);
previousChild = w;
}
executeShifts(v);
float midpoint = (v.children[0].prelim + v.children[v.children.length-1].prelim) / 2f;
TreeNode w = leftSibling;
if(w != null){
v.prelim = w.prelim + getDistance(v, w);
v.mode = v.prelim - midpoint;
}else{
v.prelim = midpoint;
}
}
}
private void secondWalk(TreeNode v, float m, int level, float levelStart){
float levelChangeSign = getLevelChangeSign();
boolean levelChangeOnYAxis = isLevelChangeInYAxis();
float levelSize = getSizeOfLevel(level);
float x = v.prelim + m;
float y;
if(alignment == TreeAlignment.center){
y = levelStart + levelChangeSign * (levelSize / 2);
}else if(alignment == TreeAlignment.towardsRoot){
y = levelStart + levelChangeSign * (getNodeThickness(v) / 2);
}else{
y = levelStart + levelSize - levelChangeSign
* (getNodeThickness(v) / 2);
}
if(!levelChangeOnYAxis){
float t = x;
x = y;
y = t;
}
v.x = x;
v.y = y;
updateBounds(v, x, y);
if(!v.isLeaf()){
float nextLevelStart = levelStart
+ (levelSize + gapBetweenLevels)
* levelChangeSign;
for(TreeNode w : v.children){
secondWalk(w, m + v.mode, level + 1, nextLevelStart);
}
}
}
public enum TreeLocation{
top, left, bottom, right
}
public enum TreeAlignment{
center, towardsRoot, awayFromRoot
}
public static class TreeNode{
public float width, height, x, y;
//should be initialized by user
public TreeNode[] children;
public TreeNode parent;
private float mode, prelim, change, shift;
private int number = -1;
private TreeNode thread, ancestor;
boolean isLeaf(){
return children == null || children.length == 0;
}
}
}

View File

@@ -26,6 +26,7 @@ public class DeployDialog extends FloatingDialog{
cont.clear();
addCloseButton();
buttons.addButton("$techtree", () -> ui.tech.show()).size(200f, 60f);
cont.stack(new Table(){{
top().left().margin(10);

View File

@@ -0,0 +1,107 @@
package io.anuke.mindustry.ui.dialogs;
import io.anuke.arc.Core;
import io.anuke.arc.collection.ObjectSet;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.Lines;
import io.anuke.arc.graphics.g2d.ScissorStack;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.scene.Element;
import io.anuke.arc.scene.event.InputEvent;
import io.anuke.arc.scene.event.InputListener;
import io.anuke.mindustry.content.TechTree;
import io.anuke.mindustry.content.TechTree.TechNode;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.game.UnlockableContent;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.ui.TreeLayout;
import io.anuke.mindustry.ui.TreeLayout.TreeNode;
import io.anuke.mindustry.world.Block;
public class TechTreeDialog extends FloatingDialog{
private TreeLayout layout;
private ObjectSet<TechTreeNode> nodes = new ObjectSet<>();
public TechTreeDialog(){
super("$techtree");
layout = new TreeLayout();
layout.gapBetweenLevels = 60f;
layout.gapBetweenNodes = 40f;
layout.layout(new TechTreeNode(TechTree.root, null));
cont.add(new View()).grow();
addCloseButton();
}
class TechTreeNode extends TreeNode{
final TechNode node;
public TechTreeNode(TechNode node, TreeNode parent){
this.node = node;
this.parent = parent;
this.width = this.height = 60f;
nodes.add(this);
if(node.children != null){
children = new TechTreeNode[node.children.length];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children[i], this);
}
}
}
}
class View extends Element{
float panX = 0, panY = 0;
Rectangle clip = new Rectangle();
{
addListener(new InputListener(){
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
super.touchDragged(event, x, y, pointer);
panX += Core.input.deltaX(pointer);
panY += Core.input.deltaY(pointer);
}
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
return true;
}
});
}
@Override
public void draw(){
if(!ScissorStack.pushScissors(clip.set(x, y, width, height))){
return;
}
float offsetX = panX + width/2f + x, offsetY = panY + height/2f + y;
Lines.stroke(3f, Palette.accent);
for(TreeNode node : nodes){
for(TreeNode child : node.children){
Lines.line(node.x + offsetX, node.y + offsetY, child.x + offsetX, child.y + offsetY);
}
}
Draw.color();
for(TechTreeNode node : nodes){
Draw.drawable("content-background", node.x + offsetX - node.width/2f, node.y + offsetY - node.height/2f, node.width, node.height);
Content content = node.node.content;
TextureRegion region = content instanceof Block ? ((Block)content).getEditorIcon() :
((UnlockableContent)content).getContentIcon();
Draw.rect(region, node.x + offsetX, node.y + offsetY, 8*3, 8*3);
}
ScissorStack.popScissors();
}
}
}