Skip to main content

Taskflow update #4 Writing a node: Node UI

It has been quite some time since last update. I've been slowly polishing things here and there in my free time.

Давненько я не постил обновлений. Все это время я медленно допиливал всякие мелочи и не очень, в свободное время по вечерам.

My plans of making an pre-alpha version public have changed. I really don't think there is much people following this project at all, so there is no rush dropping this half working demos. 

 Планы опубликовать пре-альфа демку изменились. Я подумал - тут и так максимум полтора человека интересуются этим проектом, и врядли полуработающая демка кого-то сейчас заинтересует.

I will better make it actually usable, make it able to actually be used in work, and make some example workflows. There is no rush for anything.

Я решил, что лучше я таки доведу альфу до полноценно рабочего состояния, сделаю примеров работы с системой, чтобы буквально можно было уже тестировать таскфлоу на своих реальных задачах. Не думаю, что стоит торопиться.

Anyhow, in this update i will describe the current state of node interface api, how you will be able to create interface for your own nodes.

В этом апдейте я опишу текущее состояние апи для создания интерфейса собственных нод.

As you remember, taskflow is all about processing tasks with nodes. And you can easily (that's the goal at least) write your own node types, define what they do, what workers' payload do they create and how does their interface look like.

 Напомню, в таскфлоу задачи (таски) обрабатываются нодами. Каждый может легко (по крайней мере такая цель) написать свой собственный тип ноды: определить поведение ноды, какие нагрузки для рабочих она создаёт, и как выглядит её юзер интерфейс.

Here we will concentrate on the interface part.

Об интерфейсе мы тут и поговорим.

Though inspired by Houdini node interface, taskflow node interface is simpler.

Хоть многие вещи и были вдохновлены интерфейсом нод гудини, в таскфлоу всё несколько проще.

A parameter displayed in node's UI is defined by an instance of Parameter class, those instances are grouped into layout hierarchy.

 Параметр ноды определяется объектом класса Parameter. эти объекты объединяются в произвольную иерархию лейаутов.

Adding a parameter to a node is as simple as doing this during node construction:

Добавить параметр для инстанса ноды в конструкторе класса очень просто:

self.get_ui().add_parameter('param name', 'label for display', NodeParameterType.String, 'default value')

Here 'param name' whould be the name by which you can address this parameter, and 'label for display' is displayed label in the UI

'param name' - это имя параметра, по которому его можно найти на ноде, а 'label for display' - текст исключительно для отображения в интерфейсе

As you might have noticed, parameter type is defined with an enum value. Why this and not subclassing? Dunno. Maybe that will actually be changed in the future. Most of what this type currently does is ensures type consistency when setting value, the rest of the code is the same for all types due to python's dynamic typing capabilities.

 Возможно вас интересует, почему тип параметра определяется енумом, а не дочерним классом? хз, может в будущем это поменяется. на данный момент в связи с динамической типизацией питона, код параметр класса практически одинаковый для разных типов, так что этот енум используется практически только для проверки типа задаваемого параметру значения.

Currently you can only have parameters of types: int, float, bool and string.

На данный момент доступны следующие типы параметров: int, float, bool и string.

any parameter can have a menu attached to it, it's as simple as:

так же любой параметр может иметь меню, добавить меню параметру достаточно просто:

param.add_menu((('display val1', 0), ('display val2', 4), ('donkey!', -2)))

Passed argument is just a sequence of pairs of label and value, where label is always string, and value is of the type of the parameter.

Подаваемый в функцию add_menu аргумент - это просто последовательность из пар текст-значение, где текст должен быть некоторой строкой, а значение - значением соответствующим типу параметра.

Menus are not affecting parameter evaluation and processing - it's a sort of metadata solemnly for UI to process and interpret.

Меню никак не влияет на логику и поведение параметра - это просто метадата, исключительно для подсказки интерфейсу, как отображать параметр.

Remember that parameters exist in layout hierarchy? well, you can use some predefined layouts with a simple with statement. For example, you want to have several parameters in the same line, default parameter layout is vertical, parameters appear from top to bottom, each taking one full line, and so instead of manually creating a horizontal layout and adding parameters to it, you can simply use this "shortcut" when initializing node:

Ранее я говорил, что параметры группируются в иерархию лейаутов. Но чтобы не усложнять процесс создания интерфейса, некоторые стандартные лейауты могут быть созданы через with блок. Обычно параметры отображаются сверху вниз, каждый занимает целую строку или более, но например если вам нужно создать несколько парамтров в одну строку - вместо ручного создания горизонтального лейаута вы можете воспользоваться простым with блоком:

ui = self.get_ui()
with ui.parameters_on_same_line_block():
    ui.add_parameter('name', 'Name', NodeParameterType.STRING, '')
    ui.add_parameter('type', '', NodeParameterType.INT, 0)

And those parameters will be part of automatically created horizontal parameter layout

Параметры, созданные внутри with блока будут добавлены в автоматически созданный горизонтальный лейаут.

same type of shortcut exists for creating dynamic parameter layouts, like multiparm blocks in Houdini:

подобный with блок так же существует для динамического лейаута, что-то в стиле multiparm блоков в гудини

with ui.multigroup_parameter_block('attr_count'):
    ui.add_parameter('smth', 'label', NodeParameterType.INT, 0)

As you can see, additional parameter with the name supplied to the multigroup_parameter_block function and label "count" appears. changing it will change the amount of instances of parameters inside the block. each will have a unique number added to it's name, like smth0, smth1, smth2 ... in this example

Дополнительный параметр с именем, переданным функции multigroup_parameter_block будет создан, и его изменение приведет к созданию или удалению копий параметров, созданных внутри with блока. Имена копий будут модифицированны добавлением порядкового номера копии, в данном случае параметры будут называться smth0, smth1, smth2 ...

One more feature is parameter visibility condition. Again as with menu, this feature does not a single bit affects parameter evaluation, and only serves as metadata for UI to decide how to display the parameter.

Еще одна возможность параметров - это задания условия отображения. Как и с меню, эта фича является лишь метадатой, которую интерфейс может взять во внимание для отображения.

Currently it's decided to use the most simple type of conditions: you can only give it another parameter on the same node, comparison type and value, like: other_param == 7 if parameter other_param value is equal to 7 - then the parameter with this visibility condition will be displayed

В данный момент возможны лишь простейшие условия: условие состоит из параметра с этой же ноды, типа сравнения и значения, с которым сравнивается параметр, например other_param == 7 что значит если параметр other_param равен 7 - параметр с таким условием отображения будет отображён

Now, combining these together we can easily implement interface for a node that sets a number of attributes on a task:

Итак, совмещая всё описанное выше мы можем определить интерфейс ноды, создающей атрибуты на тасках.


with ui.initializing_interface_lock():
    with ui.multigroup_parameter_block('attr_count'):
        with ui.parameters_on_same_line_block():
            ui.add_parameter('name', 'Name', NodeParameterType.STRING, '')
            type_param = ui.add_parameter('type', '', NodeParameterType.INT, 0)
            type_param.add_menu((('int', NodeParameterType.INT.value),
                                 ('bool', NodeParameterType.BOOL.value),
                                 ('float', NodeParameterType.FLOAT.value),
                                 ('string', NodeParameterType.STRING.value)))
            
            ui.add_parameter('svalue', 'val', NodeParameterType.STRING, '') \
              .add_visibility_condition(type_param, '==', NodeParameterType.STRING.value)
            ui.add_parameter('ivalue', 'val', NodeParameterType.INT, 0) \
              .add_visibility_condition(type_param, '==', NodeParameterType.INT.value)
            ui.add_parameter('fvalue', 'val', NodeParameterType.FLOAT, 0.0) \
              .add_visibility_condition(type_param, '==', NodeParameterType.FLOAT.value)
            ui.add_parameter('bvalue', 'val', NodeParameterType.BOOL, False) \
              .add_visibility_condition(type_param, '==', NodeParameterType.BOOL.value)

You can see that there is a parameter for each type of attribute value, but each of them has a visibility condition that makes them visible only when appropriate value is selected in "type" parameter.

Как видно из кода - создаётся по одному параметру на каждый тип создаваемого атрибута, но каждому из них задаётся условие видимости, чтобы он отрисовывался только когда выбран соотвествующий тип в параметре type

the resulting interface will look like this:

созданный выше интерфейс выглядит так:

And to query values from parameters, in node instance's processing and   postprocessing overridden methods just use supplied evaluation context and do: context.param_value('parameter name')
all parameter expressions and expansions will be dealt with by the context, and you just get the final value

Получение значений параметров из методов processing и postprocessing объекта нового класса ноды происходит через предоставляемый этим методам параметр context:
context.param_value('parameter name')
значение параметра будет получено с учетом его экспрешнов.

From example above you might have noticed that all ui operations happen within with ui.initializing_interface_lock() block. It's just a safaguard, only inside it node UI modification is allowed. 

Так же в примере выше все операции над интерфейсом происходят внутри блока with ui.initializing_interface_lock(). Это блок предохранитель, модификации интерфейса ноды разрешены только внутри него

Well, that's it for this update. Probably next update i should show an example of a whole node, or at least of it's actual processing part.

Наверное в следующем апдейте я покажу оставшуюся часть определения ноды. А пока что - вот вцелом и всё.