OverviewFilesCommitsIssuesDocsReleasesPeers

Attila772-hypergodot / main.gd

533 lines · gd ref:
extends Node2D


var edges : Dictionary
var nodes : Dictionary
var node_to_edges = {}  # node_id -> [edge_instances]
class NodeData:
	var id = 0 # Node identifier
	var edges = {} # Array to hold edge identifiers (based on line number or custom ID)
	
func create_nodes(nodes: Dictionary):
	var scene = load("res://HyperNode.tscn")
	var row = 1
	var column = 1
	var spacing = Vector2(100, 100)
	var per_row = 10
	
	for id in nodes.keys():
		var node_instance = scene.instantiate()
		node_instance.node_id = id
		node_instance.name = str(id)
		node_instance.position = Vector2(column * spacing.x, row * spacing.y)
		node_instance._set_node_id(str(id))
		
		# Set the edges property for the node
		node_instance.edges = nodes[id].edges
		
		# Connect the position change signal
		node_instance.connect("node_position_changed", _on_node_position_changed)
	
		# Add the node instance to the scene
		add_child(node_instance)
		
		# Increment column and row for positioning nodes
		column += 1
		if column >= per_row:
			column = 0
			row += 1

func _on_node_position_changed(node):
	# Only mark edges that are connected to this node as dirty
	if node_to_edges.has(node.node_id):
		for edge_instance in node_to_edges[node.node_id]:
			if edge_instance and edge_instance.has_method("mark_dirty"):
				edge_instance.mark_dirty()

func populate_edge_data(file_data):
	var hyperedge_scene = load("res://HyperEdge.tscn")
	var nodes = file_data["nodes"]  # Retrieve nodes from the file data
	var edges = file_data["edges"]  # Retrieve edges from the file data

	var group_names = []
	for edge_id in edges:
		var group_name = edges[edge_id]["group"]
		if group_name not in group_names:
			group_names.append(group_name)
	Global.initialize_groups(group_names)

	# Step 1: Build a per-node mapping of edge IDs to radii
	var node_edge_radius_map = {}  # Dictionary mapping node_id to {edge_id: radius}
	for node_id in nodes:
		var node_edges = nodes[node_id].edges
		var circle_ref = get_node(str(node_id))  # Get node instance with 'offset_circles'
		var offset_circles = circle_ref.offset_circles  # List of radii for offset circles

		var edge_ids = node_edges.keys()
		var edge_radius_map = {}

		# Ensure there are enough radii for each edge
		if offset_circles.size() < edge_ids.size():
			print("Error: Node %s has more edges (%s) than offset circles provided." % [node_id, edge_ids.size()])
			continue  # Skip this node if radii are insufficient

		# Map each edge_id to its corresponding radius for this node
		for i in range(edge_ids.size()):
			var edge_id = edge_ids[i]
			var radius = offset_circles[i]
			edge_radius_map[edge_id] = radius

		node_edge_radius_map[node_id] = edge_radius_map

	# Step 2: Build edges data using node_edge_radius_map
	for edge_id in edges:
		var edge_nodes = edges[edge_id]["nodes"]  # Nodes connected by this edge
		var edge_radii = []
		var edge_node_ids = []

		for node_id in edge_nodes:
			if node_edge_radius_map.has(node_id):
				var edge_radius_map = node_edge_radius_map[node_id]
				if edge_radius_map.has(edge_id):
					var radius = edge_radius_map[edge_id]
					edge_node_ids.append(node_id)
					edge_radii.append(radius)
				else:
					print("Error: Edge radius not found for node %s and edge %s" % [node_id, edge_id])
			else:
				print("Error: Node %s not found in node_edge_radius_map" % node_id)

		# Update edges[edge_id] with aligned nodes and radii
		edges[edge_id]["nodes"] = edge_node_ids
		edges[edge_id]["radii"] = edge_radii

	# Step 3: Instantiate hyperedges and visualize them
	for edge_id in edges:
		var hyperedge_instance = hyperedge_scene.instantiate()
		var edge_data = edges[edge_id]
		var node_radii_pairs = {}

		# Ensure nodes and radii arrays are aligned
		if edge_data["nodes"].size() != edge_data["radii"].size():
			print("Error: Mismatch between nodes and radii sizes for edge", edge_id)
			continue  # Skip this edge if there's a mismatch

		# Map each node to its radius
		for i in range(edge_data["nodes"].size()):
			var nodeid = edge_data["nodes"][i]
			var radius = edge_data["radii"][i]
			node_radii_pairs[nodeid] = radius

		# Assign the nodes and radii to the hyperedge instance
		hyperedge_instance.nodes = node_radii_pairs

		# Assign a color based on the group
		var color = Color(randf(), randf(), randf(), 0.4)  # Generate a random color
		hyperedge_instance.color = color

		# Set the group name to the hyperedge instance for future use
		hyperedge_instance.group = edge_data["group"]

		hyperedge_instance.add_to_group("edges")
		hyperedge_instance.name = str(edge_id)
		var config = ConfigFile.new()
		var expression = Expression.new()
		var err = config.load("res://conf.cfg")
		var expression_text = config.get_value("graph_settings", "edge_width_expression")
	
		var error = expression.parse(expression_text, ["support", "node_count", "total_nodes"])

		if error != OK:
			$AcceptDialog.window_title = "Error"
			$AcceptDialog.dialog_text = "The expression cannot be parsed: " + error
			$AcceptDialog.get_ok().text = "Close"
			$AcceptDialog.popup_centered()
		# Set the width of the hyperedge based on the support value
		var result = expression.execute([edges[edge_id]["support"], edge_data["nodes"].size(), nodes.size()] )
		if result == null: 
			$AcceptDialog.title = "Error"
			$AcceptDialog.dialog_text = "The expression cannot be parsed:  Error" + str(error)
			$AcceptDialog.ok_button_text = "Close"
			$AcceptDialog.popup_centered()
			return 1.0
		hyperedge_instance.width = result

		# Add the hyperedge instance to the scene tree
		add_child(hyperedge_instance)
		
		# Build node-to-edges mapping
		for node_id in edge_data["nodes"]:
			if not node_to_edges.has(node_id):
				node_to_edges[node_id] = []
			node_to_edges[node_id].append(hyperedge_instance)
	
	Global.edges = edges



