Fattorino / ImNodeFlow

Node based editor/blueprints for ImGui
MIT License
119 stars 15 forks source link

[Feature Request] Use std::function as ConnectionFilter rather than just using an enum #22

Closed eminor1988 closed 4 weeks ago

eminor1988 commented 2 months ago

Thanks for this amazing library. ImNodeFlow is very easy for me to use.

I have some ideas regarding the enum "ConnectionFilter". I need various types of nodes for complex pipeline design. My nodes might have nested relationships (parent/children). I hope the ConnectionFilter can be a std::function that determines whether the nodes are connectable or not (based on port type or parent type), rather than just using an enum.

Not sure if anyone else has any other ideas?

Fattorino commented 2 months ago

It's certainly an interesting concept, and I'd like to explore this further. Could you provide an example where a std::function filter would come into play?

eminor1988 commented 2 months ago

Sorry for my poor English skill, I'm not so familiar in ImNodeFlow. So below of code is just trying to explain the concepts(might be not so smart).

In ImNodeFlow.inl: Enum as filter (original):

template<class T>
void InPin<T>::createLink(Pin *other)
{
    ...

    if (!((m_filter & other->getFilter()) != 0 || m_filter == ConnectionFilter_None || other->getFilter() == ConnectionFilter_None)) // Check Filter
        return;

    ...
}

Using std::function as filer (new):

using ConnectionFilter = std::function<bool(OutPin* outPin, InPin* inPin)>;

template<class T>
void InPin<T>::createLink(Pin *other)
{
    ...

    ConnectionFilter currFilterFunc = m_filter;
    ConnectionFilter otherFilterFunc = other->getFilter();

    if (currFilterFunc) {
        if (!currFilterFunc(other, this)) {
            return;
        }
    }

    if (otherFilterFunc) {
        if (!otherFilterFunc(other, this)) {
            return;
        }
    }

    ...
}

That means we need to call the two ConnectionFilter functions for the pins between the link. It cannot be connected if ANY ConnectionFilter of the pin rejects it.

For those original enum values like ConnectionFilter_Integer and ConnectionFilter_Numbers, we can add some default ConnectionFilters written in std::function to check whether the "typeid(T) of InPin/OutPin" is the same for pins between the link.

Fattorino commented 2 months ago

Sorry, I meant to ask for a practical use case where such a filter could be useful. Knowing how it will be used will make implementation easier. Just a simple example of nodes and pins interaction to show the use of function filters

eminor1988 commented 2 months ago

I have a GPU sampling function node (perhaps written in GLSL, where the value is merely a cache in the graphics card), the OutPin\<Float32> of GPU should not be able to connect to InPin\<Float32> of a CPU node (because the value of OutPin\<Float32> is in cache of Graphic Card).

And I have others similar cases like it. Some nodes can be used in certain virtual device or context only. For download/upload data must to use certain node.

The virtual device/context can be CPU/GPU, different processes, remote computer, etc.

So I need to isolate the nodes by different scope/context except the certain upload/download nodes)

Fattorino commented 2 months ago

Ok, if you had function filters, ideally what would you check to validate a connection in your use case?

eminor1988 commented 2 months ago

I'm not a native English speaker, so sorry if my expression is not fluent.

Background: I am trying to re-design my old game engine. The main purpose is make it more programmable by user and make it can hot-reload when runtime. I've just started working on it; perhaps I haven't considered it thoroughly enough yet.

If I have function filters, I will check those condition in most of cases: The context(parent) of node must be the same with other pin. The data type of pins must be same or convertable(need to determine by other function).

But I carefully considered various scenarios that I might encounter next, there are some special cases...

Sometimes, I will allow "Circular reference" for time-based node that works in frame by frame: Allowing "Circular reference" can save many nodes and feel more intuitive in this kind of frame swapping cases. But in most of time I need to reject "Circular reference", so I will use an additional function to check it.

