package noppes.npcs.client.gui.util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.ScaledResolution;
import noppes.npcs.constants.EnumDiagramLayout;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GL11;
import java.awt.*;
import java.util.List;
import java.util.Queue;
import java.util.*;
/**
* Abstract GUI class for drawing diagrams with icons (nodes) and connections (arrows).
*
* Features include:
*
* - Parent reference to disable mouse interaction when a subGUI is active.
* - Caching for icons and connections (subclasses must implement createIcons() and createConnections()).
* - Support for different layout types (circular, square, tree, generated, and manual versions)
* plus a CHART layout.
* - Drawing of connections as either straight or curved arrows (with hit‐detection improved for curves).
* - Chart layout: a grid with header icons and inner cells that show connection colors.
* When hovering over a header or cell, only the corresponding row/column (or cell and its two icons) are highlighted
* while all non‑relevant cells are dimmed. Also, cells with no connection are drawn with a darker gray.
*
* - Zooming and panning controls apply to both chart and non‑chart layouts.
* - Callbacks for icon and connection interactions including click, held, release and hover events.
*
*/
public abstract class GuiDiagram extends Gui {
// --- Diagram Area & Controls ---
protected int x, y, width, height; // Drawing area for the diagram
protected float panX = 0, panY = 0; // Panning offsets
protected float zoom = 1.0f; // Zoom factor
protected boolean dragging = false; // Flag indicating active panning
protected int lastDragX, lastDragY; // Last mouse positions used for panning
// --- Node (Icon) Sizing ---
protected int iconSize = 16; // Icon image size (square)
protected int slotPadding = 4; // Padding around icons
protected int slotSize = iconSize + slotPadding; // Total size of the icon box
// --- Icon Box Appearance ---
protected int iconBackgroundColor = 0xFF999999; // Background color for icon boxes
protected int iconBorderColor = 0xFF555555; // Border color for icon boxes
protected int iconBorderThickness = 1; // Border thickness in pixels
// --- Connection Options ---
protected boolean showArrowHeads = true; // Whether to draw arrow heads
protected boolean useColorScaling = true; // Scale connection color based on a percentage
// --- Layout Options ---
protected EnumDiagramLayout layout = EnumDiagramLayout.CIRCULAR; // Default layout
/**
* Sets the layout type and invalidates caches.
*/
public void setLayout(EnumDiagramLayout layout) {
this.layout = layout;
invalidateCache();
}
// --- Arrow Drawing Options ---
protected boolean curvedArrows = true; // If false, draw straight connection lines
protected int curveAngle = 15; // Curvature angle in degrees
public void setCurvedArrows(boolean curved) {
this.curvedArrows = curved;
}
public void setCurveAngle(int angle) {
this.curveAngle = angle;
}
// --- Caching for icons and connections ---
protected Map cachedPositions = null; // Cached positions for nodes (non-CHART)
protected List iconsCache = null; // Cached list of icons
protected List connectionsCache = null; // Cached list of connections
/**
* Clears cached layout information.
*/
public void invalidateCache() {
cachedPositions = null;
iconsCache = null;
connectionsCache = null;
}
// --- Parent reference ---
protected GuiNPCInterface parent;
// --- Constructor ---
public GuiDiagram(GuiNPCInterface parent, int x, int y, int width, int height) {
this.parent = parent;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
// --- Data Providers (Subclasses must implement these) ---
protected abstract List createIcons();
protected abstract List createConnections();
protected final List getIcons() {
if (iconsCache == null) {
iconsCache = createIcons();
}
return iconsCache;
}
protected final List getConnections() {
if (connectionsCache == null) {
connectionsCache = createConnections();
}
return connectionsCache;
}
/**
* Subclasses must implement how an icon is rendered.
*/
protected abstract void renderIcon(DiagramIcon icon, int posX, int posY, IconRenderState state);
/**
* Returns the tooltip text lines for an icon.
*/
protected List getIconTooltip(DiagramIcon icon) { return null; }
/**
* Returns the tooltip text lines for a connection.
*/
protected List getConnectionTooltip(DiagramConnection conn) {
List tooltip = new ArrayList<>();
DiagramIcon iconFrom = getIconById(conn.idFrom);
DiagramIcon iconTo = getIconById(conn.idTo);
String nameFrom = iconFrom != null ? getIconName(iconFrom) : "Unknown";
String nameTo = iconTo != null ? getIconName(iconTo) : "Unknown";
tooltip.add(nameFrom + " > " + nameTo + ":");
tooltip.add(conn.hoverText);
DiagramConnection reverse = getConnectionByIds(conn.idTo, conn.idFrom);
if (reverse != null) {
tooltip.add(nameTo + " > " + nameFrom + ":");
tooltip.add(reverse.hoverText);
}
return tooltip;
}
protected String getIconName(DiagramIcon icon) { return "Icon " + icon.id; }
// --- Layout Calculation ---
/**
* For non‑CHART layouts, returns the mapping from node ID to (x,y) position.
* CHART layout is drawn directly.
*/
protected Map calculatePositions() {
if (cachedPositions != null) return cachedPositions;
if (layout == EnumDiagramLayout.CHART) {
return new HashMap<>();
}
switch (layout) {
case CIRCULAR: cachedPositions = calculateCircularPositions(); break;
case SQUARE: cachedPositions = calculateSquarePositions(); break;
case TREE: cachedPositions = calculateTreePositions(); break;
case GENERATED: cachedPositions = calculateGeneratedPositions(); break;
case CIRCULAR_MANUAL: cachedPositions = calculateCircularManualPositions(); break;
case SQUARE_MANUAL: cachedPositions = calculateSquareManualPositions(); break;
case TREE_MANUAL: cachedPositions = calculateTreeManualPositions(); break;
default: cachedPositions = calculateCircularPositions(); break;
}
return cachedPositions;
}
// --- Standard Layout Methods ---
protected Map calculateCircularPositions() {
List icons = getIcons();
Map positions = new HashMap<>();
int count = icons.size();
int centerX = x + width/2, centerY = y + height/2;
int radius = Math.min(width, height)/2 - iconSize - 10;
for (int i = 0; i < count; i++) {
double angle = 2 * Math.PI * i / count;
int posX = centerX + (int)(radius * Math.cos(angle));
int posY = centerY + (int)(radius * Math.sin(angle));
positions.put(icons.get(i).id, new Point(posX, posY));
}
return positions;
}
protected Map calculateSquarePositions() {
List icons = getIcons();
Map positions = new HashMap<>();
int count = icons.size();
int n = (int)Math.ceil(Math.sqrt(count));
int gridWidth = width - 20, gridHeight = height - 20;
int cellWidth = gridWidth / n, cellHeight = gridHeight / n;
int startX = x + (width - gridWidth)/2, startY = y + (height - gridHeight)/2;
for (int i = 0; i < count; i++) {
int row = i / n, col = i % n;
int posX = startX + col * cellWidth + cellWidth/2;
int posY = startY + row * cellHeight + cellHeight/2;
positions.put(icons.get(i).id, new Point(posX, posY));
}
return positions;
}
protected Map calculateTreePositions() {
List icons = getIcons();
Map positions = new HashMap<>();
int count = icons.size();
int levels = (int)Math.ceil(Math.log(count + 1) / Math.log(2));
int levelHeight = height / (levels + 1);
int index = 0;
for (int level = 0; level < levels && index < count; level++) {
int nodesThisLevel = (int)Math.min(Math.pow(2, level), count - index);
int levelWidth = width - 20;
int cellWidth = levelWidth / nodesThisLevel;
for (int i = 0; i < nodesThisLevel && index < count; i++, index++) {
int posX = x + 10 + i * cellWidth + cellWidth/2;
int posY = y + (level + 1) * levelHeight;
positions.put(icons.get(index).id, new Point(posX, posY));
}
}
return positions;
}
protected Map calculateGeneratedPositions() {
List icons = getIcons();
List connections = getConnections();
Map> graph = buildGraph(icons, connections);
List> clusters = getConnectedComponents(graph);
int clusterCount = clusters.size();
int gridCols = (int)Math.ceil(Math.sqrt(clusterCount));
int gridRows = (int)Math.ceil((double)clusterCount / gridCols);
int clusterMargin = 20;
int compWidth = (width - (gridCols + 1) * clusterMargin) / gridCols;
int compHeight = (height - (gridRows + 1) * clusterMargin) / gridRows;
Map globalPositions = new HashMap<>();
int clusterIndex = 0;
for (Set cluster : clusters) {
int col = clusterIndex % gridCols;
int row = clusterIndex / gridCols;
int compX = x + clusterMargin + col * (compWidth + clusterMargin);
int compY = y + clusterMargin + row * (compHeight + clusterMargin);
Map compPositions;
if (isCycle(cluster, graph))
compPositions = layoutCycle(cluster, compX, compY, compWidth, compHeight);
else if (isTree(cluster, graph))
compPositions = layoutTree(cluster, compX, compY, compWidth, compHeight, graph);
else if (isSquare(cluster))
compPositions = layoutSquare(cluster, compX, compY, compWidth, compHeight);
else
compPositions = layoutForceDirected(cluster, compX, compY, compWidth, compHeight, connections);
globalPositions.putAll(compPositions);
clusterIndex++;
}
return globalPositions;
}
// --- Helpers for Generated Layout ---
private Map> buildGraph(List icons, List connections) {
Map> graph = new HashMap<>();
for (DiagramIcon icon : icons) {
graph.put(icon.id, new ArrayList<>());
}
for (DiagramConnection conn : connections) {
if (graph.containsKey(conn.idFrom) && graph.containsKey(conn.idTo)) {
graph.get(conn.idFrom).add(conn.idTo);
graph.get(conn.idTo).add(conn.idFrom);
}
}
return graph;
}
private List> getConnectedComponents(Map> graph) {
Set visited = new HashSet<>();
List> components = new ArrayList<>();
for (Integer node : graph.keySet()) {
if (!visited.contains(node)) {
Set comp = new HashSet<>();
dfsComponent(node, graph, visited, comp);
components.add(comp);
}
}
return components;
}
private void dfsComponent(Integer current, Map> graph, Set visited, Set comp) {
visited.add(current);
comp.add(current);
for (Integer neighbor : graph.get(current)) {
if (!visited.contains(neighbor))
dfsComponent(neighbor, graph, visited, comp);
}
}
private boolean isCycle(Set cluster, Map> graph) {
if (cluster.size() < 3) return false;
for (Integer node : cluster) {
int degree = 0;
for (Integer neighbor : graph.get(node)) {
if (cluster.contains(neighbor)) degree++;
}
if (degree != 2) return false;
}
return true;
}
private boolean isTree(Set cluster, Map> graph) {
int edgeCount = 0;
for (Integer node : cluster) {
for (Integer neighbor : graph.get(node)) {
if (cluster.contains(neighbor)) edgeCount++;
}
}
edgeCount /= 2;
return edgeCount == cluster.size() - 1;
}
private boolean isSquare(Set cluster) {
int n = cluster.size();
int sqrt = (int)Math.round(Math.sqrt(n));
return sqrt * sqrt == n;
}
// --- Layout Algorithms for Clusters ---
private Map layoutCycle(Set cluster, int compX, int compY, int compWidth, int compHeight) {
Map positions = new HashMap<>();
int centerX = compX + compWidth / 2, centerY = compY + compHeight / 2;
int radius = Math.min(compWidth, compHeight) / 3;
int i = 0;
for (Integer id : cluster) {
double angle = 2 * Math.PI * i / cluster.size();
int posX = centerX + (int)(radius * Math.cos(angle));
int posY = centerY + (int)(radius * Math.sin(angle));
positions.put(id, new Point(posX, posY));
i++;
}
return positions;
}
private Map layoutTree(Set cluster, int compX, int compY, int compWidth, int compHeight,
Map> fullGraph) {
Map positions = new HashMap<>();
Map> subgraph = new HashMap<>();
for (Integer id : cluster) {
List neighbors = new ArrayList<>();
for (Integer neighbor : fullGraph.get(id)) {
if (cluster.contains(neighbor))
neighbors.add(neighbor);
}
subgraph.put(id, neighbors);
}
Integer root = cluster.iterator().next();
for (Integer id : cluster) {
if (id < root) root = id;
}
Map levelMap = new HashMap<>();
Map> levels = new HashMap<>();
Queue queue = new LinkedList<>();
Set visited = new HashSet<>();
queue.add(root);
visited.add(root);
levelMap.put(root, 0);
while (!queue.isEmpty()) {
Integer curr = queue.poll();
int lvl = levelMap.get(curr);
levels.computeIfAbsent(lvl, k -> new ArrayList<>()).add(curr);
for (Integer neighbor : subgraph.get(curr)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
levelMap.put(neighbor, lvl + 1);
queue.add(neighbor);
}
}
}
int maxLevel = levels.keySet().stream().max(Integer::compare).orElse(0);
double levelHeight = compHeight / (maxLevel + 1.0);
for (Map.Entry> entry : levels.entrySet()) {
int lvl = entry.getKey();
List nodesAtLevel = entry.getValue();
nodesAtLevel.sort(Comparator.comparingInt(a -> getIconById(a).priority));
double spacing = compWidth / (nodesAtLevel.size() + 1.0);
for (int i = 0; i < nodesAtLevel.size(); i++) {
int id = nodesAtLevel.get(i);
int posX = compX + (int)((i + 1) * spacing);
int posY = compY + (int)(lvl * levelHeight + levelHeight / 2);
positions.put(id, new Point(posX, posY));
}
}
return positions;
}
private Map layoutSquare(Set cluster, int compX, int compY, int compWidth, int compHeight) {
Map positions = new HashMap<>();
int n = (int)Math.round(Math.sqrt(cluster.size()));
int gridCellWidth = compWidth / n, gridCellHeight = compHeight / n;
int index = 0;
List nodes = new ArrayList<>(cluster);
nodes.sort(Comparator.comparingInt(a -> getIconById(a).priority));
for (int row = 0; row < n; row++) {
for (int col = 0; col < n; col++) {
if (index >= nodes.size()) break;
int posX = compX + col * gridCellWidth + gridCellWidth / 2;
int posY = compY + row * gridCellHeight + gridCellHeight / 2;
positions.put(nodes.get(index), new Point(posX, posY));
index++;
}
}
return positions;
}
private Map layoutForceDirected(Set cluster, int compX, int compY, int compWidth, int compHeight,
List connections) {
Map positions = new HashMap<>();
Random rand = new Random(100);
for (Integer id : cluster) {
int posX = compX + rand.nextInt(compWidth);
int posY = compY + rand.nextInt(compHeight);
positions.put(id, new Point(posX, posY));
}
double area = compWidth * compHeight;
double k = Math.sqrt(area / cluster.size());
int iterations = 100;
double temperature = compWidth / 10.0;
double cooling = temperature / (iterations + 1);
double minDistance = iconSize + slotPadding;
List compConnections = new ArrayList<>();
for (DiagramConnection conn : connections) {
if (cluster.contains(conn.idFrom) && cluster.contains(conn.idTo))
compConnections.add(conn);
}
for (int iter = 0; iter < iterations; iter++) {
Map disp = new HashMap<>();
for (Integer id : cluster) {
disp.put(id, new double[]{0.0, 0.0});
}
List nodes = new ArrayList<>(cluster);
for (int i = 0; i < nodes.size(); i++) {
Integer v = nodes.get(i);
Point posV = positions.get(v);
for (int j = i + 1; j < nodes.size(); j++) {
Integer u = nodes.get(j);
Point posU = positions.get(u);
double dx = posV.x - posU.x, dy = posV.y - posU.y;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) distance = minDistance;
double repForce = (k * k) / distance;
double[] dispV = disp.get(v);
double[] dispU = disp.get(u);
dispV[0] += (dx / distance) * repForce;
dispV[1] += (dy / distance) * repForce;
dispU[0] -= (dx / distance) * repForce;
dispU[1] -= (dy / distance) * repForce;
}
}
for (DiagramConnection conn : compConnections) {
Point posV = positions.get(conn.idFrom);
Point posU = positions.get(conn.idTo);
double dx = posV.x - posU.x, dy = posV.y - posU.y;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 0.01) distance = 0.01;
double attrForce = (distance * distance) / k;
double[] dispV = disp.get(conn.idFrom);
double[] dispU = disp.get(conn.idTo);
dispV[0] -= (dx / distance) * attrForce;
dispV[1] -= (dy / distance) * attrForce;
dispU[0] += (dx / distance) * attrForce;
dispU[1] += (dy / distance) * attrForce;
}
for (Integer id : cluster) {
double[] d = disp.get(id);
double dispLength = Math.sqrt(d[0] * d[0] + d[1] * d[1]);
if (dispLength < 0.01) dispLength = 0.01;
int newX = positions.get(id).x + (int)((d[0] / dispLength) * Math.min(dispLength, temperature));
int newY = positions.get(id).y + (int)((d[1] / dispLength) * Math.min(dispLength, temperature));
newX = Math.max(compX, Math.min(compX + compWidth, newX));
newY = Math.max(compY, Math.min(compY + compHeight, newY));
positions.get(id).x = newX;
positions.get(id).y = newY;
}
temperature -= cooling;
}
return positions;
}
// --- Manual Layouts ---
// Manual layouts use the icon index (e.g. ring number, row, level) and the priority for ordering.
private Map calculateCircularManualPositions() {
List icons = getIcons();
Map> groups = new TreeMap<>();
for (DiagramIcon icon : icons) {
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map positions = new HashMap<>();
int centerX = x + width/2, centerY = y + height/2;
int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet());
int maxRadius = Math.min(width, height)/2 - iconSize - 10;
double ringSpacing = (maxRing + 1) > 0 ? (double)maxRadius/(maxRing + 1) : 0;
for (Map.Entry> entry : groups.entrySet()) {
int ring = entry.getKey();
List groupIcons = entry.getValue();
groupIcons.sort(Comparator.comparingInt(icon -> icon.priority));
double radius = ringSpacing * (ring + 1);
int count = groupIcons.size();
double startAngle = (ring == 0) ? -Math.PI/2 : -3 * Math.PI/4;
for (int i = 0; i < count; i++) {
double angle = startAngle + 2 * Math.PI * i / count;
int posX = centerX + (int)(radius * Math.cos(angle));
int posY = centerY + (int)(radius * Math.sin(angle));
positions.put(groupIcons.get(i).id, new Point(posX, posY));
}
}
return positions;
}
private Map calculateSquareManualPositions() {
List icons = getIcons();
Map> groups = new TreeMap<>();
for (DiagramIcon icon : icons) {
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map positions = new HashMap<>();
int centerX = x + width/2, centerY = y + height/2;
int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet());
int halfSide = Math.min(width, height)/2 - iconSize - 10;
double ringSpacing = (maxRing + 1) > 0 ? (double)halfSide/(maxRing + 1) : 0;
for (Map.Entry> entry : groups.entrySet()) {
int ring = entry.getKey();
List groupIcons = entry.getValue();
groupIcons.sort(Comparator.comparingInt(icon -> icon.priority));
double offset = ringSpacing * (ring + 1);
int count = groupIcons.size();
double perimeter = 8 * offset;
for (int i = 0; i < count; i++) {
double posAlong = (perimeter * i) / count;
int posX, posY;
if (posAlong < 2 * offset) {
posX = centerX - (int)offset + (int)posAlong;
posY = centerY - (int)offset;
} else if (posAlong < 4 * offset) {
posX = centerX + (int)offset;
posY = centerY - (int)offset + (int)(posAlong - 2 * offset);
} else if (posAlong < 6 * offset) {
posX = centerX + (int)offset - (int)(posAlong - 4 * offset);
posY = centerY + (int)offset;
} else {
posX = centerX - (int)offset;
posY = centerY + (int)offset - (int)(posAlong - 6 * offset);
}
positions.put(groupIcons.get(i).id, new Point(posX, posY));
}
}
return positions;
}
private Map calculateTreeManualPositions() {
List icons = getIcons();
Map> levels = new TreeMap<>();
for (DiagramIcon icon : icons) {
levels.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map positions = new HashMap<>();
int maxLevel = levels.isEmpty() ? 0 : Collections.max(levels.keySet());
int levelHeight = height / (maxLevel + 2);
for (Map.Entry> entry : levels.entrySet()) {
int level = entry.getKey();
List levelIcons = entry.getValue();
levelIcons.sort(Comparator.comparingInt(icon -> icon.priority));
int count = levelIcons.size();
int horizontalSpacing = (width - 40) / (count + 1);
for (int i = 0; i < count; i++) {
int posX = x + 20 + (i + 1) * horizontalSpacing;
int posY = y + levelHeight * (level + 1);
positions.put(levelIcons.get(i).id, new Point(posX, posY));
}
}
return positions;
}
// --- Arrow Drawing Methods ---
/**
* Draws a connection line between two points.
* If curvedArrows is enabled, draws a quadratic Bezier curve.
*/
protected void drawConnectionLine(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) {
int color = getConnectionColor(conn);
if (!useColorScaling) color = 0xFFFFFFFF;
if (!curvedArrows) {
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glLineWidth(2.0F);
float r = ((color >> 16) & 0xFF) / 255f;
float g = ((color >> 8) & 0xFF) / 255f;
float b = (color & 0xFF) / 255f;
if (dim) { r *= 0.4f; g *= 0.4f; b *= 0.4f; }
GL11.glColor4f(r, g, b, 1f);
GL11.glBegin(GL11.GL_LINES);
GL11.glVertex2i(x1, y1);
GL11.glVertex2i(x2, y2);
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
} else {
Point start = new Point(x1, y1);
Point end = new Point(x2, y2);
double dx = x2 - x1, dy = y2 - y1;
double len = Math.sqrt(dx * dx + dy * dy);
if (len == 0) len = 1;
double angleRad = Math.toRadians(curveAngle);
double offset = len * Math.tan(angleRad) / 2;
double perpX = -dy / len, perpY = dx / len;
Point control = new Point((x1 + x2) / 2 + (int)(perpX * offset),
(y1 + y2) / 2 + (int)(perpY * offset));
int segments = 100;
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glLineWidth(2.0F);
float rr = ((color >> 16) & 0xFF) / 255f;
float gg = ((color >> 8) & 0xFF) / 255f;
float bb = (color & 0xFF) / 255f;
if (dim) { rr *= 0.4f; gg *= 0.4f; bb *= 0.4f; }
GL11.glColor4f(rr, gg, bb, 1f);
GL11.glBegin(GL11.GL_LINE_STRIP);
for (int i = 0; i <= segments; i++) {
double t = (double)i / segments;
int bx = (int)((1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * control.x + t * t * end.x);
int by = (int)((1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * control.y + t * t * end.y);
GL11.glVertex2i(bx, by);
}
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
}
}
/**
* Draws the arrow head at the end of a connection.
*/
protected void drawArrowHead(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) {
int color = getConnectionColor(conn);
if (!useColorScaling) color = 0xFFFFFFFF;
double angle;
if (!curvedArrows) {
angle = Math.atan2(y2 - y1, x2 - x1);
} else {
double dx = x2 - x1, dy = y2 - y1;
double len = Math.sqrt(dx * dx + dy * dy);
if (len == 0) len = 1;
double offset = len * Math.tan(Math.toRadians(curveAngle)) / 2;
double perpX = -dy / len, perpY = dx / len;
int cx = (x1 + x2) / 2 + (int)(perpX * offset);
int cy = (y1 + y2) / 2 + (int)(perpY * offset);
double t = 0.95;
double bx = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * cx + t * t * x2;
double by = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * cy + t * t * y2;
double dBx = 2 * (1 - t) * (cx - x1) + 2 * t * (x2 - cx);
double dBy = 2 * (1 - t) * (cy - y1) + 2 * t * (y2 - cy);
angle = Math.atan2(dBy, dBx);
}
float defenderEdgeX = x2 - (slotSize / 2f) * (float)Math.cos(angle);
float defenderEdgeY = y2 - (slotSize / 2f) * (float)Math.sin(angle);
int arrowSize = 6;
float leftX = defenderEdgeX - arrowSize * (float)Math.cos(angle - Math.PI / 6);
float leftY = defenderEdgeY - arrowSize * (float)Math.sin(angle - Math.PI / 6);
float rightX = defenderEdgeX - arrowSize * (float)Math.cos(angle + Math.PI / 6);
float rightY = defenderEdgeY - arrowSize * (float)Math.sin(angle + Math.PI / 6);
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glDisable(GL11.GL_TEXTURE_2D);
float rr = ((color >> 16) & 0xFF) / 255f;
float gg = ((color >> 8) & 0xFF) / 255f;
float bb = (color & 0xFF) / 255f;
if(dim){ rr *= 0.4f; gg *= 0.4f; bb *= 0.4f; }
GL11.glColor4f(rr, gg, bb, 1f);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glBegin(GL11.GL_TRIANGLES);
GL11.glVertex2f(defenderEdgeX, defenderEdgeY);
GL11.glVertex2f(leftX, leftY);
GL11.glVertex2f(rightX, rightY);
GL11.glEnd();
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
}
// --- Standard Icon Box Drawing ---
protected void drawIconBox(int slotX, int slotY, int slotSize, boolean highlighted) {
int bg = highlighted ? mixColors(iconBackgroundColor, 0xFFFFFFFF, 0.2f) : iconBackgroundColor;
drawRect(slotX, slotY, slotX + slotSize, slotY + slotSize, bg);
for (int i = 0; i < iconBorderThickness; i++) {
drawHorizontalLine(slotX + i, slotX + slotSize - i, slotY + i, iconBorderColor);
drawHorizontalLine(slotX + i, slotX + slotSize - i, slotY + slotSize - 1 - i, iconBorderColor);
drawVerticalLine(slotX + i, slotY + i, slotY + slotSize - i, iconBorderColor);
drawVerticalLine(slotX + slotSize - 1 - i, slotY + i, slotY + slotSize - i, iconBorderColor);
}
}
protected int mixColors(int color1, int color2, float ratio) {
int a1 = (color1 >> 24) & 0xFF, r1 = (color1 >> 16) & 0xFF, g1 = (color1 >> 8) & 0xFF, b1 = color1 & 0xFF;
int a2 = (color2 >> 24) & 0xFF, r2 = (color2 >> 16) & 0xFF, g2 = (color2 >> 8) & 0xFF, b2 = color2 & 0xFF;
int a = (int)(a1 + (a2 - a1) * ratio);
int r = (int)(r1 + (r2 - r1) * ratio);
int g = (int)(g1 + (g2 - g1) * ratio);
int b = (int)(b1 + (b2 - b1) * ratio);
return (a << 24) | (r << 16) | (g << 8) | b;
}
// --- Hit-detection helpers ---
private double distanceToBezier(Point start, Point control, Point end, int segments, int px, int py) {
double minDist = Double.MAX_VALUE;
Point prev = null;
for (int i = 0; i <= segments; i++) {
double t = (double)i / segments;
int x = (int)((1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * control.x + t * t * end.x);
int y = (int)((1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * control.y + t * t * end.y);
Point curr = new Point(x, y);
if (prev != null) {
double d = distancePointToSegment(px, py, prev, curr);
if(d < minDist) minDist = d;
}
prev = curr;
}
return minDist;
}
private double distancePointToSegment(int px, int py, Point a, Point b) {
double A = px - a.x, B = py - a.y;
double C = b.x - a.x, D = b.y - a.y;
double dot = A * C + B * D;
double lenSq = C * C + D * D;
double param = (lenSq != 0) ? (dot / lenSq) : -1;
double xx, yy;
if(param < 0){ xx = a.x; yy = a.y; }
else if(param > 1){ xx = b.x; yy = b.y; }
else { xx = a.x + param * C; yy = a.y + param * D; }
double dx = px - xx, dy = py - yy;
return Math.sqrt(dx * dx + dy * dy);
}
// --- CHART Layout Drawing ---
/**
* Draws the CHART layout.
*
* - The grid has (n+1) columns and rows, where n is the number of icons.
* - The top row and left column show header icons.
* - Each inner cell is filled with the connection color (if one exists) or a darker gray otherwise.
* - Hovering over a header or inner cell highlights the corresponding column/row or cell (and its two header icons),
* while dimming non-relevant cells.
* - Zoom and pan are applied to the entire chart.
*
*/
private void drawChartDiagram(int mouseX, int mouseY, boolean subGui) {
Minecraft mc = Minecraft.getMinecraft();
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int factor = sr.getScaleFactor();
// 1. Draw the static background for the entire diagram.
drawRect(x, y, x + width, y + height, 0xFF333333);
// 2. Setup scissor to clip drawing to the diagram area.
GL11.glEnable(GL11.GL_SCISSOR_TEST);
GL11.glScissor(x * factor, (sr.getScaledHeight() - (y + height)) * factor, width * factor, height * factor);
// 3. Push a matrix and apply pan/zoom for grid, icons, and overlays.
GL11.glPushMatrix();
int centerX = x + width / 2;
int centerY = y + height / 2;
GL11.glTranslatef(centerX + panX, centerY + panY, 0);
GL11.glScalef(zoom, zoom, 1.0f);
GL11.glTranslatef(-centerX, -centerY, 0);
// 4. Compute grid parameters.
List icons = getIcons();
int n = icons.size();
int cols = n + 1, rows = n + 1;
int cellSize = Math.min(width / cols, height / rows);
int gridWidth = cellSize * cols;
int gridHeight = cellSize * rows;
int startX = x + (width - gridWidth) / 2;
int startY = y + (height - gridHeight) / 2;
// 5. Draw grid cells.
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int cellX = startX + col * cellSize;
int cellY = startY + row * cellSize;
int fillColor = 0xFFCCCCCC; // Default for header cells.
if (row > 0 && col > 0) { // inner cell: use connection color (or darker gray)
DiagramIcon leftIcon = icons.get(row - 1);
DiagramIcon topIcon = icons.get(col - 1);
DiagramConnection conn = getConnectionByIds(leftIcon.id, topIcon.id);
fillColor = (conn != null) ? getConnectionColor(conn) : 0xFF777777;
}
drawRect(cellX, cellY, cellX + cellSize, cellY + cellSize, fillColor);
}
}
// 6. Draw grid lines.
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glColor4f(0, 0, 0, 1);
GL11.glLineWidth(1.0f);
GL11.glBegin(GL11.GL_LINES);
for (int col = 0; col <= cols; col++) {
int xLine = startX + col * cellSize;
GL11.glVertex2i(xLine, startY);
GL11.glVertex2i(xLine, startY + gridHeight);
}
for (int row = 0; row <= rows; row++) {
int yLine = startY + row * cellSize;
GL11.glVertex2i(startX, yLine);
GL11.glVertex2i(startX + gridWidth, yLine);
}
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
// 7. Draw header icons (top and left) scaled to fit within each cell.
float iconMargin = 1f; // margin in pixels inside the cell.
float iconScale = (cellSize - iconMargin) / (float)iconSize;
for (int i = 0; i < n; i++) {
// Top header icon:
int topHeaderCenterX = startX + (i + 1) * cellSize + cellSize / 2;
int topHeaderCenterY = startY + cellSize / 2;
GL11.glPushMatrix();
GL11.glTranslatef(topHeaderCenterX, topHeaderCenterY, 0);
GL11.glScalef(iconScale, iconScale, 1);
// renderIcon should draw centered at (0,0)
renderIcon(icons.get(i), 0, 0, IconRenderState.DEFAULT);
GL11.glPopMatrix();
// Left header icon:
int leftHeaderCenterX = startX + cellSize / 2;
int leftHeaderCenterY = startY + (i + 1) * cellSize + cellSize / 2;
GL11.glPushMatrix();
GL11.glTranslatef(leftHeaderCenterX, leftHeaderCenterY, 0);
GL11.glScalef(iconScale, iconScale, 1);
renderIcon(icons.get(i), 0, 0, IconRenderState.DEFAULT);
GL11.glPopMatrix();
}
// 8. Compute effective mouse coordinates (reverse pan/zoom).
int effectiveMouseX = (int)(((float)mouseX - (centerX + panX)) / zoom + centerX);
int effectiveMouseY = (int)(((float)mouseY - (centerY + panY)) / zoom + centerY);
// 9. Determine hover state on the grid.
boolean hoverInGrid = (effectiveMouseX >= startX && effectiveMouseX < startX + gridWidth &&
effectiveMouseY >= startY && effectiveMouseY < startY + gridHeight);
int hoveredRow = -1, hoveredCol = -1;
Set highlightTop = new HashSet<>();
Set highlightLeft = new HashSet<>();
List tooltipToShow = null;
if (hoverInGrid) {
int colIndex = (effectiveMouseX - startX) / cellSize;
int rowIndex = (effectiveMouseY - startY) / cellSize;
// If hovering over a top header cell.
if (rowIndex == 0 && colIndex >= 1) {
highlightTop.add(colIndex - 1);
tooltipToShow = getIconTooltip(icons.get(colIndex - 1));
onIconHover(icons.get(colIndex - 1));
}
// If hovering over a left header cell.
else if (colIndex == 0 && rowIndex >= 1) {
highlightLeft.add(rowIndex - 1);
tooltipToShow = getIconTooltip(icons.get(rowIndex - 1));
onIconHover(icons.get(rowIndex - 1));
}
// Else, hovering over an inner cell.
else if (rowIndex >= 1 && colIndex >= 1) {
hoveredRow = rowIndex - 1;
hoveredCol = colIndex - 1;
highlightTop.add(hoveredCol);
highlightLeft.add(hoveredRow);
DiagramIcon leftIcon = icons.get(hoveredRow);
DiagramIcon topIcon = icons.get(hoveredCol);
DiagramConnection conn = getConnectionByIds(leftIcon.id, topIcon.id);
if (conn != null) {
tooltipToShow = getConnectionTooltip(conn);
onConnectionHover(conn);
}
}
}
// 10. Draw hover overlay:
// Dim all inner cells (row>=1 & col>=1) not in highlighted row/column.
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glColor4f(0, 0, 0, 0.5f);
GL11.glBegin(GL11.GL_QUADS);
for (int r = 1; r < rows; r++) {
for (int c = 1; c < cols; c++) {
boolean keepBright = false;
if (!highlightTop.isEmpty() && highlightTop.contains(c - 1))
keepBright = true;
if (!highlightLeft.isEmpty() && highlightLeft.contains(r - 1))
keepBright = true;
// Also, if a specific cell is hovered, its entire row/column is bright.
if (hoveredRow != -1 && hoveredCol != -1) {
if (r - 1 == hoveredRow || c - 1 == hoveredCol)
keepBright = true;
}
if (!keepBright) {
int cellX = startX + c * cellSize;
int cellY = startY + r * cellSize;
GL11.glVertex2i(cellX, cellY);
GL11.glVertex2i(cellX + cellSize, cellY);
GL11.glVertex2i(cellX + cellSize, cellY + cellSize);
GL11.glVertex2i(cellX, cellY + cellSize);
}
}
}
GL11.glEnd();
// Then draw a white overlay on the highlighted headers/inner cell.
GL11.glColor4f(1, 1, 1, 0.3f);
GL11.glBegin(GL11.GL_QUADS);
for (int i = 0; i < n; i++) {
if (highlightTop.contains(i)) {
int hx = startX + (i + 1) * cellSize;
int hy = startY;
GL11.glVertex2i(hx, hy);
GL11.glVertex2i(hx + cellSize, hy);
GL11.glVertex2i(hx + cellSize, hy + cellSize);
GL11.glVertex2i(hx, hy + cellSize);
}
if (highlightLeft.contains(i)) {
int hx = startX;
int hy = startY + (i + 1) * cellSize;
GL11.glVertex2i(hx, hy);
GL11.glVertex2i(hx + cellSize, hy);
GL11.glVertex2i(hx + cellSize, hy + cellSize);
GL11.glVertex2i(hx, hy + cellSize);
}
}
if (hoveredRow != -1 && hoveredCol != -1) {
int cellX = startX + (hoveredCol + 1) * cellSize;
int cellY = startY + (hoveredRow + 1) * cellSize;
GL11.glVertex2i(cellX, cellY);
GL11.glVertex2i(cellX + cellSize, cellY);
GL11.glVertex2i(cellX + cellSize, cellY + cellSize);
GL11.glVertex2i(cellX, cellY + cellSize);
}
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
// End pan/zoom block.
GL11.glPopMatrix();
GL11.glDisable(GL11.GL_SCISSOR_TEST);
// 11. Draw tooltip (using raw screen coordinates).
if (tooltipToShow != null && !tooltipToShow.isEmpty()) {
drawHoveringText(tooltipToShow, mouseX, mouseY, mc.fontRenderer);
}
}
/**
* Main method for drawing the diagram.
* If layout == CHART, delegates to drawChartDiagram().
* Otherwise, applies pan/zoom, draws connections (with hit-detection fixed for curves)
* and icons, and shows tooltips and hover events.
*/
public void drawDiagram(int mouseX, int mouseY, boolean subGui) {
boolean allowInput = (parent == null || !parent.hasSubGui());
if (allowInput && isWithin(mouseX, mouseY) && !subGui)
handleMouseScroll(Mouse.getDWheel());
if (layout == EnumDiagramLayout.CHART) {
drawChartDiagram(mouseX, mouseY, subGui);
return;
}
Minecraft mc = Minecraft.getMinecraft();
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int factor = sr.getScaleFactor();
Map positions = calculatePositions();
int centerX = x + width / 2, centerY = y + height / 2;
int effectiveMouseX = (int)(((float)mouseX - (centerX + panX)) / zoom + centerX);
int effectiveMouseY = (int)(((float)mouseY - (centerY + panY)) / zoom + centerY);
Integer hoveredIconId = null;
int hoveredConnFrom = -1, hoveredConnTo = -1;
Set selectedIconIds = new HashSet<>();
// Check if mouse is over an icon.
for (DiagramIcon icon : getIcons()) {
if (!icon.enabled) continue;
Point pos = positions.get(icon.id);
if (pos == null) continue;
int slotX = pos.x - slotSize / 2, slotY = pos.y - slotSize / 2;
if (effectiveMouseX >= slotX && effectiveMouseX < slotX + slotSize &&
effectiveMouseY >= slotY && effectiveMouseY < slotY + slotSize) {
hoveredIconId = icon.id;
selectedIconIds.add(icon.id);
onIconHover(icon);
break;
}
}
// If no icon was hovered, check connections.
if (hoveredIconId == null) {
final double threshold = 5.0;
outer:
for (DiagramConnection conn : getConnections()) {
DiagramIcon iconFrom = getIconById(conn.idFrom);
DiagramIcon iconTo = getIconById(conn.idTo);
if (iconFrom == null || iconTo == null || !iconFrom.enabled || !iconTo.enabled)
continue;
Point pFrom = positions.get(conn.idFrom);
Point pTo = positions.get(conn.idTo);
if (pFrom == null || pTo == null) continue;
double dist;
if (curvedArrows) {
double dx = pTo.x - pFrom.x, dy = pTo.y - pFrom.y;
double len = Math.sqrt(dx * dx + dy * dy);
if (len == 0) len = 1;
double offset = len * Math.tan(Math.toRadians(curveAngle)) / 2;
double perpX = -dy / len, perpY = dx / len;
Point control = new Point((pFrom.x + pTo.x) / 2 + (int)(perpX * offset),
(pFrom.y + pTo.y) / 2 + (int)(perpY * offset));
dist = distanceToBezier(pFrom, control, pTo, 200, effectiveMouseX, effectiveMouseY);
} else {
dist = pointLineDistance(effectiveMouseX, effectiveMouseY, pFrom.x, pFrom.y, pTo.x, pTo.y);
}
if (dist < threshold) {
hoveredConnFrom = conn.idFrom;
hoveredConnTo = conn.idTo;
selectedIconIds.add(conn.idFrom);
selectedIconIds.add(conn.idTo);
onConnectionHover(conn);
break outer;
}
}
}
// Setup scissor.
GL11.glEnable(GL11.GL_SCISSOR_TEST);
GL11.glScissor(x * factor, (sr.getScaledHeight() - (y + height)) * factor, width * factor, height * factor);
drawRect(x, y, x + width, y + height, 0xFF333333);
GL11.glPushMatrix();
GL11.glTranslatef(centerX + panX, centerY + panY, 0);
GL11.glScalef(zoom, zoom, 1.0f);
GL11.glTranslatef(-centerX, -centerY, 0);
// Draw connections.
for (DiagramConnection conn : getConnections()) {
DiagramIcon iconFrom = getIconById(conn.idFrom);
DiagramIcon iconTo = getIconById(conn.idTo);
if (iconFrom == null || iconTo == null || !iconFrom.enabled || !iconTo.enabled)
continue;
Point pFrom = positions.get(conn.idFrom);
Point pTo = positions.get(conn.idTo);
if (pFrom == null || pTo == null) continue;
int ax = pFrom.x, ay = pFrom.y, bx = pTo.x, by = pTo.y;
boolean dim = !selectedIconIds.isEmpty() &&
!(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo));
drawConnectionLine(ax, ay, bx, by, conn, dim);
}
// Draw icons.
for (DiagramIcon icon : getIcons()) {
if (!icon.enabled) continue;
Point pos = positions.get(icon.id);
if (pos == null) continue;
int slotX = pos.x - slotSize / 2, slotY = pos.y - slotSize / 2;
boolean highlighted = selectedIconIds.isEmpty() || selectedIconIds.contains(icon.id);
drawIconBox(slotX, slotY, slotSize, highlighted);
IconRenderState state = highlighted ? IconRenderState.HIGHLIGHTED : IconRenderState.NOT_HIGHLIGHTED;
renderIcon(icon, pos.x, pos.y, state);
}
GL11.glPopMatrix();
// Draw arrow heads.
if (showArrowHeads) {
GL11.glPushMatrix();
GL11.glTranslatef(centerX + panX, centerY + panY, 0);
GL11.glScalef(zoom, zoom, 1.0f);
GL11.glTranslatef(-centerX, -centerY, 0);
for (DiagramConnection conn : getConnections()) {
DiagramIcon iconFrom = getIconById(conn.idFrom);
DiagramIcon iconTo = getIconById(conn.idTo);
if (iconFrom == null || iconTo == null || !iconFrom.enabled || !iconTo.enabled)
continue;
Point pFrom = positions.get(conn.idFrom);
Point pTo = positions.get(conn.idTo);
if (pFrom == null || pTo == null) continue;
int ax = pFrom.x, ay = pFrom.y, bx = pTo.x, by = pTo.y;
boolean dim = !selectedIconIds.isEmpty() &&
!(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo));
drawArrowHead(ax, ay, bx, by, conn, dim);
}
GL11.glPopMatrix();
}
GL11.glDisable(GL11.GL_SCISSOR_TEST);
// Show tooltips if an icon or connection was hovered.
if (hoveredIconId != null) {
DiagramIcon icon = getIconById(hoveredIconId);
List tooltip = getIconTooltip(icon);
if (tooltip != null && !tooltip.isEmpty())
drawHoveringText(tooltip, mouseX, mouseY, mc.fontRenderer);
} else if (hoveredConnFrom != -1 && hoveredConnTo != -1) {
DiagramConnection conn = getConnectionByIds(hoveredConnFrom, hoveredConnTo);
List tooltip = getConnectionTooltip(conn);
if (tooltip != null && !tooltip.isEmpty())
drawHoveringText(tooltip, mouseX, mouseY, mc.fontRenderer);
}
}
// --- Utility Methods ---
protected DiagramIcon getIconById(int id) {
for (DiagramIcon icon : getIcons()) {
if (icon.id == id) return icon;
}
return null;
}
protected DiagramConnection getConnectionByIds(int idFrom, int idTo) {
for (DiagramConnection conn : getConnections()) {
if (conn.idFrom == idFrom && conn.idTo == idTo) return conn;
}
return null;
}
protected int getConnectionColor(DiagramConnection conn) {
float percent = conn.percent;
if (percent <= 0.10f) return 0xFF80FF80;
else if (percent <= 0.40f) return 0xFFCCFF66;
else if (percent <= 0.50f) return 0xFFFFA500;
else {
float ratio = (percent - 0.50f) / 0.50f;
ratio = Math.min(1f, Math.max(0f, ratio));
int r1 = 0xFF, g1 = 0xA5, b1 = 0x00;
int r2 = 0xFF, g2 = 0x00, b2 = 0x00;
int r = r1 + (int)((r2 - r1) * ratio);
int g = g1 + (int)((g2 - g1) * ratio);
int b = b1 + (int)((b2 - b1) * ratio);
return (0xFF << 24) | (r << 16) | (g << 8) | b;
}
}
private double pointLineDistance(int px, int py, int x1, int y1, int x2, int y2) {
double A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1;
double dot = A * C + B * D;
double lenSq = C * C + D * D;
double param = (lenSq != 0) ? (dot / lenSq) : -1;
double xx, yy;
if (param < 0) { xx = x1; yy = y1; }
else if (param > 1) { xx = x2; yy = y2; }
else { xx = x1 + param * C; yy = y1 + param * D; }
double dx = px - xx, dy = py - yy;
return Math.sqrt(dx * dx + dy * dy);
}
// --- Inner Helper Classes ---
public static class DiagramIcon {
public int id;
public boolean enabled = true;
public boolean pressable = false;
public int index = 0; // For row, ring, or level.
public int priority = 0; // For ordering.
public DiagramIcon(int id) { this.id = id; }
public DiagramIcon(int id, int index, int priority) {
this.id = id;
this.index = index;
this.priority = priority;
}
}
public static class DiagramConnection {
public int idFrom, idTo;
public float percent;
public String hoverText;
public DiagramConnection(int idFrom, int idTo, float percent, String hoverText) {
this.idFrom = idFrom;
this.idTo = idTo;
this.percent = percent;
this.hoverText = hoverText;
}
}
// Used by the CHART layout to represent an inner cell.
private static class ConnectionCell {
int x, y, size;
DiagramConnection conn;
ConnectionCell(int x, int y, int size, DiagramConnection conn) {
this.x = x; this.y = y; this.size = size; this.conn = conn;
}
}
// Simple helper class for rectangles.
private static class Rectangle {
int x, y, width, height;
Rectangle(int x, int y, int width, int height) {
this.x = x; this.y = y; this.width = width; this.height = height;
}
int centerX() { return x + width / 2; }
int centerY() { return y + height / 2; }
}
public enum IconRenderState {
DEFAULT,
HIGHLIGHTED,
NOT_HIGHLIGHTED;
}
// --- Mouse Handling ---
public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) {
if (parent != null && parent.hasSubGui()) return false;
if (mouseButton == 0) {
for (DiagramIcon icon : getIcons()) {
if (!icon.enabled || !icon.pressable) continue;
Point pos = calculatePositions().get(icon.id);
if (pos == null) continue;
int centerX = x + width / 2, centerY = y + height / 2;
int effectiveX = (int)(((float)mouseX - (centerX + panX)) / zoom + centerX);
int effectiveY = (int)(((float)mouseY - (centerY + panY)) / zoom + centerY);
int slotX = pos.x - slotSize / 2, slotY = pos.y - slotSize / 2;
if (effectiveX >= slotX && effectiveX < slotX + slotSize &&
effectiveY >= slotY && effectiveY < slotY + slotSize) {
onIconClick(icon);
currentlyPressedIcon = icon;
return true;
}
}
if (isWithin(mouseX, mouseY)) {
dragging = true;
lastDragX = mouseX;
lastDragY = mouseY;
return true;
}
}
return false;
}
protected DiagramIcon currentlyPressedIcon = null;
public void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) {
if (parent != null && parent.hasSubGui()) return;
if (dragging) {
int dx = mouseX - lastDragX, dy = mouseY - lastDragY;
panX += dx / zoom * 0.7f;
panY += dy / zoom * 0.7f;
lastDragX = mouseX;
lastDragY = mouseY;
}
if (currentlyPressedIcon != null) onIconHeld(currentlyPressedIcon);
}
public void mouseReleased(int mouseX, int mouseY, int state) {
if (parent != null && parent.hasSubGui()) return;
if (currentlyPressedIcon != null) {
onIconRelease(currentlyPressedIcon);
currentlyPressedIcon = null;
}
dragging = false;
}
public boolean isWithin(int mouseX, int mouseY) {
if (parent.hasSubGui())
return false;
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
}
/**
* Handles zooming based on mouse wheel scrolling.
*/
public void handleMouseScroll(int scrollDelta) {
zoom += scrollDelta * 0.0009f;
if (zoom < 0.5f) zoom = 0.5f;
if (zoom > 2.0f) zoom = 2.0f;
}
/**
* Override to render tooltips.
*/
protected void drawHoveringText(List text, int mouseX, int mouseY, FontRenderer fontRenderer) {}
// --- Flag Setters ---
public void setShowArrowHeads(boolean showArrowHeads) {
this.showArrowHeads = showArrowHeads;
}
public void setUseColorScaling(boolean useColorScaling) {
this.useColorScaling = useColorScaling;
}
// --- Icon and Connection Event Callbacks ---
/**
* Called when an icon is clicked.
*/
protected void onIconClick(DiagramIcon icon) {
System.out.println("Icon " + icon.id + " clicked.");
}
/**
* Called when an icon is held.
*/
protected void onIconHeld(DiagramIcon icon) {
System.out.println("Icon " + icon.id + " held.");
}
/**
* Called when an icon is released.
*/
protected void onIconRelease(DiagramIcon icon) {
System.out.println("Icon " + icon.id + " released.");
}
/**
* Called when an icon is hovered over.
*/
protected void onIconHover(DiagramIcon icon) {
// Override to handle icon hovering events.
}
/**
* Called when a connection is hovered over.
*/
protected void onConnectionHover(DiagramConnection conn) {
// Override to handle connection hovering events.
}
}