package mindustry.logic; import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.input.*; import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.io.*; import mindustry.logic.LogicNode.*; import mindustry.logic.LogicNodes.*; import mindustry.logic.SavedLogic.*; import mindustry.ui.*; public class LogicCanvas extends WidgetGroup{ private static final Color backgroundCol = Pal.darkMetal.cpy().mul(0.1f), gridCol = Pal.darkMetal.cpy().mul(0.5f); private Element selected; private Element entered; private Vec2 offset = new Vec2(); { addListener(new InputListener(){ float lastX, lastY; @Override public void touchDragged(InputEvent event, float mx, float my, int pointer){ if(Core.app.isMobile() && pointer != 0) return; float dx = mx - lastX, dy = my - lastY; offset.add(dx, dy); for(Element e : getChildren()){ e.moveBy(dx, dy); } lastX = mx; lastY = my; } @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ if((Core.app.isMobile() && pointer != 0) || (Core.app.isDesktop() && button != KeyCode.mouseMiddle)) return false; lastX = x; lastY = y; return true; } }); //TODO debug stuff add(new BinaryOpNode()); add(new BinaryOpNode()); add(new BinaryOpNode()); add(new NumberNode()); add(new NumberNode()); add(new NumberNode()); add(new ConditionNode()); add(new ConditionNode()); add(new SignalNode()); add(new SequenceNode()); Log.info(JsonIO.print(JsonIO.write(save()))); } private void add(LogicNode node){ NodeElement e = new NodeElement(node); e.setPosition(Core.graphics.getWidth()/2f, Core.graphics.getHeight()/2f); addChild(e); e.pack(); } public SavedLogic save(){ //convert elements to saved nodes for writing to JSON return new SavedLogic(getChildren().as().map(e -> { SavedNode node = new SavedNode(); node.state = e.node; node.x = e.x; node.y = e.y; node.connections = new SavedConnection[e.slots.length]; for(int i = 0; i < e.slots.length; i++){ SavedConnection con = (node.connections[i] = new SavedConnection()); SlotElement slot = e.slots[i]; if(slot.connection != null){ SlotElement to = slot.connection; con.node = getChildren().indexOf(to.node); con.slot = Structs.indexOf(to.node.slots, to); }else{ con.node = -1; } } return node; }).toArray(SavedNode.class)); } public void load(SavedLogic state){ clear(); //add nodes as children first to make sure they can be accessed. for(SavedNode node : state.nodes){ NodeElement elem = new NodeElement(node.state); elem.setPosition(node.x, node.y); addChild(elem); } //assign connection data for(int i = 0; i < state.nodes.length; i++){ SavedNode node = state.nodes[i]; NodeElement elem = (NodeElement)getChildren().get(i); for(int j = 0; j < node.connections.length; j++){ SavedConnection con = node.connections[j]; if(con.node >= 0 && con.node < state.nodes.length){ SlotElement slot = elem.slots[j]; slot.connection = ((NodeElement)getChildren().get(con.node)).slots[con.slot]; } } elem.pack(); } } @Override public void draw(){ Draw.color(backgroundCol); Fill.crect(x, y, width, height); Draw.color(gridCol); float spacing = Scl.scl(50f); int xbars = (int)(width / spacing) + 2, ybars = (int)(width / spacing) + 2; float ox = offset.x % spacing, oy = offset.y % spacing; Lines.stroke(Scl.scl(3f)); for(int i = 0; i < xbars; i++){ float cx = x + width/2f + (i - xbars/2) * spacing + ox; Lines.line(cx, y, cx, y + height); } for(int i = 0; i < ybars; i++){ float cy = y + height/2f + (i - ybars/2) * spacing + oy; Lines.line(0, cy, x + width, cy); } Draw.reset(); super.draw(); for(Element e : getChildren()){ if(e instanceof NodeElement){ NodeElement l = (NodeElement)e; for(SlotElement field : l.slots){ field.drawConnection(); } } } if(selected != null){ SlotElement field = (SlotElement)selected.userObject; Vec2 dest = selected.localToStageCoordinates(Tmp.v1.set(selected.getWidth()/2f, selected.getHeight()/2f)); Vec2 mouse = Core.input.mouse(); drawCurve(dest.x, dest.y, mouse.x, mouse.y, field.color); } } void drawCurve(float x, float y, float x2, float y2, Color color){ Lines.stroke(4f, color); float dist = Math.abs(x - x2)/2f; Lines.curve( x, y, x + dist, y, x2 - dist, y2, x2, y2, Math.max(3, (int)(Mathf.dst(x, y, x2, y2) / 5)) ); Draw.reset(); } class NodeElement extends Table{ final LogicNode node; SlotElement[] slots; Table slotTable; NodeElement(LogicNode node){ this.node = node; background(Tex.whitePane); setColor(node.category().color); margin(0f); table(Tex.whiteui, t -> { t.update(() -> { t.setColor(color); }); t.margin(8f); t.touchable = Touchable.enabled; t.add(node.name()).style(Styles.outlineLabel).color(color).padRight(8); t.add().growX(); t.button(Icon.cancel, Styles.onlyi, () -> { //TODO disconnect things remove(); }); t.addListener(new InputListener(){ float lastx, lasty; @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ Vec2 v = localToStageCoordinates(Tmp.v1.set(x, y)); lastx = v.x; lasty = v.y; toFront(); return true; } @Override public void touchDragged(InputEvent event, float x, float y, int pointer){ Vec2 v = localToStageCoordinates(Tmp.v1.set(x, y)); moveBy(v.x - lastx, v.y - lasty); lastx = v.x; lasty = v.y; } }); }).growX().padBottom(6); row(); node.build(this); row(); table(t -> slotTable = t).fill(); rebuildSlots(); marginBottom(7); } void rebuildSlots(){ slotTable.clear(); slotTable.defaults().height(30); NodeSlot[] nslots = node.slots(); this.slots = new SlotElement[nslots.length]; for(int i = 0; i < nslots.length; i++){ this.slots[i] = new SlotElement(this, nslots[i]); } for(SlotElement field : slots){ slotTable.add(field).growX().align(field.slot.input ? Align.left : Align.right); slotTable.row(); } pack(); } @Override public void draw(){ float pad = 10f; Fill.dropShadow(x + width/2f, y + height/2f, width + pad, height + pad, 20f, 0.95f); Draw.color(0, 0, 0, 0.3f); Fill.crect(x, y, width, height); Draw.reset(); super.draw(); } } class SlotElement extends Table{ final NodeSlot slot; final NodeElement node; ImageButton button; SlotElement connection; SlotElement(NodeElement node, NodeSlot slot){ this.slot = slot; this.node = node; setColor(slot.type.color); float marg = 24f; if(slot.input){ addIcon(); left(); marginRight(marg); }else{ right(); marginLeft(marg); } add(slot.name).padLeft(5).padRight(5).style(Styles.outlineLabel).color(color); if(!slot.input){ addIcon(); } } void drawConnection(){ if(connection != null){ ImageButton cb = connection.button; Vec2 from = localToStageCoordinates(Tmp.v2.set(button.x + button.getWidth()/2f, button.y + button.getHeight()/2f)); Vec2 to = cb.localToStageCoordinates(Tmp.v1.set(cb.getWidth()/2f, cb.getHeight()/2f)); drawCurve(from.x, from.y, to.x, to.y, color); } } void addIcon(){ float s = 30f; Cell c = button(Tex.logicNode, Styles.colori, () -> { }).size(s); button = c.get(); button.userObject = this; button.getStyle().imageUpColor = color; float pad = s/2f - 3f; if(slot.input){ c.padLeft(-pad); }else{ c.padRight(-pad); } button.addListener(new InputListener(){ @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode code){ if(selected == null && !slot.input){ selected = button; return true; } return false; } @Override public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){ entered = button; } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode code){ localToStageCoordinates(Tmp.v1.set(x, y)); Element element = entered; if(element != null && element.userObject instanceof SlotElement){ SlotElement field = (SlotElement)element.userObject; //make sure inputs are matched to outputs, and that slot types match if(field != SlotElement.this && field.slot.input != slot.input && field.slot.type == slot.type){ connection = field; //field.connection = button; } } if(selected == button){ selected = null; } } }); } } }