func _ready():
	# Read the file and get both nodes and edges
	var file_data = read_file("patterns.txt")
	# Extract nodes and edges from the file data
	var nodes = file_data["nodes"]
	var edges = file_data["edges"]
	create_nodes(nodes)
	populate_edge_data(file_data)

	# Load layout configuration from conf.cfg
	var config = ConfigFile.new()
	var err = config.load("res://conf.cfg")

	if err != OK:
		print("Error loading conf.cfg: ", err)
		return

	var layout = config.get_value("graph_settings", "layout", "")
	var parameters = config.get_value("graph_settings", "parameters", [])

	match layout:
		"force-directed":
			if parameters.size() == 3:
				apply_force_directed_layout(parameters[0], parameters[1], parameters[2])
				print("Applied force-directed layout with parameters: ", parameters)
			else:
				print("Invalid parameters for force-directed layout: ", parameters)
		"circular":
			if parameters.size() == 1:
				apply_circular_layout(parameters[0])
				print("Applied circular layout with parameter: ", parameters[0])
			else:
				print("Invalid parameters for circular layout: ", parameters)
		"radial":
			if parameters.size() == 1:
				apply_radial_layout(parameters[0],file_data)
				print("Applied radial layout with radius step: ", parameters[0])
			else:
				print("Invalid parameters for radial layout: ", parameters)
		_:
			print("Unknown layout type: ", layout)

	calculate_centrality_and_resize_nodes(file_data)
	#apply_hyperedge_constrained_layout()
	#apply_circular_layout(500)
	pass # Replace with function body.



func read_file(path):
	var file = FileAccess.open(path, FileAccess.READ)
	var nodes = {} # Dictionary to hold all nodes
	var edges = {} # Dictionary to hold all edges
	var edge_id = 0 # Unique identifier for each edge
	
	while not file.eof_reached():
		var line = file.get_line().strip_edges()
		if line.find("#WEIGHT:") == -1:
			continue # Skip this line if no support value
		
		var parts = line.split(" #WEIGHT: ")
		var node_ids = parts[0].split(" ") # Nodes involved in this edge
		
		# Extract support and group
		var supp_group_info = parts[1].split(" #GROUP ")
		var support = int(supp_group_info[0])
		var group = supp_group_info[1]
		
		# Process each node ID in the line
		for node_id in node_ids:
			if not nodes.has(node_id):
				nodes[node_id] = NodeData.new() # Assuming NodeData is a class for node info
				
			# Add this edge to the node's edge list
			nodes[node_id].edges[edge_id] = support
		
		# Store edge information with group and support in edges dictionary
		edges[edge_id] = {
			"nodes": node_ids,
			"support": support,
			"radii" : [],
			"group": group
		}
		
		edge_id += 1 # Increment edge ID for the next line

	file.close()

	# Return both nodes and edges in a single dictionary
	return {"nodes": nodes, "edges": edges}



# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass
	
	
func apply_hyperedge_constrained_layout():
	var node_positions = {} # Dictionary to store node positions temporarily
	var edge_bounds = {} # Dictionary to store edge bounds (min and max positions)
	
	# Initialize node positions
	for node in get_children():
		if node.is_in_group("nodes"): # Assuming your nodes are added to a "nodes" group
			node_positions[node.node_id] = node.position
	
	# Calculate edge bounds based on nodes in the edge
	for edge_id in edges:
		var edge_nodes = edges[edge_id]["nodes"]
		var min_x = INF
		var min_y = INF
		var max_x = -INF
		var max_y = -INF
		
		for node_id in edge_nodes:
			var pos = node_positions[node_id]
			min_x = min(min_x, pos.x)
			min_y = min(min_y, pos.y)
			max_x = max(max_x, pos.x)
			max_y = max(max_y, pos.y)
		
		edge_bounds[edge_id] = Rect2(Vector2(min_x, min_y), Vector2(max_x - min_x, max_y - min_y))
	
	# Reposition nodes to ensure they are outside of edges they don't belong to
	for node_id in node_positions.keys():
		var node_pos = node_positions[node_id]
		for edge_id in edges:
			var edge_nodes = edges[edge_id]["nodes"]
			if node_id not in edge_nodes:
				var bounds = edge_bounds[edge_id]
				if bounds.has_point(node_pos):
					# Move the node outside the edge bounds
					var direction = (node_pos - bounds.position).normalized()
					node_positions[node_id] = bounds.position + bounds.size * direction
	
	# Update actual node positions
	for node in get_children():
		if node.is_in_group("nodes"):
			node.position = node_positions[node.node_id]
	
	
func apply_force_directed_layout(iterations: int, repulsion_force: float, spring_length: float):
	var node_positions = {} # Szótár a csomópontok pozícióinak ideiglenes tárolására
	var forces = {}         # Szótár a csomópontokra ható erők tárolására
	
	var all_nodes = []
	for child in get_children():
		if child.is_in_group("nodes"):
			all_nodes.append(child)

	if all_nodes.is_empty():
		print("Nincsenek csomópontok az elrendezéshez.")
		return

	# Kezdeti pozíciók és erők inicializálása
	for node in all_nodes:
		node_positions[node.node_id] = node.position
		forces[node.node_id] = Vector2.ZERO

	# Iteratív erőszimuláció
	for i in range(iterations):
		# --- 1. Vonzóerők számítása (ÚJ, SÚLYPONT-ALAPÚ LOGIKA) ---
		var edge_centers = {} # Szótár a hiperélek középpontjainak tárolására

		# Először minden hiperél középpontját kiszámoljuk
		for edge_id in edges:
			var edge_data = edges[edge_id]
			var center_pos = Vector2.ZERO
			var node_count_in_edge = edge_data["nodes"].size()
			
			if node_count_in_edge > 0:
				for node_id in edge_data["nodes"]:
					if node_positions.has(node_id):
						center_pos += node_positions[node_id]
				center_pos /= node_count_in_edge
				edge_centers[edge_id] = center_pos

		# Most alkalmazzuk a vonzóerőt a csomópontokra a középpontok felé
		for edge_id in edges:
			var edge_data = edges[edge_id]
			if not edge_centers.has(edge_id):
				continue

			var center = edge_centers[edge_id]
			for node_id in edge_data["nodes"]:
				if forces.has(node_id):
					var delta = center - node_positions[node_id]
					var distance = delta.length()
					
					# A Hooke-törvényhez hasonló rugóerő
					# A spring_length most a csomópont és a hiperél-középpont ideális távolsága
					var displacement = distance - spring_length
					var spring_force_magnitude = displacement * 0.1 # A 0.1 egy "rugóállandó", finomhangolható
					
					if distance > 0:
						forces[node_id] += delta.normalized() * spring_force_magnitude

		# --- 2. Taszítóerők számítása (VÁLTOZATLAN LOGIKA) ---
		for j in range(all_nodes.size()):
			for k in range(j + 1, all_nodes.size()):
				var node1_id = all_nodes[j].node_id
				var node2_id = all_nodes[k].node_id

				var delta = node_positions[node1_id] - node_positions[node2_id]
				var distance = delta.length()
				
				if distance > 0: # Elkerüljük a nullával való osztást
					# Az erő a távolság négyzetével fordítottan arányos a realisztikusabb hatásért
					var repulsive_force_magnitude = repulsion_force / (distance * distance)
					var force_vector = delta.normalized() * repulsive_force_magnitude
					
					forces[node1_id] += force_vector
					forces[node2_id] -= force_vector # Egyenlő és ellentétes erő

		# --- 3. Erők alkalmazása és pozíciók frissítése ---
		var damping = 0.9 # Csillapítás, hogy a rendszer stabilizálódjon és ne "robbanjon szét"
		for node_id in node_positions.keys():
			node_positions[node_id] += forces[node_id] * damping
			forces[node_id] = Vector2.ZERO # Erők nullázása a következő iterációhoz

	# Végleges pozíciók beállítása a szimuláció után
	for node in all_nodes:
		node.position = node_positions[node.node_id]
			
			
			
			

