Nodes: Support for input/output sockets in same vertical space #112250

Merged
Lukas Tönne merged 25 commits from LukasTonne/blender:node-inline-sockets into main 2023-09-14 16:08:11 +02:00
Member

Declarations can use the add_input_output method to create a combined input/output socket. The drawing code supports moving sockets up one vertical slot to align them with the predecessor.

Closes #112235

Declarations can use the `add_input_output` method to create a combined input/output socket. The drawing code supports moving sockets up one vertical slot to align them with the predecessor. Closes #112235
Lukas Tönne added 5 commits 2023-09-11 19:05:15 +02:00
Author
Member

Currently this will fail on the socket validation function (correctly!) because the inlined input socket appears before output sockets, which is considered invalid. Either have to relax this condition or add a special flag to exempt these sockets.

Currently this will fail on the socket validation function (correctly!) because the inlined input socket appears before output sockets, which is considered invalid. Either have to relax this condition or add a special flag to exempt these sockets.
Lukas Tönne added this to the Nodes & Physics project 2023-09-11 19:07:01 +02:00
Hans Goudey added this to the 4.0 milestone 2023-09-11 19:13:38 +02:00
Hans Goudey requested changes 2023-09-11 20:06:39 +02:00
Hans Goudey left a comment
Member

Do you think eventually node declarations could be refactored to be stored more like the interface tree items, so that code like this wouldn't be necessary? I'm fine with doing that later, but it seems like this leaves the code in an unnecessarily complicated state.

    if (decl_in_) {
      decl_in_->description = std::move(value);
    }
    if (decl_out_) {
      decl_out_->description = std::move(value);
    }
