Files
CustomNPC-Plus/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java
T
2025-03-03 23:03:12 -05:00

1293 lines
57 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:
* <ul>
* <li>Parent reference to disable mouse interaction when a subGUI is active.</li>
* <li>Caching for icons and connections (subclasses must implement createIcons() and createConnections()).</li>
* <li>Support for different layout types (circular, square, tree, generated, and manual versions)
* plus a CHART layout.</li>
* <li>Drawing of connections as either straight or curved arrows (with hit‐detection improved for curves).</li>
* <li>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.
* </li>
* <li>Zooming and panning controls apply to both chart and non‑chart layouts.</li>
* <li>Callbacks for icon and connection interactions including click, held, release and hover events.</li>
* </ul>
*/
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<Integer, Point> cachedPositions = null; // Cached positions for nodes (non-CHART)
protected List<DiagramIcon> iconsCache = null; // Cached list of icons
protected List<DiagramConnection> 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<DiagramIcon> createIcons();
protected abstract List<DiagramConnection> createConnections();
protected final List<DiagramIcon> getIcons() {
if (iconsCache == null) {
iconsCache = createIcons();
}
return iconsCache;
}
protected final List<DiagramConnection> 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<String> getIconTooltip(DiagramIcon icon) { return null; }
/**
* Returns the tooltip text lines for a connection.
*/
protected List<String> getConnectionTooltip(DiagramConnection conn) {
List<String> 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<Integer, Point> 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<Integer, Point> calculateCircularPositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, Point> 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<Integer, Point> calculateSquarePositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, Point> 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<Integer, Point> calculateTreePositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, Point> 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<Integer, Point> calculateGeneratedPositions() {
List<DiagramIcon> icons = getIcons();
List<DiagramConnection> connections = getConnections();
Map<Integer, List<Integer>> graph = buildGraph(icons, connections);
List<Set<Integer>> 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<Integer, Point> globalPositions = new HashMap<>();
int clusterIndex = 0;
for (Set<Integer> 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<Integer, Point> 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<Integer, List<Integer>> buildGraph(List<DiagramIcon> icons, List<DiagramConnection> connections) {
Map<Integer, List<Integer>> 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<Set<Integer>> getConnectedComponents(Map<Integer, List<Integer>> graph) {
Set<Integer> visited = new HashSet<>();
List<Set<Integer>> components = new ArrayList<>();
for (Integer node : graph.keySet()) {
if (!visited.contains(node)) {
Set<Integer> comp = new HashSet<>();
dfsComponent(node, graph, visited, comp);
components.add(comp);
}
}
return components;
}
private void dfsComponent(Integer current, Map<Integer, List<Integer>> graph, Set<Integer> visited, Set<Integer> 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<Integer> cluster, Map<Integer, List<Integer>> 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<Integer> cluster, Map<Integer, List<Integer>> 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<Integer> cluster) {
int n = cluster.size();
int sqrt = (int)Math.round(Math.sqrt(n));
return sqrt * sqrt == n;
}
// --- Layout Algorithms for Clusters ---
private Map<Integer, Point> layoutCycle(Set<Integer> cluster, int compX, int compY, int compWidth, int compHeight) {
Map<Integer, Point> 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<Integer, Point> layoutTree(Set<Integer> cluster, int compX, int compY, int compWidth, int compHeight,
Map<Integer, List<Integer>> fullGraph) {
Map<Integer, Point> positions = new HashMap<>();
Map<Integer, List<Integer>> subgraph = new HashMap<>();
for (Integer id : cluster) {
List<Integer> 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<Integer, Integer> levelMap = new HashMap<>();
Map<Integer, List<Integer>> levels = new HashMap<>();
Queue<Integer> queue = new LinkedList<>();
Set<Integer> 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<Integer, List<Integer>> entry : levels.entrySet()) {
int lvl = entry.getKey();
List<Integer> 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<Integer, Point> layoutSquare(Set<Integer> cluster, int compX, int compY, int compWidth, int compHeight) {
Map<Integer, Point> positions = new HashMap<>();
int n = (int)Math.round(Math.sqrt(cluster.size()));
int gridCellWidth = compWidth / n, gridCellHeight = compHeight / n;
int index = 0;
List<Integer> 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<Integer, Point> layoutForceDirected(Set<Integer> cluster, int compX, int compY, int compWidth, int compHeight,
List<DiagramConnection> connections) {
Map<Integer, Point> 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<DiagramConnection> 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<Integer, double[]> disp = new HashMap<>();
for (Integer id : cluster) {
disp.put(id, new double[]{0.0, 0.0});
}
List<Integer> 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<Integer, Point> calculateCircularManualPositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, List<DiagramIcon>> groups = new TreeMap<>();
for (DiagramIcon icon : icons) {
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map<Integer, Point> 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<Integer, List<DiagramIcon>> entry : groups.entrySet()) {
int ring = entry.getKey();
List<DiagramIcon> 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<Integer, Point> calculateSquareManualPositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, List<DiagramIcon>> groups = new TreeMap<>();
for (DiagramIcon icon : icons) {
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map<Integer, Point> 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<Integer, List<DiagramIcon>> entry : groups.entrySet()) {
int ring = entry.getKey();
List<DiagramIcon> 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<Integer, Point> calculateTreeManualPositions() {
List<DiagramIcon> icons = getIcons();
Map<Integer, List<DiagramIcon>> levels = new TreeMap<>();
for (DiagramIcon icon : icons) {
levels.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
}
Map<Integer, Point> positions = new HashMap<>();
int maxLevel = levels.isEmpty() ? 0 : Collections.max(levels.keySet());
int levelHeight = height / (maxLevel + 2);
for (Map.Entry<Integer, List<DiagramIcon>> entry : levels.entrySet()) {
int level = entry.getKey();
List<DiagramIcon> 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.
* <ul>
* <li>The grid has (n+1) columns and rows, where n is the number of icons.</li>
* <li>The top row and left column show header icons.</li>
* <li>Each inner cell is filled with the connection color (if one exists) or a darker gray otherwise.</li>
* <li>Hovering over a header or inner cell highlights the corresponding column/row or cell (and its two header icons),
* while dimming non-relevant cells.</li>
* <li>Zoom and pan are applied to the entire chart.</li>
* </ul>
*/
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<DiagramIcon> 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<Integer> highlightTop = new HashSet<>();
Set<Integer> highlightLeft = new HashSet<>();
List<String> 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<Integer, Point> 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<Integer> 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<String> 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<String> 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<String> 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.
}
}