Seneral / Node_Editor_Framework

A flexible and modular Node Editor Framework for creating node based displays and editors in Unity
https://nodeeditor.seneral.dev
MIT License
2k stars 415 forks source link

Making children of node drag along with parent #200

Closed TimOgden closed 2 years ago

TimOgden commented 2 years ago

Hey, I'm trying to modify the code so that when I move one node, any children of that node are dragged by the same amount.

I created an extension class to NodeEditorCallbackReceiver and overrided the OnMoveNode(Node node) method. However, my code isn't working, and the Debug print statement I put in that function never does anything, so I know the method is not being called. I also noticed that the NodeEditorCallbacks class has a static Action<Node> for each of the node callbacks. I tried setting this value in OnEnable() in my class, but still, no effect.

What am I missing here?

using System;
using UnityEngine;
using NodeEditorFramework;
using NodeEditorFramework.Standard;

namespace NodeEditorFramework
{
    public class DragChildrenCallbackReceiver : NodeEditorCallbackReceiver
    {

        void OnEnable()
        {
            NodeEditorCallbacks.OnMoveNode = DragNode;
        }

        public override void OnMoveNode(Node node)
        {
            DragNode(node);
        }

        public void DragNode(Node node)
        {
            Debug.Log("Running OnMoveNode callback");
            var n = node as BaseBTNode;
            if (n != null)
            {
                for (int i = 0; i < n.children.Length; i++)
                {
                    n.children[i].position = n.position + n.children[i].offset;
                }
                n.parent.offset = n.parent.position - n.position;
            }
        }
    }
}
Seneral commented 2 years ago

Its called here: https://github.com/Seneral/Node_Editor_Framework/blob/develop/Node_Editor_Framework/Runtime/Framework/Interface/NodeEditorInputControls.cs#L173 so it should call it once after the drag ended. It does not currently call it every frame, nor does it call it when the keyboard is used (that is a bug). Note that the Receiver is only working as an instantiated MonoBehaviour due to implementation, I assume you don't have ExecuteInEditMode or are in play mode so the code will not execute. You will have to get a static script to work in edit mode and use the Actions, not sure how the details were for that in Unity. Alternatively, you can take a look at what the built-in NodeGroup does. It implements custom controls that override the default drag in certain scenarios and also moves multiple nodes, that might be something to refer to. The static functions tagged with the proper attributes will be called automatically if you set the priorities right, but you might have to copy and modify the existing drag function which is more complex

TimOgden commented 2 years ago

@Seneral Thanks so much for such a quick and informative reply. I realized early on that I didn't have much of a use for the NodeGroups, so I didn't do much research into them, and turns out the NodeGroup class was a perfect example to show me what to do. I spent today writing my own methods, structured similar to those in NodeGroup and it's working perfectly now.

dragNodes

I used recursion since I don't just want a node's children to be dragged along, I want the whole subtree. Here's the code I wrote inside of my BaseBTNode class:

public void SetNodeOffset(Node node)
{
    this.offset = this.position - node.position;
    for (int i = 0; i < children.Length; i++)
    {
        children[i].SetNodeOffset(node);
    }
}

public void UpdatePosition(Node node)
{
    //Debug.Log(offset);
    this.position = node.position + offset;
    for (int i = 0; i < children.Length; i++)
    {
        if (children[i] != null)
            children[i].UpdatePosition(node);
    }
}

[EventHandlerAttribute(EventType.MouseDown, 109)]
private static void HandleNodeStartDrag(NodeEditorInputInfo inputInfo)
{
    //Debug.Log("Running OnMoveNode callback");
    if (GUIUtility.hotControl > 0)
        return; // GUI has control

    NodeEditorState state = inputInfo.editorState;
    if(inputInfo.inputEvent.button == 0 && !state.dragNode)
    {
        var n = NodeEditor.NodeAtPosition(NodeEditor.ScreenToCanvasSpace(inputInfo.inputPos)) as BaseBTNode;
        if(n!=null)
        {
            for(int i = 0; i<n.children.Length; i++)
            {
                n.children[i].SetNodeOffset(n);
            }
            state.StartDrag("node", inputInfo.inputPos, n.position);
            state.dragNode = true;
        }
    }
}

[EventHandlerAttribute(EventType.MouseDrag)]
private static void HandleNodeDragging(NodeEditorInputInfo inputInfo)
{
    NodeEditorState state = inputInfo.editorState;
    var n = state.selectedNode as BaseBTNode;

    if(n!=null)
    {
        if(state.dragUserID != "node")
        {
            state.selectedNode = null;
            state.dragNode = false;
            return;
        }
        //Update drag operation
        Vector2 dragChange = state.UpdateDrag("node", inputInfo.inputPos);
        Vector2 newPos = state.dragObjectPos;

        // Update positions
        n.position = newPos;
        for(int i = 0; i<n.children.Length; i++)
        {
            n.children[i].UpdatePosition(n);
        }

        inputInfo.inputEvent.Use();
        NodeEditor.RepaintClients();
    }
}

[EventHandlerAttribute(EventType.MouseUp)]
private static void HandleDraggingEnd(NodeEditorInputInfo inputInfo)
{
    if(inputInfo.editorState.dragUserID == "node")
    {
        inputInfo.editorState.EndDrag("node");
        NodeEditor.RepaintClients();
    }

    inputInfo.editorState.selectedNode = null;
    inputInfo.editorState.dragNode = false;
}
Seneral commented 2 years ago

Looking good, just one thing, I'd expect ypu need to change at least the drag id from "node" to something custom so the default mouseDrag events won't interfere. Right now it seems it will come down to chance which one will get called first, at least from a quick glance. Having a different drag id means the builtin function will not consider it as a drag it needs to handle.

Also make sure all your relevant canvas types are disallowing loops (via property) if you are relying on recursion like this, especially if you intend to have other users use the system.

TimOgden commented 2 years ago

Yeah, good point, I was wondering if that would cause an issue. In testing so far, it never seemed like the default mouseDrag events ever activated, I think perhaps because I set the priority of the EventHandlerAttribute to a lesser value, but it definitely would be good to change it to something unique.

And yeah, I never disallowed recursive loops, I just knew I wasn't going to be using them. How should I do that? Maybe in ValidateSelf() in my custom Canvas class, if I detect any recursive loops I could delete one of the nodes?

TimOgden commented 2 years ago

Nevermind, Canvas has an allowRecursion property, that's what you meant, haha. Thanks again