Do you think eventually node declarations could be refactored to be stored more like the interface tree items, so that code like this wouldn't be necessary? I'm fine with doing that later, but it seems like this leaves the code in an unnecessarily complicated state. ``` if (decl_in_) { decl_in_->description = std::move(value); } if (decl_out_) { decl_out_->description = std::move(value); } ```
@ -474,1 +501,4 @@
struct NodeInterfacePanelData {
const nodes::PanelDeclaration *decl_;
bNodePanelState *state_;
Member

Since this is drawing code, it would be nice if these were const pointers. Same below.

Since this is drawing code, it would be nice if these were const pointers. Same below.
Author
Member

I've added some comments to this struct. state could potentially be made const, but currently draw code also updates the NODE_PANEL_PARENT_COLLAPSED flag in there (hiding the panel and its content). This could be done in a separate pass, but that would just duplicate the drawing code loop.

The runtime data stores transient draw info like the location, so it needs to be mutable.

I've added some comments to this struct. `state` could potentially be made const, but currently draw code also updates the `NODE_PANEL_PARENT_COLLAPSED` flag in there (hiding the panel and its content). This could be done in a separate pass, but that would just duplicate the drawing code loop. The `runtime` data stores transient draw info like the location, so it needs to be mutable.
Member

Okay-- I'd like to move that runtime location storage to SpaceNode runtime, but while we still reorder nodes based on selection that's too tricky.

Okay-- I'd like to move that runtime location storage to `SpaceNode` runtime, but while we still reorder nodes based on selection that's too tricky.
LukasTonne marked this conversation as resolved
@ -475,0 +511,4 @@
};
struct NodeInterfaceSocketData {
const nodes::SocketDeclaration *decl_;
Member

These are public members, which shouldn't have the _ suffix. Same above.

These are public members, which shouldn't have the `_` suffix. Same above.
LukasTonne marked this conversation as resolved
@ -475,0 +517,4 @@
operator bool() const
{
/* One socket side is enough to be valid. */
Member

It's not clear what "valid" means here

It's not clear what "valid" means here
LukasTonne marked this conversation as resolved
@ -500,2 +646,2 @@
* the stack. Each panel expects a number of items to be added, after which the panel is removed
* from the stack again. */
* new #PanelUpdate is added to the stack. Items in the declaration are added to the top panel
* of the stack. Each panel expects a number of items to be added, after which the panel is
Member

Unnecessary formatting change?

Unnecessary formatting change?
Author
Member

Bah, clang-format keeps doing this, and it's usually in a multiline comment where it's not immediately obvious ...

Bah, clang-format keeps doing this, and it's usually in a multiline comment where it's not immediately obvious ...
Author
Member

make format insists on this, guess it was wrong before 🤷

`make format` insists on this, guess it was wrong before 🤷
LukasTonne marked this conversation as resolved
@ -247,3 +248,3 @@
class BaseSocketDeclarationBuilder {
protected:
int index_ = -1;
int index_in_ = -1;
Member

Seems important to add comments here

Seems important to add comments here
LukasTonne marked this conversation as resolved
Author
Member

Do you think eventually node declarations could be refactored to be stored more like the interface tree items, so that code like this wouldn't be necessary?

That might be nice, but it could also just become more complicated.

The declarations code sits between two different systems:

  1. The semantic system of declarations and node group interfaces, where there is a true hierarchy of items and a conceptual "pass-through" socket.
  2. The actual node instances where sockets are simple flat lists on nodes. Panels, pass-throughs, and the like are just UI layout features.

I want to have a single builder instance returned by add_input_output to make declarations intuitive, but it has to generate 2 basic sockets. The node declarations themselves also have to match the sockets 1:1 because of how they are used in various places as supplementary info for sockets. So now 1 builder has to keep track of 2 declarations.

> Do you think eventually node declarations could be refactored to be stored more like the interface tree items, so that code like this wouldn't be necessary? That might be nice, but it could also just become more complicated. The declarations code sits between two different systems: 1. The semantic system of declarations and node group interfaces, where there is a true hierarchy of items and a conceptual "pass-through" socket. 2. The actual node instances where sockets are simple flat lists on nodes. Panels, pass-throughs, and the like are just UI layout features. I want to have a single builder instance returned by `add_input_output` to make declarations intuitive, but it has to generate 2 basic sockets. The node declarations themselves also have to match the sockets 1:1 because of how they are used in various places as supplementary info for sockets. So now 1 builder has to keep track of 2 declarations.

Hi Lukas, could you please check what am I doing wrong? Because on my initial tests this is not working for me in two ways:

  • The inlined sockets are still drawn one above the other.
  • The label of the inlined output should be hidden.

image

diff --git a/source/blender/nodes/composite/nodes/node_composite_exposure.cc b/source/blender/nodes/composite/nodes/node_composite_exposure.cc
index 7abad178f2f..4a2786bac63 100644
--- a/source/blender/nodes/composite/nodes/node_composite_exposure.cc
+++ b/source/blender/nodes/composite/nodes/node_composite_exposure.cc
@@ -18,11 +18,10 @@ namespace blender::nodes::node_composite_exposure_cc {
 
 static void cmp_node_exposure_declare(NodeDeclarationBuilder &b)
 {
-  b.add_input<decl::Color>("Image")
+  b.add_input_output<decl::Color>("Image")
       .default_value({1.0f, 1.0f, 1.0f, 1.0f})
       .compositor_domain_priority(0);
   b.add_input<decl::Float>("Exposure").min(-10.0f).max(10.0f).compositor_domain_priority(1);
-  b.add_output<decl::Color>("Image");
 }
Hi Lukas, could you please check what am I doing wrong? Because on my initial tests this is not working for me in two ways: * The inlined sockets are still drawn one above the other. * The label of the inlined output should be hidden. ![image](/attachments/64b2de1f-aa09-4235-a5f8-fe85af0d9c8e) ``` diff --git a/source/blender/nodes/composite/nodes/node_composite_exposure.cc b/source/blender/nodes/composite/nodes/node_composite_exposure.cc index 7abad178f2f..4a2786bac63 100644 --- a/source/blender/nodes/composite/nodes/node_composite_exposure.cc +++ b/source/blender/nodes/composite/nodes/node_composite_exposure.cc @@ -18,11 +18,10 @@ namespace blender::nodes::node_composite_exposure_cc { static void cmp_node_exposure_declare(NodeDeclarationBuilder &b) { - b.add_input<decl::Color>("Image") + b.add_input_output<decl::Color>("Image") .default_value({1.0f, 1.0f, 1.0f, 1.0f}) .compositor_domain_priority(0); b.add_input<decl::Float>("Exposure").min(-10.0f).max(10.0f).compositor_domain_priority(1); - b.add_output<decl::Color>("Image"); } ```
Author
Member

@dfelinto You are just missing a line to enable the custom layout features:

static void cmp_node_exposure_declare(NodeDeclarationBuilder &b)
{
  b.use_custom_socket_order();
  ...
@dfelinto You are just missing a line to enable the custom layout features: ``` static void cmp_node_exposure_declare(NodeDeclarationBuilder &b) { b.use_custom_socket_order(); ... ```
Lukas Tönne added 2 commits 2023-09-12 11:36:01 +02:00
Lukas Tönne added 1 commit 2023-09-12 12:01:33 +02:00
Lukas Tönne added 2 commits 2023-09-12 12:12:16 +02:00
Lukas Tönne force-pushed node-inline-sockets from 5dcbc47f0e to e2313abf3a 2023-09-12 13:37:19 +02:00 Compare
Lukas Tönne added 1 commit 2023-09-12 13:53:45 +02:00
Lukas Tönne added 2 commits 2023-09-12 14:43:58 +02:00
Lukas Tönne added 2 commits 2023-09-12 14:58:42 +02:00
Lukas Tönne requested review from Hans Goudey 2023-09-12 14:59:13 +02:00
Hans Goudey approved these changes 2023-09-12 15:29:09 +02:00
@ -457,0 +445,4 @@
/* Context pointers for current node and socket. */
PointerRNA nodeptr = RNA_pointer_create(&ntree.id, &RNA_Node, &node);
PointerRNA sockptr = RNA_pointer_create(&ntree.id, &RNA_NodeSocket, input_socket);
uiLayoutSetContextPointer(row, "node", &nodeptr);
Member

Might as well set the node constext pointer only once above

Might as well set the node constext pointer only once above
LukasTonne marked this conversation as resolved
@ -475,0 +529,4 @@
};
/* Utility to ensure valid state while iterating over interface items and sockets. */
struct NodeInterfaceIterator {
Member

Maybe it's worth mentioning the similarity to bNodeTreeInterface::foreach_item here? Or how it's different too maybe?

Maybe it's worth mentioning the similarity to `bNodeTreeInterface::foreach_item` here? Or how it's different too maybe?
LukasTonne marked this conversation as resolved
@ -475,0 +530,4 @@
/* Utility to ensure valid state while iterating over interface items and sockets. */
struct NodeInterfaceIterator {
private:
Member

Instead of struct ... { private:, this could just be class ...

Instead of `struct ... { private:`, this could just be `class ...`
LukasTonne marked this conversation as resolved
@ -475,0 +543,4 @@
NodeInterfaceIterator(bNode &node)
: node_(node),
decl_index_(0),
input_(static_cast<bNodeSocket *>(node.inputs.first)),
Member

Can use node.inputs() and node.outputs() spans here instead of using the next and prev pointers I think

Can use `node.inputs()` and `node.outputs()` spans here instead of using the next and prev pointers I think
LukasTonne marked this conversation as resolved
Lukas Tönne added 2 commits 2023-09-13 10:31:53 +02:00
Lukas Tönne added 4 commits 2023-09-13 11:08:30 +02:00
Jacques Lucke reviewed 2023-09-13 11:08:50 +02:00
@ -475,0 +528,4 @@
}
};
/* Utility to ensure valid state while iterating over interface items and sockets. */
Member

In my experience it is often easier to just fill a vector with all the elements instead of building an iterator that has to keep track of the current iteration state explicitly. You could just build a Vector<std::variant<NodeInterfaceSocketData, NodeInterfacePanelData>> in one go (with some large inline buffer), and then iterate over it afterwards. That also often makes debugging easier, because it's much easier to check that the generated vector contains what you expect.

In my experience it is often easier to just fill a vector with all the elements instead of building an iterator that has to keep track of the current iteration state explicitly. You could just build a `Vector<std::variant<NodeInterfaceSocketData, NodeInterfacePanelData>>` in one go (with some large inline buffer), and then iterate over it afterwards. That also often makes debugging easier, because it's much easier to check that the generated vector contains what you expect.
Author
Member

Sounds reasonable, i'll give it a try.

Sounds reasonable, i'll give it a try.
Author
Member

I've replaced the iterator as suggested, hopefully the code is a little bit easier to understand.

I've replaced the iterator as suggested, hopefully the code is a little bit easier to understand.
Jacques Lucke reviewed 2023-09-13 11:14:14 +02:00
@ -273,2 +278,3 @@
static_assert(std::is_base_of_v<SocketDeclaration, SocketDecl>);
SocketDecl *decl_;
SocketDecl *decl_in_;
SocketDecl *decl_out_;
Member

What do you think about also storing a Vector<SocketDecl *, 2> here that either contains one or two decls? This could reduce some redundancy in the methods below because one could use a loop instead of repeating the code twice.

What do you think about also storing a `Vector<SocketDecl *, 2>` here that either contains one or two decls? This could reduce some redundancy in the methods below because one could use a loop instead of repeating the code twice.
Author
Member

I've tried this and it doesn't meaningfully reduce the amount of code. There are also a bunch of places where an input declaration is treated differently from an output declaration and would have to check the type, for example (code from main):

  Self &field_on_all()
  {
    if (decl_->in_out == SOCK_IN) {
      this->supports_field();
    }
    else {
      this->field_source();
    }
    field_on_all_ = true;
    return *(Self *)this;
  }

We might eventually have cases where a single declaration builder can generate a whole bunch of sockets, like a multi-input, but i'd rather implement this when needed.

I've tried this and it doesn't meaningfully reduce the amount of code. There are also a bunch of places where an input declaration is treated differently from an output declaration and would have to check the type, for example (code from `main`): ```cpp Self &field_on_all() { if (decl_->in_out == SOCK_IN) { this->supports_field(); } else { this->field_source(); } field_on_all_ = true; return *(Self *)this; } ``` We might eventually have cases where a single declaration builder can generate a whole bunch of sockets, like a multi-input, but i'd rather implement this when needed.
LukasTonne marked this conversation as resolved
Lukas Tönne added 1 commit 2023-09-13 12:52:51 +02:00
Jacques Lucke approved these changes 2023-09-13 13:04:36 +02:00
Author
Member

@blender-bot build

@blender-bot build
Author
Member

@blender-bot build

@blender-bot build
Author
Member

@blender-bot build

@blender-bot build
Lukas Tönne added 2 commits 2023-09-14 15:50:33 +02:00
Author
Member

@blender-bot build

@blender-bot build
Lukas Tönne merged commit 9433a1701a into main 2023-09-14 16:08:11 +02:00
Lukas Tönne deleted branch node-inline-sockets 2023-09-14 16:08:13 +02:00
Sign in to join this conversation.
No reviewers
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No Assignees
4 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#112250
No description provided.