myspace / CommonLua /Ged /GedGraphEditor.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
21.5 kB
-- TODO:
-- 1. Add scrollbars to GraphEditorPanel
-- 2. Undo support
-- 3. Support multiple split points, serialize them
local SocketLinkOffset = point(10, 0)
local LinkMouseHoverDist = 7
local LinkThickness = 8
local SplineSampleStep = 10
local NodeSelectedColor = RGB(248, 220, 120)
local GraphSocketColors = {
number = RGB(205, 0, 205),
string = RGB(0, 150, 100),
boolean = RGB(0, 100, 0),
any = RGB(240, 240, 240),
}
DefineClass.XGraphEditor = {
__parents = { "XMap" },
drag_start_socket = false,
drag_end_socket = false,
drag_end_pt = false,
last_node_pt = false,
menu = false,
links = false,
read_only = false,
selected_node = false,
UseCustomTime = false,
NodeClassItems = false, -- set to a table with { EditorName = ..., Class = ... } items for each node class
OnGraphEdited = empty_func,
OnNodeSelected = empty_func,
}
function XGraphEditor:Init()
self.links = {}
end
function XGraphEditor:SelectNode(node)
if self.selected_node ~= node then
if self.selected_node then
self.selected_node:Select(false)
end
self.selected_node = node
if node then
node:Select(true)
end
end
self:OnNodeSelected(node)
end
function XGraphEditor:GetGraphData()
local data = { links = {} }
local node_to_idx = {}
for idx, node in ipairs(self) do
if IsKindOf(node, "XGraphNode") then
table.insert(data, { x = node.PosX, y = node.PosY, node_class = node.node_class, handle = node.handle })
node_to_idx[node] = idx
end
end
for _, link in ipairs(self.links) do
local start_sock = link.start_socket
local end_sock = link.end_socket
table.insert(data.links, {
start_node = node_to_idx[start_sock.parent_node],
start_socket = start_sock.socket_id,
end_node = node_to_idx[end_sock.parent_node],
end_socket = end_sock.socket_id,
})
end
return data
end
function XGraphEditor:SetGraphData(data)
self:DeleteChildren()
for _, data in ipairs(data) do
local node = XGraphNode:new({}, self)
node:SetNodeClass(data.node_class)
node:SetPos(data.x, data.y)
node.handle = data.handle
end
self.links = {}
for _, link in ipairs(data and data.links) do
local start_sock = table.find_value(self[link.start_node].sockets, "socket_id", link.start_socket)
local end_sock = table.find_value(self[link.end_node].sockets, "socket_id", link.end_socket)
if start_sock and end_sock then
self:AddLink(start_sock, end_sock)
end
end
end
function XGraphEditor:SetReadOnly(read_only)
self.read_only = read_only
self:CloseContextMenu()
self:Invalidate()
end
function XGraphEditor:DrawContent()
UIL.DrawSolidRect(box(0, 0, self.map_size:x(), self.map_size:y()), RGB(85, 85, 85))
local color1 = self.read_only and RGB(115, 115, 115) or RGB(175, 175, 175)
local color2 = self.read_only and RGB( 95, 95, 95) or RGB(125, 125, 125)
for x = 0, self.map_size:x(), 25 do
local thick = x % 125 == 0
UIL.DrawLineAntialised(thick and 7 or 3, point(x, 0), point(x, self.map_size:y()), thick and color2 or color1)
end
for y = 0, self.map_size:y(), 25 do
local thick = y % 125 == 0
UIL.DrawLineAntialised(thick and 7 or 3, point(0, y), point(self.map_size:x(), y), thick and color2 or color1)
end
end
local function get_link_spline(start_point, end_point)
local offset = start_point:x() < end_point:x() and SocketLinkOffset or -SocketLinkOffset
local spline_start = start_point + offset
local spline_end = end_point - offset
local the_x = spline_end:x() - spline_start:x()
local vector_end1 = point(spline_start:x() + the_x/4, spline_start:y())
local vector_end2 = point(spline_end:x() - the_x/4, spline_end:y())
return { spline_start, vector_end1, vector_end2, spline_end }
end
local function draw_link(start_point, end_point, spline, color)
UIL.DrawLineAntialised(LinkThickness, start_point, spline[1], color)
local spline_length = BS3_GetSplineLength2D(spline)
local last_pos = spline[1]
local i = SplineSampleStep
while i < spline_length do
local next_pos = point(BS3_GetSplinePos2D(spline, i, spline_length))
UIL.DrawLineAntialised(LinkThickness, last_pos, next_pos, color)
last_pos = next_pos
i = i + SplineSampleStep
end
UIL.DrawLineAntialised(LinkThickness, last_pos, spline[4], color)
UIL.DrawLineAntialised(LinkThickness, spline[4], end_point, color)
end
function XGraphEditor:DrawChildren(clip_box)
XMap.DrawChildren(self, clip_box)
if self.drag_end_pt and self.drag_start_socket then
local start_pos, end_pos = self.drag_start_socket.box:Center(), self.drag_end_pt
local spline = get_link_spline(start_pos, end_pos)
draw_link(start_pos, end_pos, spline, self.drag_start_socket.ImageColor)
end
for _, link in ipairs(self.links) do
link:DrawSpline(self.drag_end_pt)
end
end
function XGraphEditor:CloseContextMenu()
if self.menu then
self.menu:delete()
self.menu = false
end
end
function XGraphEditor:OnMousePos(pt, button)
self.drag_end_pt = self:ScreenToMapPt(pt)
self:Invalidate() -- allow connections to change color depending on mouse position
return XMap.OnMousePos(self, pt, button)
end
function XGraphEditor:OnMouseButtonDown(pt, button)
self:CloseContextMenu()
local pt1 = self:ScreenToMapPt(pt)
if not self.read_only and button == "L" then
if #self.links > 0 then
for _, spline in ipairs(self.links) do
if BS3_GetSplineToPointDist2D(spline.spline1, self.drag_end_pt) <= LinkMouseHoverDist and not spline.split_point then
local split = XGraphEditorSplineSplitPoint:new({ PosX = pt1:x(), PosY = pt1:y() }, self)
split:OnMouseButtonDown(pt, "L") -- start dragging the split point
spline.split_point = split
self:OnGraphEdited()
break
end
end
end
elseif not self.read_only and button == "R" then
-- if there is a link under the mouse, delete it
for idx, spline in ipairs(self.links) do
if BS3_GetSplineToPointDist2D(spline.spline1, self.drag_end_pt) <= LinkMouseHoverDist or
spline.split_point and BS3_GetSplineToPointDist2D(spline.spline2, self.drag_end_pt) <= LinkMouseHoverDist
then
spline:delete()
table.remove(self.links, idx)
self:OnGraphEdited()
return "break"
end
end
-- create context menu
self.menu = XPopupList:new({
Background = RGB(196, 196, 196),
}, terminal.desktop)
self.menu:SetAnchorType("mouse")
self.menu.Anchor = pt
for _, obj in ipairs(self.NodeClassItems) do
XTextButton:new({
OnPress = function()
local node = XGraphNode:new({}, self)
node:SetNodeClass(obj.Class)
node:SetPos(pt1:x(), pt1:y())
node.handle = #self == 1 and 1 or self[#self - 1].handle + 1
node.OnLayoutComplete = function()
local clip = false
for _, obj in ipairs(node.map) do
if obj ~= node and node.box:Intersect2D(obj.box) ~= const.irOutside then
clip = true
node:Move(node.box:Center(), obj.box:sizex(), 0, 100)
break
end
end
if not clip then
node.last_valid_pt = node:GetPos()
end
node.OnLayoutComplete = nil
end
self:CloseContextMenu()
self:OnGraphEdited()
end,
Text = obj.EditorName,
RolloverBackground = RGB(204, 232, 255),
PressedBackground = RGB(121, 189, 241),
}, self.menu)
end
return "break"
end
return XMap.OnMouseButtonDown(self, pt, button)
end
function XGraphEditor:AddLink(sock1, sock2)
if self:CanConnect(sock1, sock2) then
local link = XGraphLink:new{ start_socket = sock2, end_socket = sock1 }
table.insert(self.links, link)
return link
end
end
function XGraphEditor:RemoveLinks(sock)
for idx = #self.links, 1, -1 do
local spline = self.links[idx]
if spline.end_socket == sock or spline.start_socket == sock then
spline:delete()
table.remove(self.links, idx)
end
end
end
function XGraphEditor:OnLayoutComplete()
self:UpdateLinkSplines()
return XMap.OnLayoutComplete(self)
end
function XGraphEditor:UpdateLinkSplines(sock)
for _, spline in ipairs(self.links) do
if not sock or spline.end_socket == sock or spline.start_socket == sock then
spline:CalcSplinePoints()
end
end
end
function XGraphEditor:CanConnect(sock1, sock2)
local data1, data2 = sock1.link_data, sock2.link_data
return
sock1.parent_node ~= sock2.parent_node and
(data1.type == nil or data2.type == nil or data1.type == data2.type) and
(data1.input == nil or data2.input == nil or data1.input ~= data2.input)
end
function XGraphEditor:IsConnected(sock1, sock2)
for _, spline in ipairs(self.links) do
if spline.start_socket == sock2 and spline.end_socket == sock1 or spline.start_socket == sock1 and spline.end_socket == sock2 then
return true
end
end
end
DefineClass.XGraphNode = {
__parents = { "XMapObject" },
Background = RGB(196, 196, 196),
BorderColor = NodeSelectedColor,
LayoutMethod = "HList",
node_class = false,
handle = false, -- temporary handle sent from the game to Ged, used to recognize the object when sending the data back
sockets = false, -- array of XGraphSocket
last_valid_pt = false,
diff_x = false,
diff_y = false,
}
function XGraphNode:SetNodeClass(node_class)
self:DeleteChildren()
self.sockets = {}
self.node_class = node_class
local class = g_Classes[node_class]
local title = XText:new({
Dock = "top",
TextHAlign = "center",
TextStyle = "GedTitle",
UseClipBox = false,
Clip = false,
}, self)
title:SetText(class.EditorName)
local inputW = XWindow:new({
HAlign = "left",
LayoutMethod = "VList",
UseClipBox = false,
Margins = box(0, 0, 10, 0),
}, self)
local outputW = XWindow:new({
HAlign = "right",
LayoutMethod = "VList",
UseClipBox = false,
Margins = box(10, 0, 0, 0),
}, self)
-- add input sockets
for _, link_data in ipairs(class.GraphLinkSockets) do
local alignment = link_data.input and "right" or "left"
local socket_parent = link_data.input and inputW or outputW
local inputBlock = XWindow:new({
HAlign = link_data.input and "left" or "right",
UseClipBox = false,
}, socket_parent)
local sock = XGraphSocket:new({
HAlign = alignment,
parent_node = self,
link_data = link_data,
}, inputBlock)
local text = XText:new({
Dock = alignment,
TextHAlign = alignment,
TextStyle = "GedTitle",
UseClipBox = false,
Clip = false,
}, inputBlock)
text:SetText(link_data.name or link_data.id)
table.insert(self.sockets, sock)
end
end
function XGraphNode:Select(selected)
self:SetBorderWidth(selected and 2 or 0)
end
function XGraphNode:OnMouseButtonDown(pt, button)
self.map:CloseContextMenu()
if not self.map.read_only and button == "L" then
terminal.desktop:SetMouseCapture(self)
local pt1 = self.map:ScreenToMapPt(pt)
self.map.last_node_pt = self:GetPos()
self.last_valid_pt = self:GetPos()
self.diff_x = self.PosX - pt1:x()
self.diff_y = self.PosY - pt1:y()
self.map:SelectNode(self)
return "break"
elseif not self.map.read_only and button == "R" then
local map = self.map
map.menu = XPopupList:new({
Background = RGB(196, 196, 196),
}, terminal.desktop)
map.menu:SetAnchorType("mouse")
map.menu.Anchor = pt
XTextButton:new({
OnPress = function()
self:DeleteLinks()
self:delete()
map:CloseContextMenu()
map:OnGraphEdited()
end,
Text = "Delete Node",
RolloverBackground = RGB(204, 232, 255),
PressedBackground = RGB(121, 189, 241),
HAlign = "left",
}, self.map.menu)
return "break"
end
end
function XGraphNode:OnMousePos(pt, button)
if terminal.desktop:GetMouseCapture() == self then
if self:IsThreadRunning("ScrollThread") then
if self.map.box:Point2DInside(pt) then
self:DeleteThread("ScrollThread")
end
return "break"
end
local my_pt = self:GetPos()
local pt1 = self.map:ScreenToMapPt(pt)
local my_min = self.map:ScreenToMapPt(point(self.map.box:minx(),self.map.box:miny()))
local my_max = self.map:ScreenToMapPt(point(self.map.box:maxx(),self.map.box:maxy()))
if my_pt:x() > 0 + self.box:sizex()/2 or
my_pt:x() < self.map.box:sizex() - self.box:sizex()/2 or
my_pt:y() > 0 + self.box:sizey()/2 or
my_pt:y() < self.map.box:sizey() - self.box:sizex()/2
then
if pt1:x() < my_min:x() + self.box:sizex()/2 or
pt1:x() > my_max:x() - self.box:sizex()/2 or
pt1:y() < my_min:y() + self.box:sizey()/2 or
pt1:y() > my_max:y() - self.box:sizey()/2
then
self:CreateThread("ScrollThread", function()
while true do
self:Move(pt1, self.diff_x, self.diff_y, 100)
Sleep(100)
end
end)
else
self:Move(pt1, self.diff_x, self.diff_y, false)
end
end
local clip = false
for i, obj in ipairs(self.map) do
if obj ~= self and self.box:Intersect2D(obj.box) ~= const.irOutside then
clip = true
break
end
end
if not clip then
self.last_valid_pt = self:GetPos()
end
return "break"
end
end
function XGraphNode:OnMouseButtonUp(pt, button)
if terminal.desktop:GetMouseCapture() == self then
if self:IsThreadRunning("ScrollThread") then
self:DeleteThread("ScrollThread")
else
self:SetPos(self.last_valid_pt:x(), self.last_valid_pt:y())
end
terminal.desktop:SetMouseCapture(false)
self.map:OnGraphEdited()
return "break"
end
end
function XGraphNode:Move(mouse_pt, dx, dy, scroll)
local my_min = self.map:ScreenToMapPt(point(self.map.box:minx(), self.map.box:miny()))
local my_max = self.map:ScreenToMapPt(point(self.map.box:maxx(), self.map.box:maxy()))
self:SetPos(
Clamp(mouse_pt:x() + dx, self.box:sizex()/2, self.map.map_size:x() - self.box:sizex()/2),
Clamp(mouse_pt:y() + dy, self.box:sizey()/2, self.map.map_size:y() - self.box:sizey()/2),
scroll)
if scroll then
local xd = self.PosX - self.map.last_node_pt:x()
local yd = self.PosY - self.map.last_node_pt:y()
xd = MulDivRound(xd, 1000, self.map.scale:x())
yd = MulDivRound(yd, 1000, self.map.scale:y())
self.map:ScrollMap(-xd, -yd, scroll)
end
if mouse_pt:x() > self.map.map_size:x() or mouse_pt:y() > self.map.map_size:y() then
if mouse_pt:x() > self.map.map_size:x() then
self.map.map_size = point(self.map.map_size:x() + 25, self.map.map_size:y())
end
if mouse_pt:y() > self.map.map_size:y() then
self.map.map_size = point(self.map.map_size:x(), self.map.map_size:y() + 25)
end
end
self.map.last_node_pt = self:GetPos()
for _, sock in ipairs(self.sockets) do
self.map:UpdateLinkSplines(sock)
end
end
function XGraphNode:DeleteLinks()
for _, sock in ipairs(self.sockets) do
self.map:RemoveLinks(sock)
end
end
DefineClass.XGraphSocket = {
__parents = { "XImage" },
UseClipBox = false,
Image = "CommonAssets/UI/Ged/socket_empty",
ImageScale = point(500, 500),
HandleMouse = true,
link_data = false, -- the data for this socket from the node's GraphLinkSockets table
socket_id = false, -- set to link_data.id
parent_node = false, -- my parent node
connections = 0, -- how many links to this socket
}
function XGraphSocket:Init()
self.socket_id = self.link_data.id
self.ImageColor = GraphSocketColors[self.link_data.type] or GraphSocketColors.any
end
function XGraphSocket:AddConnection()
self.connections = self.connections + 1
self:SetImage("CommonAssets/UI/Ged/socket_full")
end
function XGraphSocket:RemoveConnection()
self.connections = self.connections - 1
if self.connections == 0 then
self:SetImage("CommonAssets/UI/Ged/socket_empty")
end
end
function XGraphSocket:OnMouseButtonDown(pt, button)
local map = GetParentOfKind(self, "XMap")
if not map.read_only and button == "L" then
map.drag_start_socket = self
terminal.desktop:SetMouseCapture(self)
return "break"
elseif not map.read_only and button == "R" then
map:RemoveLinks(self)
map:OnGraphEdited()
return "break"
end
end
function XGraphSocket:OnMousePos(pt, button)
if terminal.desktop:GetMouseCapture() == self then
self:SetImage("CommonAssets/UI/Ged/socket_full")
local map = GetParentOfKind(self, "XMap")
local target = map:GetMouseTarget(pt)
if IsKindOf(target, "XGraphSocket") and map:CanConnect(self, target) and target.connections == 0 then
if map.drag_end_socket and map.drag_end_socket ~= target then
map.drag_end_socket:SetImage("CommonAssets/UI/Ged/socket_empty")
end
target:SetImage("CommonAssets/UI/Ged/socket_full")
map.drag_end_socket = target
elseif map.drag_end_socket then
map.drag_end_socket:SetImage("CommonAssets/UI/Ged/socket_empty")
map.drag_end_socket = false
end
map.drag_end_pt = map:ScreenToMapPt(pt)
map:Invalidate()
return "break"
end
end
function XGraphSocket:OnMouseButtonUp(pt, button)
if terminal.desktop:GetMouseCapture() == self then
terminal.desktop:SetMouseCapture(false)
local map = GetParentOfKind(self, "XMap")
local target = map:GetMouseTarget(pt)
if IsKindOf(target, "XGraphSocket") and map:CanConnect(target, self) then
map:AddLink(self, target):CalcSplinePoints()
elseif self.connections == 0 then
self:SetImage("CommonAssets/UI/Ged/socket_empty")
end
map.drag_end_socket = false
map.drag_start_socket = false
map:Invalidate()
map:OnGraphEdited()
return "break"
end
end
function XGraphSocket:OnSetRollover(inside)
if inside or self.connections > 0 then
self:SetImage("CommonAssets/UI/Ged/socket_full")
else
self:SetImage("CommonAssets/UI/Ged/socket_empty")
end
end
DefineClass.XGraphLink = {
__parents = { "InitDone" },
spline1 = false, -- the spline before the split point
spline2 = false, -- the spline after the split point
start_socket = false, -- a reference to the input socket in the connection
end_socket = false, -- a reference to the output socket in the connection
split_point = false,
}
function XGraphLink:Init()
self.start_socket:AddConnection()
self.end_socket:AddConnection()
end
function XGraphLink:Done()
self.start_socket:RemoveConnection()
self.end_socket:RemoveConnection()
if spline.split_point then
spline.split_point:delete()
end
end
function XGraphLink:CalcSplinePoints()
local start_point = self.start_socket.box:Center()
local end_point = self.end_socket.box:Center()
if self.split_point then
local midpoint = self.split_point.box:Center()
self.spline1 = get_link_spline(start_point, midpoint - SocketLinkOffset)
self.spline2 = get_link_spline(midpoint + SocketLinkOffset, end_point)
else
self.spline1 = get_link_spline(start_point, end_point)
end
end
function XGraphLink:PointInside(mouse_pt)
if self.split_point then
return (BS3_GetSplineToPointDist2D(self.spline1, mouse_pt) <= LinkMouseHoverDist or
BS3_GetSplineToPointDist2D(self.spline2, mouse_pt) <= LinkMouseHoverDist)
else
return BS3_GetSplineToPointDist2D(self.spline1, mouse_pt) <= LinkMouseHoverDist
end
end
function XGraphLink:DrawSpline(mouse_pt)
local color = GraphSocketColors[self.start_socket.link_data.type or self.end_socket.link_data.type or "any"]
if mouse_pt and self:PointInside(mouse_pt) and not terminal.desktop:GetMouseCapture() then
color = InterpolateRGB(color, const.clrWhite, 1, 3) -- highlighted color
end
local start_point = self.start_socket.box:Center()
local end_point = self.end_socket.box:Center()
if not self.split_point then
draw_link(start_point, end_point, self.spline1, color)
else
local split_point = self.split_point.box:Center()
draw_link(start_point, split_point, self.spline1, color)
draw_link(split_point, end_point, self.spline2, color)
end
end
DefineClass.XGraphEditorSplineSplitPoint = {
__parents = { "XMapObject" },
HandleMouse = true,
MinWidth = 10,
MaxWidth = 10,
MinHeight = 10,
MaxHeight = 10,
}
function XGraphEditorSplineSplitPoint:OnMouseButtonDown(pt, button)
if not self.map.read_only and button == "L" then
terminal.desktop:SetMouseCapture(self)
return "break"
end
end
function XGraphEditorSplineSplitPoint:OnMousePos(pt, button)
if terminal.desktop:GetMouseCapture() == self then
local pt1 = self.map:ScreenToMapPt(pt)
self:SetPos(pt1:x(), pt1:y())
self.map:Invalidate()
return "break"
end
end
function XGraphEditorSplineSplitPoint:OnMouseButtonUp(pt, button)
if terminal.desktop:GetMouseCapture() == self then
local pt1 = self.map:ScreenToMapPt(pt)
self:SetPos(pt1:x(), pt1:y())
terminal.desktop:SetMouseCapture(false)
self.map:OnGraphEdited()
return "break"
end
end
-- Classes for testing
DefineClass("TestGraphNode", "PropertyObject")
DefineClass.GraphNodeMath = {
__parents = { "TestGraphNode" },
properties = {
{ id = "Operation", editor = "text", default = "" },
},
GraphLinkSockets = {
{ id = "input1", name = "A", input = true, type = "number", },
{ id = "input2", name = "B", input = true, type = "number", },
{ id = "output", name = "Output", input = false, type = "number", },
},
EditorName = "Math Node",
}
DefineClass.GraphNodeBooleanCheck = {
__parents = { "TestGraphNode" },
GraphLinkSockets = {
{ id = "input1", name = "InfoCheck", input = true, },
{ id = "input2", name = "InfoGot", input = true, },
{ id = "output1", name = "false", input = false, type = "boolean", },
{ id = "output2", name = "true", input = false, type = "boolean", },
},
EditorName = "Boolean Node",
}
DefineClass.GraphNodeString = {
__parents = { "TestGraphNode" },
GraphLinkSockets = {
{ id = "input1", name = "word", input = true, type = "string", },
{ id = "input2", name = "another word", input = true, type = "string", },
{ id = "input3", name = "even more word", input = true, type = "string", },
{ id = "input4", name = "many a word", input = true, type = "string", },
{ id = "output", name = "Sentence", input = false, type = "string", },
},
EditorName = "String Node",
}