Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 251 additions & 2 deletions TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ public enum ConnectionMode
{
Previous,
Next,
Else
Else,
// Dynamic input/output slots start at index 100 to avoid conflicts
// Actual slot index is (mode - InputBase) or (mode - OutputBase)
InputBase = 100,
OutputBase = 200
}

public partial class NodeEditor : UserControl
Expand Down Expand Up @@ -59,6 +63,10 @@ public List<TriggerNode> Nodes
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ConnectionMode HotNodeMode { get; set; } = ConnectionMode.Previous;

[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int HotNodeSlotIndex { get; set; } = -1; // For input/output slot index

[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool LockNodeChanges { get; set; } = false;
Expand Down Expand Up @@ -145,6 +153,11 @@ public List<TriggerNode> Nodes
public bool LinksAsRopes { get; set; } = false;
public bool ShowGrips { get; set; } = false;

// Current event mode for node validation
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string CurrentEventMode { get; set; } = string.Empty;

private const float _mouseWheelScrollFactor = 0.04f;

private const float _hotNodeTransparency = 0.6f;
Expand Down Expand Up @@ -301,6 +314,28 @@ public void AddNode(bool linkToPrevious, bool linkToElse, bool condition)
LayoutVisibleNodes();
}

/// <summary>
/// Validates if a node is compatible with the current event mode.
/// Returns true if node is valid for the current event mode, or if no restrictions apply.
/// </summary>
public bool ValidateNodeEventMode(TriggerNode node, out string warningMessage)
{
warningMessage = string.Empty;

// If no event mode is set or node has no restrictions, allow it
if (string.IsNullOrEmpty(CurrentEventMode) || node.AllowedEventModes.Count == 0)
return true;

// Check if the node explicitly allows this event mode
if (!node.AllowedEventModes.Contains(CurrentEventMode))
{
warningMessage = $"Node '{node.Name}' is not explicitly designed for '{CurrentEventMode}' events. Proceed with caution.";
return false;
}

return true;
}

public void AddConditionNode(bool linkToPrevious, bool linkToElse)
{
AddNode(linkToPrevious, linkToElse, true);
Expand Down Expand Up @@ -368,6 +403,80 @@ public void LinkSelectedNodes()
Invalidate();
}

/// <summary>
/// Links an input variable of the target node to an output variable of the source node.
/// This creates a data flow connection between nodes beyond the standard Next/Previous flow.
/// </summary>
public bool LinkInputToOutput(TriggerNode sourceNode, string outputName, TriggerNode targetNode, string inputName)
{
if (sourceNode == null || targetNode == null)
return false;

// Check if the output exists on the source node
var output = sourceNode.Outputs.FirstOrDefault(o => o.Name == outputName);
if (output == null)
return false;

// Find or create the input on the target node
var input = targetNode.Inputs.FirstOrDefault(i => i.Name == inputName);
if (input == null)
{
input = new InputVariable { Name = inputName };
targetNode.Inputs.Add(input);
}

// Link the input to the output using unique node ID
input.LinkedOutputNodeId = sourceNode.Id;
input.LinkedOutputName = outputName;

return true;
}

/// <summary>
/// Unlinks an input variable from its connected output.
/// </summary>
public void UnlinkInput(TriggerNode node, string inputName)
{
if (node == null)
return;

var input = node.Inputs.FirstOrDefault(i => i.Name == inputName);
if (input != null)
{
input.LinkedOutputNodeId = Guid.Empty;
input.LinkedOutputName = string.Empty;
}
}

/// <summary>
/// Gets the effective value for an input - either from a linked output or from user-defined arguments.
/// </summary>
public string GetEffectiveInputValue(TriggerNode node, string inputName)
{
if (node == null)
return string.Empty;

var input = node.Inputs.FirstOrDefault(i => i.Name == inputName);
if (input != null && input.IsLinked)
{
// Try to find the linked source node by unique ID
var sourceNode = Nodes.FirstOrDefault(n => n.Id == input.LinkedOutputNodeId);
if (sourceNode != null)
{
// In a real implementation, this would evaluate the output value
return $"[Linked from {sourceNode.Name}.{input.LinkedOutputName}]";
}
}

// Fall back to user-defined argument
if (node.DynamicArguments.UserDefinedArguments.TryGetValue(inputName, out string value))
{
return value;
}

return string.Empty;
}

public void MoveSelectedNodes(TriggerNode rootNode, Vector2 delta)
{
if (SelectedNodes.Count <= 1)
Expand Down Expand Up @@ -931,6 +1040,138 @@ private void DrawHeader(PaintEventArgs e, VisibleNodeBase node)
}
}