func apply_circular_layout(radius: float):
	var center = get_viewport_rect().size / 2
	var nodes = [] # List to store nodes
	for child in get_children():
		if child.is_in_group("nodes"):
			nodes.append(child)
	var node_count = nodes.size()
	var angle_step = 2 * PI / node_count
	for i in range(node_count):
		var node = nodes[i] # Get the node from the filtered list
		var angle = i * angle_step
		var x = center.x + radius * cos(angle)
		var y = center.y + radius * sin(angle)
		node.position = Vector2(x, y)
		
		
func calculate_centrality_and_resize_nodes(file_data):
	var nodes = file_data["nodes"]
	var max_centrality = 0
	var centrality_scores = {}
	Global.total_nodes = nodes.size()  # Store total node count in a global variable

	# Calculate centrality (degree centrality in this case)
	for node_id in nodes:
		var degree = nodes[node_id].edges.size()
		centrality_scores[node_id] = degree
		max_centrality = max(max_centrality, degree)

	# Resize nodes based on centrality
	for node_id in nodes:
		var node_instance = get_node(str(node_id))
		if node_instance:
			var normalized_centrality = float(centrality_scores[node_id]) / max_centrality
			node_instance.update_size(normalized_centrality, normalized_centrality)


func apply_radial_layout(radius_step: float, file_data):
	# A csomópontokat egy egyszerű listában gyűjtjük, a circular_layout mintájára.
	var all_node_instances = []
	for child in get_children():
		if child.is_in_group("nodes"):
			all_node_instances.append(child)

	if all_node_instances.is_empty():
		print("Hiba: Nincsenek csomópontok a radiális elrendezéshez.")
		return

	# A gráf szerkezetének beolvasása a fájlból a megbízhatóság érdekében.

	var nodes_data = file_data["nodes"]
	var edges_data = file_data["edges"]
	print(nodes_data)
	if nodes_data.is_empty() or edges_data.is_empty():
		print("Hiba: A fájlból beolvasott gráf adatok üresek.")
		return

	# --- 1. A legcentrálisabb csomópont megkeresése ---
	var central_node_id = ""
	var max_degree = -1
	for node_id in nodes_data:
		var degree = nodes_data[node_id].edges.size()
		if degree > max_degree:
			max_degree = degree
			central_node_id = node_id

	if central_node_id == "":
		print("Nem található központi csomópont.")
		return

	# --- 2. Szélességi bejárás (BFS) a távolságokhoz ---
	var levels = {} # node_id -> távolság a centrumtól
	var queue = [[central_node_id, 0]]
	var visited = {central_node_id: true}
	levels[central_node_id] = 0

	var adjacency_list = {}
	for edge_data in edges_data.values():
		var edge_node_list = edge_data["nodes"]
		for i in range(edge_node_list.size()):
			var node_id = edge_node_list[i]
			if not adjacency_list.has(node_id):
				adjacency_list[node_id] = []
			for j in range(edge_node_list.size()):
				if i != j:
					var other_node_id = edge_node_list[j]
					if other_node_id not in adjacency_list[node_id]:
						adjacency_list[node_id].append(other_node_id)
	
	var head = 0
	while head < queue.size():
		var current_pair = queue[head]
		head += 1
		var current_id = current_pair[0]
		var current_level = current_pair[1]

		if adjacency_list.has(current_id):
			for neighbor_id in adjacency_list[current_id]:
				if not visited.has(neighbor_id):
					visited[neighbor_id] = true
					levels[neighbor_id] = current_level + 1
					queue.append([neighbor_id, current_level + 1])

	# Csomópont objektumok csoportosítása szintenként
	var nodes_by_level = {}
	for node_instance in all_node_instances:
		var level = levels.get(node_instance.node_id, -1)
		if not nodes_by_level.has(level):
			nodes_by_level[level] = []
		nodes_by_level[level].append(node_instance)

	# --- 3. Pozíciók kiosztása ---
	var center = get_viewport_rect().size / 2
	
	# A központi csomópont középre kerül
	if nodes_by_level.has(0) and not nodes_by_level[0].is_empty():
		nodes_by_level[0][0].position = center

	# A többi csomópont a körökre
	for level in nodes_by_level:
		if level <= 0:
			continue
			
		var nodes_on_this_level = nodes_by_level[level]
		var node_count = nodes_on_this_level.size()
		if node_count == 0:
			continue
			
		var radius = level * radius_step
		var angle_step = (2 * PI) / node_count

		for i in range(node_count):
			var node = nodes_on_this_level[i] # Itt már közvetlenül a node objektumot használjuk
			var angle = i * angle_step
			var x = center.x + radius * cos(angle)
			var y = center.y + radius * sin(angle)
			node.position = Vector2(x, y)