mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 05:27:37 +00:00
1553 lines
67 KiB
Java
1553 lines
67 KiB
Java
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 net.minecraft.client.renderer.entity.RenderItem;
|
||
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).
|
||
* <p>
|
||
* This revised version ensures that non‑Chart layouts use the full diagram bounds for positioning
|
||
* and that curved arrow drawing selects the best control point (using both curveAngle and its negative)
|
||
* to avoid icons.
|
||
*/
|
||
public abstract class GuiDiagram extends Gui {
|
||
|
||
protected RenderItem renderItem = new RenderItem();
|
||
|
||
// --- Diagram Area & Controls ---
|
||
protected int x, y, width, height;
|
||
protected float panX = 0, panY = 0;
|
||
protected float zoom = 1.0f;
|
||
protected boolean dragging = false;
|
||
protected int lastDragX, lastDragY;
|
||
|
||
// --- Node (Icon) Sizing ---
|
||
protected int iconSize = 16;
|
||
protected int slotPadding = 4;
|
||
protected int slotSize = iconSize + slotPadding;
|
||
|
||
// --- Icon Box Appearance ---
|
||
protected int iconBackgroundColor = 0xFF999999;
|
||
protected int iconBorderColor = 0xFF555555;
|
||
protected int iconBorderThickness = 1;
|
||
|
||
// --- Connection Options ---
|
||
protected boolean showArrowHeads = true;
|
||
protected boolean useColorScaling = true;
|
||
|
||
// This flag determines how two‑way connections are drawn.
|
||
// When true, a two‑way connection is drawn as one line with dual arrowheads and combined tooltips.
|
||
// When false, each direction is drawn as a separate line with its own hitbox and tooltip.
|
||
protected boolean allowTwoWay = false;
|
||
|
||
// --- Layout Options ---
|
||
protected EnumDiagramLayout layout = EnumDiagramLayout.CIRCULAR;
|
||
|
||
public void setLayout(EnumDiagramLayout layout) {
|
||
this.layout = layout;
|
||
invalidateCache();
|
||
}
|
||
|
||
// --- Arrow Drawing Options ---
|
||
protected boolean curvedArrows = true;
|
||
protected int curveAngle = 15; // 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;
|
||
protected List<DiagramIcon> iconsCache = null;
|
||
protected List<DiagramConnection> connectionsCache = null;
|
||
|
||
public void invalidateCache() {
|
||
cachedPositions = null;
|
||
iconsCache = null;
|
||
connectionsCache = null;
|
||
}
|
||
|
||
// --- Parent reference ---
|
||
protected GuiNPCInterface parent;
|
||
|
||
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) ---
|
||
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);
|
||
|
||
protected List<String> getIconTooltip(DiagramIcon icon) {
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Returns the tooltip for a connection. When allowTwoWay is true the tooltip combines both directions,
|
||
* otherwise it shows only the current connection's info.
|
||
*/
|
||
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);
|
||
if (allowTwoWay) {
|
||
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 a mapping from node ID to (x,y) position.
|
||
*/
|
||
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 ---
|
||
|
||
// Circular: use the full available radius so that outer icons are flush with bounds.
|
||
protected Map<Integer, Point> calculateCircularPositions() {
|
||
List<DiagramIcon> icons = getIcons();
|
||
Map<Integer, Point> positions = new HashMap<>();
|
||
int count = icons.size();
|
||
if (count == 0) return positions;
|
||
int centerX = x + width / 2, centerY = y + height / 2;
|
||
int radius = (Math.min(width, height) - slotSize) / 2;
|
||
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;
|
||
}
|
||
|
||
// Square: fill the entire bounds.
|
||
protected Map<Integer, Point> calculateSquarePositions() {
|
||
List<DiagramIcon> icons = getIcons();
|
||
Map<Integer, Point> positions = new HashMap<>();
|
||
int count = icons.size();
|
||
if (count == 0) return positions;
|
||
int n = (int) Math.ceil(Math.sqrt(count));
|
||
int cellWidth = width / n;
|
||
int cellHeight = height / n;
|
||
int startX = x, startY = y;
|
||
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;
|
||
}
|
||
|
||
// Tree: distribute levels vertically and icons horizontally.
|
||
protected Map<Integer, Point> calculateTreePositions() {
|
||
List<DiagramIcon> icons = getIcons();
|
||
Map<Integer, Point> positions = new HashMap<>();
|
||
int count = icons.size();
|
||
if (count == 0) return positions;
|
||
int levels = (int) Math.ceil(Math.log(count + 1) / Math.log(2));
|
||
for (int level = 0; level < levels; level++) {
|
||
int nodesThisLevel = (level == 0) ? 1 : (int) Math.min(Math.pow(2, level), count - (int) Math.pow(2, level) + 1);
|
||
int posY = (levels == 1) ? y + height / 2 : y + (int) ((height * level) / (levels - 1.0));
|
||
for (int i = 0; i < nodesThisLevel; i++) {
|
||
int posX = (nodesThisLevel == 1) ? x + width / 2 : x + (int) ((width * i) / (nodesThisLevel - 1.0));
|
||
int index = (int) (Math.pow(2, level) - 1 + i);
|
||
if (index < count) {
|
||
positions.put(icons.get(index).id, new Point(posX, posY));
|
||
}
|
||
}
|
||
}
|
||
return positions;
|
||
}
|
||
|
||
// Generated layout methods (unchanged)...
|
||
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);
|
||
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 + col * (width / gridCols);
|
||
int compY = y + row * (height / gridRows);
|
||
int compWidth = width / gridCols;
|
||
int compHeight = height / gridRows;
|
||
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 (unchanged methods: layoutCycle, layoutTree, layoutSquare, layoutForceDirected)...
|
||
private Map<Integer, Point> layoutCycle(Set<Integer> cluster, int compX, int compY, int compWidth, int compHeight) {
|
||
Map<Integer, Point> positions = new HashMap<>();
|
||
if (cluster.isEmpty()) return positions;
|
||
int centerX = compX + compWidth / 2, centerY = compY + compHeight / 2;
|
||
int radius = (Math.min(compWidth, compHeight) - slotSize) / 2;
|
||
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<>();
|
||
if (cluster.isEmpty()) return positions;
|
||
|
||
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 = Collections.min(cluster);
|
||
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);
|
||
for (Map.Entry<Integer, List<Integer>> entry : levels.entrySet()) {
|
||
int lvl = entry.getKey();
|
||
int posY = (maxLevel == 0) ? compY + compHeight / 2 : compY + (int) ((compHeight * lvl) / (maxLevel));
|
||
List<Integer> nodesAtLevel = entry.getValue();
|
||
nodesAtLevel.sort(Comparator.naturalOrder());
|
||
int m = nodesAtLevel.size();
|
||
for (int i = 0; i < m; i++) {
|
||
int posX = (m == 1) ? compX + compWidth / 2 : compX + (int) ((compWidth * i) / (m - 1.0));
|
||
positions.put(nodesAtLevel.get(i), 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<>();
|
||
if (cluster.isEmpty()) return positions;
|
||
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.naturalOrder());
|
||
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<>();
|
||
if (cluster.isEmpty()) return positions;
|
||
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 ---
|
||
private Map<Integer, Point> calculateCircularManualPositions() {
|
||
List<DiagramIcon> icons = getIcons();
|
||
Map<Integer, Point> positions = new HashMap<>();
|
||
if (icons.isEmpty()) return positions;
|
||
// Group icons by their manual index.
|
||
Map<Integer, List<DiagramIcon>> groups = new TreeMap<>();
|
||
for (DiagramIcon icon : icons) {
|
||
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
|
||
}
|
||
int centerX = x + width / 2, centerY = y + height / 2;
|
||
int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet());
|
||
int maxRadius = (Math.min(width, height) - iconSize) / 2;
|
||
|
||
// Adjustable parameters:
|
||
// spacingExponent controls the non-linear spacing between rings. Increasing this value will push outer rings farther apart.
|
||
double spacingExponent = 1.5; // default value; adjust as needed
|
||
// ringMultiplier lets you directly scale the computed radius of each ring.
|
||
double ringMultiplier = 1.5; // default value; adjust as needed
|
||
|
||
// Calculate positions for each ring using a power function and multiplier.
|
||
for (Map.Entry<Integer, List<DiagramIcon>> entry : groups.entrySet()) {
|
||
int ring = entry.getKey();
|
||
List<DiagramIcon> groupIcons = entry.getValue();
|
||
if (groupIcons.isEmpty()) continue;
|
||
groupIcons.sort(Comparator.comparingInt(icon -> icon.priority));
|
||
double normalizedRing = (ring + 1) / (double) (maxRing + 1);
|
||
double radius = maxRadius * Math.pow(normalizedRing, spacingExponent) * ringMultiplier;
|
||
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, Point> positions = new HashMap<>();
|
||
if (icons.isEmpty()) return positions;
|
||
Map<Integer, List<DiagramIcon>> groups = new TreeMap<>();
|
||
for (DiagramIcon icon : icons) {
|
||
groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon);
|
||
}
|
||
int centerX = x + width / 2, centerY = y + height / 2;
|
||
int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet());
|
||
int halfSide = (Math.min(width, height) - iconSize) / 2;
|
||
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();
|
||
if (groupIcons.isEmpty()) continue;
|
||
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<>();
|
||
if (icons.isEmpty()) return positions;
|
||
int maxLevel = levels.isEmpty() ? 0 : Collections.max(levels.keySet());
|
||
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 m = levelIcons.size();
|
||
int posY = (m > 0 && maxLevel > 0) ? y + (int) ((height * level) / (maxLevel)) : y + height / 2;
|
||
for (int i = 0; i < m; i++) {
|
||
int posX = (m > 1) ? x + (int) ((width * i) / (m - 1.0)) : x + width / 2;
|
||
positions.put(levelIcons.get(i).id, new Point(posX, posY));
|
||
}
|
||
}
|
||
return positions;
|
||
}
|
||
|
||
// --- Curved Arrow Helpers ---
|
||
|
||
/**
|
||
* Computes a quadratic Bezier control point given endpoints and an angle (in degrees).
|
||
*/
|
||
private Point computeControlPoint(int x1, int y1, int x2, int y2, int angleDegrees) {
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
double angleRad = Math.toRadians(angleDegrees);
|
||
double offset = len * Math.tan(angleRad) / 2;
|
||
double perpX = -dy / len, perpY = dx / len;
|
||
return new Point((x1 + x2) / 2 + (int) (perpX * offset),
|
||
(y1 + y2) / 2 + (int) (perpY * offset));
|
||
}
|
||
|
||
/**
|
||
* Returns the minimum distance from the given control point to any icon (except endpoints).
|
||
*/
|
||
private double getMinDistanceToIcon(Point control, DiagramConnection conn) {
|
||
double minDist = Double.MAX_VALUE;
|
||
Map<Integer, Point> positions = calculatePositions();
|
||
for (DiagramIcon icon : getIcons()) {
|
||
if (icon.id == conn.idFrom || icon.id == conn.idTo) continue;
|
||
Point pos = positions.get(icon.id);
|
||
if (pos != null) {
|
||
double dist = pos.distance(control);
|
||
if (dist < minDist) {
|
||
minDist = dist;
|
||
}
|
||
}
|
||
}
|
||
return minDist;
|
||
}
|
||
|
||
// --- Arrow Drawing Methods ---
|
||
/**
|
||
* Draws a connection line between two endpoints.
|
||
* If a reverse connection exists and allowTwoWay is false, the line is drawn with a perpendicular offset.
|
||
*/
|
||
protected void drawConnectionLine(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) {
|
||
DiagramConnection reverse = getConnectionByIds(conn.idTo, conn.idFrom);
|
||
if (!allowTwoWay && reverse != null) {
|
||
if (x1 > x2 || (x1 == x2 && y1 > y2)) {
|
||
return;
|
||
}
|
||
}
|
||
boolean twoWay = (reverse != null && allowTwoWay);
|
||
// When allowTwoWay is false and a reverse exists, draw each connection separately.
|
||
boolean separateTwoWay = (reverse != null && !allowTwoWay);
|
||
if (!curvedArrows) {
|
||
if (separateTwoWay) {
|
||
int offsetAmount = 4;
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
double offsetX = -dy / len * offsetAmount;
|
||
double offsetY = dx / len * offsetAmount;
|
||
if (conn.idFrom >= conn.idTo) {
|
||
offsetX = -offsetX;
|
||
offsetY = -offsetY;
|
||
}
|
||
int newX1 = x1 + (int) offsetX;
|
||
int newY1 = y1 + (int) offsetY;
|
||
int newX2 = x2 + (int) offsetX;
|
||
int newY2 = y2 + (int) offsetY;
|
||
int color = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color = 0xFFFFFFFF;
|
||
}
|
||
drawColoredLine(newX1, newY1, newX2, newY2, color, dim);
|
||
} else if (twoWay) {
|
||
int midX = (x1 + x2) / 2;
|
||
int midY = (y1 + y2) / 2;
|
||
int color1 = getConnectionColor(reverse);
|
||
int color2 = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color1 = color2 = 0xFFFFFFFF;
|
||
}
|
||
drawColoredLine(x1, y1, midX, midY, color1, dim);
|
||
drawColoredLine(midX, midY, x2, y2, color2, dim);
|
||
} else {
|
||
int color = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color = 0xFFFFFFFF;
|
||
}
|
||
drawColoredLine(x1, y1, x2, y2, color, dim);
|
||
}
|
||
} else {
|
||
// Curved arrows
|
||
if (separateTwoWay) {
|
||
int offsetAmount = 4;
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
double normX = -dy / len;
|
||
double normY = dx / len;
|
||
double sign = (conn.idFrom < conn.idTo) ? 1 : -1;
|
||
Point baseControl = computeControlPoint(x1, y1, x2, y2, curveAngle);
|
||
Point offsetControl = new Point(baseControl.x + (int) (normX * offsetAmount * sign),
|
||
baseControl.y + (int) (normY * offsetAmount * sign));
|
||
int segments = 800;
|
||
int color = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color = 0xFFFFFFFF;
|
||
}
|
||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_HINT_BIT);
|
||
GL11.glEnable(GL11.GL_BLEND);
|
||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||
GL11.glEnable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
|
||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||
GL11.glLineWidth(2.0F);
|
||
setColor(color, dim);
|
||
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) * x1 + 2 * (1 - t) * t * offsetControl.x + t * t * x2);
|
||
int by = (int) ((1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * offsetControl.y + t * t * y2);
|
||
GL11.glVertex2i(bx, by);
|
||
}
|
||
GL11.glEnd();
|
||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||
GL11.glDisable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glDisable(GL11.GL_BLEND);
|
||
GL11.glPopAttrib();
|
||
} else if (twoWay) {
|
||
Point cp1 = computeControlPoint(x1, y1, x2, y2, curveAngle);
|
||
Point cp2 = computeControlPoint(x1, y1, x2, y2, -curveAngle);
|
||
double d1 = getMinDistanceToIcon(cp1, conn);
|
||
double d2 = getMinDistanceToIcon(cp2, conn);
|
||
Point control = (d1 >= d2) ? cp1 : cp2;
|
||
int segments = 800;
|
||
int color1 = getConnectionColor(reverse);
|
||
int color2 = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color1 = color2 = 0xFFFFFFFF;
|
||
}
|
||
int halfSegments = segments / 2;
|
||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_HINT_BIT);
|
||
GL11.glEnable(GL11.GL_BLEND);
|
||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||
GL11.glEnable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
|
||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||
GL11.glLineWidth(2.0F);
|
||
setColor(color1, dim);
|
||
GL11.glBegin(GL11.GL_LINE_STRIP);
|
||
for (int i = 0; i <= halfSegments; i++) {
|
||
double t = (double) i / halfSegments;
|
||
int bx = (int) ((1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * control.x + t * t * x2);
|
||
int by = (int) ((1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * control.y + t * t * y2);
|
||
GL11.glVertex2i(bx, by);
|
||
}
|
||
GL11.glEnd();
|
||
setColor(color2, dim);
|
||
GL11.glBegin(GL11.GL_LINE_STRIP);
|
||
for (int i = halfSegments; i <= segments; i++) {
|
||
double t = (double) i / segments;
|
||
int bx = (int) ((1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * control.x + t * t * x2);
|
||
int by = (int) ((1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * control.y + t * t * y2);
|
||
GL11.glVertex2i(bx, by);
|
||
}
|
||
GL11.glEnd();
|
||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||
GL11.glDisable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glDisable(GL11.GL_BLEND);
|
||
GL11.glPopAttrib();
|
||
} else {
|
||
int color = getConnectionColor(conn);
|
||
if (!useColorScaling) {
|
||
color = 0xFFFFFFFF;
|
||
}
|
||
Point cp = computeControlPoint(x1, y1, x2, y2, curveAngle);
|
||
int segments = 800;
|
||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_HINT_BIT);
|
||
GL11.glEnable(GL11.GL_BLEND);
|
||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||
GL11.glEnable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
|
||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||
GL11.glLineWidth(2.0F);
|
||
setColor(color, dim);
|
||
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) * x1 + 2 * (1 - t) * t * cp.x + t * t * x2);
|
||
int by = (int) ((1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * cp.y + t * t * y2);
|
||
GL11.glVertex2i(bx, by);
|
||
}
|
||
GL11.glEnd();
|
||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||
GL11.glDisable(GL11.GL_LINE_SMOOTH);
|
||
GL11.glDisable(GL11.GL_BLEND);
|
||
GL11.glPopAttrib();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Helper method to draw a straight colored line.
|
||
private void drawColoredLine(int x1, int y1, int x2, int y2, int color, boolean dim) {
|
||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
|
||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||
GL11.glLineWidth(2.0F);
|
||
setColor(color, dim);
|
||
GL11.glBegin(GL11.GL_LINES);
|
||
GL11.glVertex2i(x1, y1);
|
||
GL11.glVertex2i(x2, y2);
|
||
GL11.glEnd();
|
||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||
GL11.glPopAttrib();
|
||
}
|
||
|
||
// Helper to set color with dimming applied.
|
||
private void setColor(int color, boolean dim) {
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Draws the arrow head for a connection.
|
||
* When the connection is drawn separately (two-way with allowTwoWay false),
|
||
* the arrow head is positioned using the same offset.
|
||
*/
|
||
// Fixed drawArrowHead:
|
||
protected void drawArrowHead(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) {
|
||
DiagramConnection reverse = getConnectionByIds(conn.idFrom, conn.idTo);
|
||
if (!allowTwoWay && reverse != null) {
|
||
if (x1 > x2 || (x1 == x2 && y1 > y2)) {
|
||
return;
|
||
}
|
||
}
|
||
int color = (reverse != null) ? getConnectionColor(reverse) : getConnectionColor(conn);
|
||
if (!useColorScaling)
|
||
color = 0xFFFFFFFF;
|
||
boolean separateTwoWay = (reverse != null && !allowTwoWay);
|
||
double angle;
|
||
if (!curvedArrows) {
|
||
if (separateTwoWay) {
|
||
int offsetAmount = 4;
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
double offsetX = -dy / len * offsetAmount;
|
||
double offsetY = dx / len * offsetAmount;
|
||
if (conn.idFrom >= conn.idTo) {
|
||
offsetX = -offsetX;
|
||
offsetY = -offsetY;
|
||
}
|
||
int newX2 = x2 + (int) offsetX;
|
||
int newY2 = y2 + (int) offsetY;
|
||
angle = Math.atan2(newY2 - y1, newX2 - x1);
|
||
x2 = newX2;
|
||
y2 = newY2;
|
||
} else {
|
||
angle = Math.atan2(y2 - y1, x2 - x1);
|
||
}
|
||
} else {
|
||
if (separateTwoWay) {
|
||
int offsetAmount = 4;
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
double normX = -dy / len;
|
||
double normY = dx / len;
|
||
double sign = (conn.idFrom < conn.idTo) ? 1 : -1;
|
||
Point baseControl = computeControlPoint(x1, y1, x2, y2, curveAngle);
|
||
Point offsetControl = new Point(baseControl.x + (int) (normX * offsetAmount * sign),
|
||
baseControl.y + (int) (normY * offsetAmount * sign));
|
||
double t = 0.95;
|
||
double bx = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * offsetControl.x + t * t * x2;
|
||
double by = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * offsetControl.y + t * t * y2;
|
||
double dBx = 2 * (1 - t) * (offsetControl.x - x1) + 2 * t * (x2 - offsetControl.x);
|
||
double dBy = 2 * (1 - t) * (offsetControl.y - y1) + 2 * t * (y2 - offsetControl.y);
|
||
angle = Math.atan2(dBy, dBx);
|
||
} else {
|
||
double dx = x2 - x1, dy = y2 - y1;
|
||
double len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len == 0) len = 1;
|
||
Point cp1 = computeControlPoint(x1, y1, x2, y2, curveAngle);
|
||
Point cp2 = computeControlPoint(x1, y1, x2, y2, -curveAngle);
|
||
double d1 = getMinDistanceToIcon(cp1, conn);
|
||
double d2 = getMinDistanceToIcon(cp2, conn);
|
||
Point control = (d1 >= d2) ? cp1 : cp2;
|
||
double t = 0.95;
|
||
double bx = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * control.x + t * t * x2;
|
||
double by = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * control.y + t * t * y2;
|
||
double dBx = 2 * (1 - t) * (control.x - x1) + 2 * t * (x2 - control.x);
|
||
double dBy = 2 * (1 - t) * (control.y - y1) + 2 * t * (y2 - control.y);
|
||
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();
|
||
}
|
||
|
||
|
||
// --- 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, 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);
|
||
}
|
||
|
||
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.
|
||
* Applies pan/zoom, draws connections (using adjusted endpoints for separate two-way lines),
|
||
* icons, and shows tooltips.
|
||
*/
|
||
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<>();
|
||
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 (hoveredIconId == null) {
|
||
final double threshold = 5.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;
|
||
double dist;
|
||
// If drawing separate two-way connections, use the offset endpoints for hit detection.
|
||
boolean separateTwoWay = (getConnectionByIds(conn.idTo, conn.idFrom) != null && !allowTwoWay);
|
||
if (separateTwoWay) {
|
||
if (curvedArrows) {
|
||
int offsetAmount = 4;
|
||
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 normX = -dy / len;
|
||
double normY = dx / len;
|
||
double sign = (conn.idFrom < conn.idTo) ? 1 : -1;
|
||
Point baseControl = computeControlPoint(pFrom.x, pFrom.y, pTo.x, pTo.y, curveAngle);
|
||
Point offsetControl = new Point(baseControl.x + (int) (normX * offsetAmount * sign),
|
||
baseControl.y + (int) (normY * offsetAmount * sign));
|
||
dist = distanceToBezier(pFrom, offsetControl, pTo, 200, effectiveMouseX, effectiveMouseY);
|
||
} else {
|
||
int offsetAmount = 4;
|
||
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 offsetX = -dy / len * offsetAmount;
|
||
double offsetY = dx / len * offsetAmount;
|
||
if (conn.idFrom >= conn.idTo) {
|
||
offsetX = -offsetX;
|
||
offsetY = -offsetY;
|
||
}
|
||
int newX1 = pFrom.x + (int) offsetX;
|
||
int newY1 = pFrom.y + (int) offsetY;
|
||
int newX2 = pTo.x + (int) offsetX;
|
||
int newY2 = pTo.y + (int) offsetY;
|
||
dist = pointLineDistance(effectiveMouseX, effectiveMouseY, newX1, newY1, newX2, newY2);
|
||
}
|
||
} else {
|
||
if (curvedArrows) {
|
||
Point cp1 = computeControlPoint(pFrom.x, pFrom.y, pTo.x, pTo.y, curveAngle);
|
||
Point cp2 = computeControlPoint(pFrom.x, pFrom.y, pTo.x, pTo.y, -curveAngle);
|
||
double d1 = getMinDistanceToIcon(cp1, conn);
|
||
double d2 = getMinDistanceToIcon(cp2, conn);
|
||
Point control = (d1 >= d2) ? cp1 : cp2;
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
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);
|
||
}
|
||
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();
|
||
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);
|
||
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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
// --- 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 value = Math.max(-1f, Math.min(1f, conn.percent));
|
||
int r, g, b;
|
||
if (value >= 0f) {
|
||
float t = value;
|
||
r = (int)(144 + (0 - 144) * t);
|
||
g = (int)(238 + (100 - 238) * t);
|
||
b = (int)(144 + (0 - 144) * t);
|
||
} else {
|
||
float t = -value;
|
||
r = (int)(255 + (139 - 255) * t);
|
||
g = (int)(200 + (0 - 200) * t);
|
||
b = 0;
|
||
}
|
||
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);
|
||
}
|
||
|
||
// --- 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;
|
||
}
|
||
|
||
public void handleMouseScroll(int scrollDelta) {
|
||
zoom += scrollDelta * 0.0009f;
|
||
if (zoom < 0.5f) zoom = 0.5f;
|
||
if (zoom > 2.0f) zoom = 2.0f;
|
||
}
|
||
|
||
protected void drawHoveringText(List<String> textLines, int mouseX, int mouseY, FontRenderer font) {
|
||
}
|
||
|
||
// --- Flag Setters ---
|
||
public void setShowArrowHeads(boolean showArrowHeads) {
|
||
this.showArrowHeads = showArrowHeads;
|
||
}
|
||
|
||
public void setUseColorScaling(boolean useColorScaling) {
|
||
this.useColorScaling = useColorScaling;
|
||
}
|
||
|
||
// --- Icon and Connection Event Callbacks ---
|
||
protected void onIconClick(DiagramIcon icon) {
|
||
}
|
||
|
||
protected void onIconHeld(DiagramIcon icon) {
|
||
}
|
||
|
||
protected void onIconRelease(DiagramIcon icon) {
|
||
}
|
||
|
||
protected void onIconHover(DiagramIcon icon) {
|
||
}
|
||
|
||
protected void onConnectionHover(DiagramConnection conn) {
|
||
}
|
||
|
||
// --- Inner Helper Classes ---
|
||
public static class DiagramIcon {
|
||
public int id;
|
||
public boolean enabled = true;
|
||
public boolean pressable = false;
|
||
public int index = 0;
|
||
public int priority = 0;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
public enum IconRenderState {
|
||
DEFAULT,
|
||
HIGHLIGHTED,
|
||
NOT_HIGHLIGHTED;
|
||
}
|
||
}
|