private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node)
{
if (!node.Visible)
return;

using (var brush = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.3f)))
{
// Draw INPUT labels at TOP, positioned ABOVE the node (opposite of then/else which are below)
if (node.Node.Inputs.Count > 0)
{
for (int i = 0; i < node.Node.Inputs.Count; i++)
{
var input = node.Node.Inputs[i];
var size = TextRenderer.MeasureText(input.Name, Font);

// Get the grip position for this input
var inputMode = (ConnectionMode)((int)ConnectionMode.InputBase + i);
var inputPoint = node.GetNodeScreenPosition(inputMode);

// Position label ABOVE the node top (grips are at y=0, so labels go above)
int rectX = (int)inputPoint[0].X;
int rectWidth = (int)(inputPoint[1].X - inputPoint[0].X);
int rectY = node.Location.Y - (int)(size.Height * 1.2f);

var rect = new Rectangle(rectX, rectY, rectWidth, size.Height);

// Draw shadow
e.Graphics.DrawImage(Properties.Resources.misc_Shadow,
new Rectangle((int)((inputPoint[0].X + inputPoint[1].X) / 2) - size.Width / 2, rectY, size.Width, size.Height));

// Draw label with different color if linked
var labelBrush = input.IsLinked
? new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.6f))
: brush;

e.Graphics.DrawString(input.Name, Font, labelBrush, rect,
new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center });

if (input.IsLinked)
labelBrush.Dispose();
}
}

// Draw OUTPUT labels at BOTTOM, positioned like then/else labels
if (node.Node.Outputs.Count > 0)
{
for (int i = 0; i < node.Node.Outputs.Count; i++)
{
var output = node.Node.Outputs[i];
var size = TextRenderer.MeasureText(output.Name, Font);

// Get the grip position for this output
var outputMode = (ConnectionMode)((int)ConnectionMode.OutputBase + i);
var outputPoint = node.GetNodeScreenPosition(outputMode);

// Position label below the grip, like then/else
int rectX = (int)outputPoint[0].X;
int rectWidth = (int)(outputPoint[1].X - outputPoint[0].X);
int rectY = node.Location.Y + node.Height + (int)(size.Height * 0.4f);

var rect = new Rectangle(rectX, rectY, rectWidth, size.Height);

// Draw shadow
e.Graphics.DrawImage(Properties.Resources.misc_Shadow,
new Rectangle((int)((outputPoint[0].X + outputPoint[1].X) / 2) - size.Width / 2, rectY, size.Width, size.Height));

// Draw label
e.Graphics.DrawString(output.Name, Font, brush, rect,
new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center });
}
}
}
}

private void DrawInputOutputLinks(PaintEventArgs e, List<VisibleNodeBase> nodes, VisibleNodeBase node)
{
if (!node.Visible)
return;

// Draw links from this node's inputs to source outputs
for (int i = 0; i < node.Node.Inputs.Count; i++)
{
var input = node.Node.Inputs[i];
if (!input.IsLinked)
continue;

// Find the source node
var sourceNode = nodes.FirstOrDefault(n => n.Node.Id == input.LinkedOutputNodeId);
if (sourceNode == null)
continue;

// Find the output index
int outputIndex = sourceNode.Node.Outputs.FindIndex(o => o.Name == input.LinkedOutputName);
if (outputIndex < 0)
continue;

// Get connection points
var p1 = GetInputOutputPosition(sourceNode, outputIndex, false); // output
var p2 = GetInputOutputPosition(node, i, true); // input

// Draw link with node color
DrawLink(e, node.Node.Color, _connectedNodeTransparency, p1, p2);
}
}

private PointF[] GetInputOutputPosition(VisibleNodeBase node, int slotIndex, bool isInput)
{
int nodeWidth = node.Width;
int slotCount = isInput ? node.Node.Inputs.Count : node.Node.Outputs.Count;
int spacing = nodeWidth / (slotCount + 1);
int xCenter = node.Location.X + spacing * (slotIndex + 1);

int yPos;
if (isInput)
{
// Input at TOP
yPos = node.Location.Y;
}
else
{
// Output at BOTTOM
yPos = node.Location.Y + node.Height;
}

int gripHalfWidth = 30; // Width of connection point
return new PointF[]
{
new PointF(xCenter - gripHalfWidth, yPos),
new PointF(xCenter + gripHalfWidth, yPos)
};
}

private void DrawVisibleNodeLink(PaintEventArgs e, List<VisibleNodeBase> nodes, VisibleNodeBase node)
{
for (int i = 0; i < 2; i++)
Expand Down Expand Up @@ -1124,10 +1365,14 @@ protected override void OnPaint(PaintEventArgs e)
// Draw node links antialiased
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

// Draw connected nodes
// Draw connected nodes (Previous/Next/Else)
foreach (var n in nodeList)
DrawVisibleNodeLink(e, nodeList, n);

// Draw input/output links
foreach (var n in nodeList)
DrawInputOutputLinks(e, nodeList, n);

// Draw hot node
DrawHotNode(e, nodeList);

Expand All @@ -1151,6 +1396,10 @@ protected override void OnPaint(PaintEventArgs e)
// Draw labels (after everything else)
foreach (var n in nodeList)
DrawHeader(e, n);

// Draw input/output labels
foreach (var n in nodeList)
DrawInputOutputLabels(e, n);
}
}

Expand Down
Loading