attempted to use Graphoon, but it just crashes

This commit is contained in:
2025-11-04 20:45:37 -07:00
commit 5f3656c816
7 changed files with 527 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

1
src/lib/Graphoon.lua Normal file
View File

@@ -0,0 +1 @@
return require( (...) .. '.init' );

15
src/lib/Graphoon/Edge.lua Normal file
View File

@@ -0,0 +1,15 @@
local current = (...):gsub('%.[^%.]+$', '');
local Edge = {};
function Edge.new( id, origin, target )
local self = {};
self.id = id;
self.origin = origin;
self.target = target;
return self;
end
return Edge;

302
src/lib/Graphoon/Graph.lua Normal file
View File

@@ -0,0 +1,302 @@
local current = (...):gsub('%.[^%.]+$', '');
-- ------------------------------------------------
-- Required Modules
-- ------------------------------------------------
local Node = require(current .. '.Node');
local Edge = require(current .. '.Edge');
-- ------------------------------------------------
-- Module
-- ------------------------------------------------
local Graph = {};
function Graph.new()
local self = {};
local nodes = {}; -- Contains all nodes in the graph.
local edges = {}; -- Contains all edges in the graph.
local edgeIDs = 0; -- Used to create a unique ID for new edges.
local minX, maxX, minY, maxY; -- The boundaries of the graph.
-- ------------------------------------------------
-- Local Functions
-- ------------------------------------------------
---
-- (Re-)Sets the graph's boundaries to nil.
--
local function resetBoundaries()
minX, maxX, minY, maxY = nil, nil, nil, nil;
end
---
-- Updates the boundaries of the graph.
-- This represents the rectangular area in which all nodes are contained.
-- @param nx - The new x position to check.
-- @param ny - The new y position to check.
--
local function updateBoundaries( nx, ny )
return math.min( minX or nx, nx ), math.max( maxX or nx, nx ), math.min( minY or ny, ny ), math.max( maxY or ny, ny );
end
---
-- Adds a new edge between two nodes.
-- @param origin - The node from which the edge originates.
-- @param target - The node to which the edge is pointing to.
--
local function addEdge( origin, target )
for _, edge in pairs( edges ) do
if edge.origin == origin and edge.target == target then
error "Trying to connect nodes which are already connected.";
end
end
assert( origin ~= target, "Tried to connect a node with itself." );
edges[edgeIDs] = Edge.new( edgeIDs, origin, target );
edgeIDs = edgeIDs + 1;
end
-- ------------------------------------------------
-- Public Functions
-- ------------------------------------------------
---
-- Adds a node to the graph.
-- @param id - The ID will be used to reference the Node inside of the graph.
-- @param x - The x coordinate the Node should be spawned at (optional).
-- @param y - The y coordinate the Node should be spawned at (optional).
-- @param anchor - Wether the node should be locked in place or not (optional).
-- @param ... - Additional parameters (useful when a custom Node class is used).
--
function self:addNode( id, name, x, y, anchor, ... )
assert( not nodes[id], "Node IDs must be unique." );
nodes[id] = Node.new( id, name, x, y, anchor, ... );
return nodes[id];
end
---
-- Removes a node from the graph.
-- This will also remove all edges pointing to, or originating from this
-- node.
-- @param node - The node to remove from the graph.
--
function self:removeNode( node )
nodes[node:getID()] = nil;
self:removeEdges( node );
end
---
-- Creates a graph from a .tgf formatted file.
-- @param path (string) The path to the .tgf file to load.
-- @param x (number) The x coordinate the nodes should be spawned at (optional).
-- @param y (number) The y coordinate the nodes should be spawned at (optional).
--
function self:loadTGF( path, x, y )
local dx = x or 0;
local dy = y or 0;
local n = {};
local e = {};
local target = n;
for line in io.lines( path ) do
-- '#' marks the definitions for edges in the .tgf file.
if line == '#' then
target = e;
else
target[#target + 1] = line;
end
end
for _, line in ipairs( n ) do
local tmp = {}
for part in line:gmatch( '[^%s]+' ) do
tmp[#tmp + 1] = part;
end
-- Add a slight random variation to the spawn coordinates to kick start
-- the physics simulation.
local rx = love.math.random( 2, 5 );
local ry = love.math.random( 2, 5 );
self:addNode( tmp[1], tmp[2], dx + rx, dy + ry, tmp[1] == '1' );
end
for _, line in ipairs( e ) do
local tmp = {}
for part in line:gmatch( '[^%s]+' ) do
tmp[#tmp + 1] = part;
end
self:connectIDs( tmp[1], tmp[2] );
end
end
---
-- Adds a new edge between two nodes.
-- @param origin - The node from which the edge originates.
-- @param target - The node to which the edge is pointing to.
--
function self:connectNodes( origin, target )
addEdge( origin, target );
end
---
-- Adds a new edge between two nodes referenced by their IDs.
-- @param origin - The node id from which the edge originates.
-- @param target - The node id to which the edge is pointing to.
--
function self:connectIDs( originID, targetID )
assert( nodes[originID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", originID ));
assert( nodes[targetID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", targetID ));
addEdge( nodes[originID], nodes[targetID] );
end
---
-- Removes all edges leading to, or originating from a node.
-- @param node - The node to remove all edges from.
--
function self:removeEdges( node )
for id, edge in pairs( edges ) do
if edge.origin == node or edge.target == node then
edges[id] = nil;
end
end
end
---
-- Updates the graph.
-- @param dt - The delta time between frames.
-- @param nodeCallback - A callback called on every node (optional).
-- @param edgeCallback - A callback called on every edge (optional).
--
function self:update( dt, nodeCallback, edgeCallback )
for _, edge in pairs( edges ) do
edge.origin:attractTo( edge.target );
edge.target:attractTo( edge.origin );
if edgeCallback then
edgeCallback( edge );
end
end
resetBoundaries();
for _, nodeA in pairs( nodes ) do
if not nodeA:isAnchor() then
for _, nodeB in pairs( nodes ) do
if nodeA ~= nodeB then
nodeA:repelFrom( nodeB );
end
end
nodeA:move( dt );
end
if nodeCallback then
nodeCallback( nodeA );
end
minX, maxX, minY, maxY = updateBoundaries( nodeA:getPosition() );
end
end
---
-- Draws the graph.
-- Takes two callback functions as a parameter. These will be called
-- on each edge and node in the graph and will be used to wite a custom
-- drawing function.
-- @param nodeCallback - A callback called on every node.
-- @param edgeCallback - A callback called on every edge.
--
function self:draw( nodeCallback, edgeCallback )
for _, edge in pairs( edges ) do
if not edgeCallback then break end
edgeCallback( edge );
end
for _, node in pairs( nodes ) do
if not nodeCallback then break end
nodeCallback( node );
end
end
---
-- Checks if a certain Node ID already exists.
-- @param id - The id to check for.
--
function self:hasNode( id )
return nodes[id] ~= nil;
end
---
-- Returns the node the id is pointing to.
-- @param id - The id to check for.
--
function self:getNode( id )
return nodes[id];
end
---
-- Gets a node at a certain point in the graph.
-- @param x - The x coordinate to check.
-- @param y - The y coordinate to check.
-- @param range - The range in which to check around the given coordinates.
--
function self:getNodeAt(x, y, range)
for _, node in pairs( nodes ) do
local nx, ny = node:getPosition();
if x < nx + range and x > nx - range and y < ny + range and y > ny - range then
return node;
end
end
end
---
-- Returns the graph's minimum and maxmimum x and y values.
--
function self:getBoundaries()
return minX, maxX, minY, maxY;
end
---
-- Returns the x and y coordinates of the graph's center.
--
function self:getCenter()
return ( maxX - minX ) * 0.5 + minX, ( maxY - minY ) * 0.5 + minY;
end
---
-- Turn a node into an anchor.
-- Anchored nodes have fixed positions and can't be moved by the physical
-- forces.
-- @param id - The node's id.
-- @param x - The x coordinate to anchor the node to.
-- @param y - The y coordinate to anchor the node to.
--
function self:setAnchor( id, x, y )
nodes[id]:setPosition( x, y );
nodes[id]:setAnchor( true );
end
return self;
end
---
-- Replaces the default Edge class with a custom one.
-- @param class - The custom Edge class to use.
--
function Graph.setEdgeClass( class )
Edge = class;
end
---
-- Replaces the default Node class with a custom one.
-- @param class - The custom Node class to use.
--
function Graph.setNodeClass( class )
Node = class;
end
return Graph;

153
src/lib/Graphoon/Node.lua Normal file
View File

@@ -0,0 +1,153 @@
local current = (...):gsub('%.[^%.]+$', '');
local Node = {};
local FORCE_SPRING = 0.005;
local FORCE_CHARGE = 200;
local FORCE_MAX = 4;
local NODE_SPEED = 128;
local DAMPING_FACTOR = 0.95;
local DEFAULT_MASS = 3;
---
-- @param id - A unique id which will be used to reference this node.
-- @param x - The x coordinate the Node should be spawned at (optional).
-- @param y - The y coordinate the Node should be spawned at (optional).
-- @param anchor - Wether the node should be locked in place or not (optional).
--
function Node.new( id, name, x, y, anchor )
local self = {};
local px, py = x or 0, y or 0;
local ax, ay = 0, 0;
local vx, vy = 0, 0;
local mass = DEFAULT_MASS;
---
-- Clamps a value to a certain range.
-- @param min
-- @param val
-- @param max
--
local function clamp( min, val, max )
return math.max( min, math.min( val, max ) );
end
---
-- Calculates the new xy-acceleration for this node.
-- The values are clamped to keep the graph from "exploding".
-- @param fx - The force to apply in x-direction.
-- @param fy - The force to apply in y-direction.
--
local function applyForce( fx, fy )
ax = clamp( -FORCE_MAX, ax + fx, FORCE_MAX );
ay = clamp( -FORCE_MAX, ay + fy, FORCE_MAX );
end
---
-- Calculates the manhattan distance from the node's coordinates to the
-- target coordinates.
-- @param tx - The target coordinate in x-direction.
-- @param ty - The target coordinate in y-direction.
--
local function getManhattanDistance( tx, ty )
return px - tx, py - ty;
end
---
-- Calculates the actual distance vector between the node's current
-- coordinates and the target coordinates based on the manhattan distance.
-- @param dx - The horizontal distance.
-- @param dy - The vertical distance.
--
local function getRealDistance( dx, dy )
return math.sqrt( dx * dx + dy * dy ) + 0.1;
end
---
-- Attract this node to another node.
-- @param node - The node to use for force calculation.
--
function self:attractTo( node )
local dx, dy = getManhattanDistance( node:getPosition() );
local distance = getRealDistance( dx, dy );
dx = dx / distance;
dy = dy / distance;
local strength = -1 * FORCE_SPRING * distance * 0.5;
applyForce( dx * strength, dy * strength );
end
---
-- Repel this node from another node.
-- @param node - The node to use for force calculation.
--
function self:repelFrom( node )
local dx, dy = getManhattanDistance( node:getPosition() );
local distance = getRealDistance( dx, dy );
dx = dx / distance;
dy = dy / distance;
local strength = FORCE_CHARGE * (( mass * node:getMass() ) / ( distance * distance ));
applyForce(dx * strength, dy * strength);
end
---
-- Update the node's position based on the calculated velocity and
-- acceleration.
-- @param dt - The delta time between frames.
--
function self:move( dt )
vx = (vx + ax * dt * NODE_SPEED) * DAMPING_FACTOR;
vy = (vy + ay * dt * NODE_SPEED) * DAMPING_FACTOR;
px = px + vx;
py = py + vy;
ax, ay = 0, 0;
end
function self:getID()
return id;
end
function self:getX()
return px;
end
function self:getY()
return py;
end
function self:getPosition()
return px, py;
end
function self:setPosition( nx, ny )
px, py = nx, ny;
end
function self:setAnchor( nanchor )
anchor = nanchor;
end
function self:isAnchor()
return anchor;
end
function self:setMass( nmass )
mass = nmass;
end
function self:getMass()
return mass;
end
function self:getName()
return name;
end
return self;
end
return Node;

31
src/lib/Graphoon/init.lua Normal file
View File

@@ -0,0 +1,31 @@
return {
_VERSION = 'Graphoon v1.0.1',
_DESCRIPTION = 'A force directed graph algorithm written in Lua.',
_URL = 'https://github.com/rm-code/Graphoon',
_LICENSE = [[
Copyright (c) 2015 - 2016 Robert Machmer
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]],
Edge = require( (...):gsub('%.init$', '') .. '.Edge' ),
Graph = require( (...):gsub('%.init$', '') .. '.Graph' ),
Node = require( (...):gsub('%.init$', '') .. '.Node' )
};

24
src/main.lua Normal file
View File

@@ -0,0 +1,24 @@
local Graphoon = require "lib.Graphoon"
local graph = Graphoon.Graph.new()
-- it just crashes :D
graph:addNode("Name", 100, 100, true) -- magic numbers are a position and anchor to that position
graph:addNode("Another")
graph:connectIDs("Name", "Another")
function love.draw()
graph:draw(function(node)
local x, y = node:getPosition()
love.graphics.circle("fill", x, y, 5)
end,
function(edge)
local origin_x, origin_y = edge.origin:getPosition()
local target_x, target_y = edge.target:getPosition()
love.graphics.line(origin_x, origin_y, target_x, target_y)
end)
end
function love.update(dt)
graph:update(dt)
end