Sometimes, I need to reject the two cases because I want to limit the number of type C nodes because some soft/hard limit (maybe limit by hardware, EX: number of texture samplers in shader): A,B,C,D are four types of node. I need to reject them because user can use only 4 C nodes in context: 1: A -> B -> C -> D -> C -> D -> C -> D A -> B -> C -> D -> C -> D 2: A -> B -> C -> D -> C -> D A -> B -> C -> D -> C -> D B -> C -> D

So I hope constraint the connectability by certain limitation in different scenarios.

Thank you for your patient reading.

Fattorino commented 2 months ago

Ok ok, thanks for the detailed explanation

Fattorino commented 2 months ago

The concept seems to be working

https://github.com/Fattorino/ImNodeFlow/assets/90210751/66bf2575-889c-482d-80f1-e5db25ee1a46

class PickyDisplay : public BaseNode
{
public:
    explicit PickyDisplay()
    {
        setTitle("PickyDisplay");
        addIN<float>("Val", 0.f, [](Pin* out, Pin* in) { return out->getDataType() == in->getDataType() && dynamic_cast<ImFlow::OutPin<float>*>(out)->val() > 4.f; });
        setStyle(NodeStyle::red());
    }

    void draw() override
    {
        ImGui::Text("Value = %.2f;", getInVal<float>("Val"));
        if (getInVal<float>("Val") <= 4.f)
            inPin("Val")->deleteLink();
    }
private:
};
eminor1988 commented 2 months ago

I tried it and it works great! Thanks!!

That's how I check for circular references. (It hasn't been well tested, no copyright, welcome to use or modify.)

namespace ImFlowEx
{
    using namespace ImFlow;

    using ConnFilterFunc = std::function<bool(Pin*, Pin*)>;

    static auto IsOutNodeInPinsCircularRef(BaseNode* checkingNode, BaseNode* rejectNode) -> bool
    {
        if (rejectNode == checkingNode) {
            return true;
        }
        for (const std::shared_ptr<Pin>& inPin : checkingNode->getIns()) {
            auto link = inPin->getLink().lock();
            if (link) {
                Pin* outPin = link->left();
                if (outPin && IsOutNodeInPinsCircularRef(outPin->getParent(), rejectNode)) {
                    return true;
                }
            }
        }
        return false;
    }

    static auto IsInNodeOutPinsCircularRef(BaseNode* checkingNode, BaseNode* rejectNode) -> bool
    {
        if (rejectNode == checkingNode) {
            return true;
        }
        for (const std::shared_ptr<Pin>& outPin : checkingNode->getOuts()) {
            auto link = outPin->getLink().lock();
            if (link) {
                Pin* inPin = link->right();
                if (inPin && IsInNodeOutPinsCircularRef(inPin->getParent(), rejectNode)) {
                    return true;
                }
            }
        }
        return false;
    }

    static auto ConnFilter_NoCircularRef() -> ConnFilterFunc
    {
        return [](Pin* out, Pin* in) {
            return
                !IsOutNodeInPinsCircularRef(out->getParent(), in->getParent()) &&
                !IsInNodeOutPinsCircularRef(in->getParent(), out->getParent());
        };
    }

    static auto CascadeConnFilters(std::vector<ConnFilterFunc>&& connFilters) -> ConnFilterFunc
    {
        return [vec = std::move(connFilters)](Pin* out, Pin* in) {
            for (const ConnFilterFunc& connFilterFunc : vec) {
                if (!connFilterFunc(out, in)) {
                    return false;
                }
            }
            return true;
        };
    }

    static ConnFilterFunc connFilterSameTypeNoCircularRef = CascadeConnFilters({ ConnectionFilter::SameType(), ConnFilter_NoCircularRef() });
}

// Use it on the node.
class NodePipelineUpdate final : public ImFlow::BaseNode
{
public:
    NodePipelineUpdate()
    {
        setTitle("Pipeline - Update");
        addIN<bool>("Prev.", false, ImFlowEx::connFilterSameTypeNoCircularRef, ImFlow::PinStyle::red());
    }
};
Fattorino commented 4 weeks ago

Finished implementation with commit aaaab72