Модулі - Rete.js

Модулі

Цей гайд базується на гайдах Базовий редактор і Dataflow. Рекомендується переглянути їх для повного розуміння цього гайду.

МодуліDataflowControl flowAllmatter

У цьому гайді описано ключові аспекти побудови вузлів Module, які обробляють вкладені графи, враховуючи доступні вузли Input і Output.

Цей гайд спирається виключно на підхід Dataflow для спрощення розуміння коду. Ознайомившись із цим гайдом і наданим прикладом, ви зможете Control flow за допомогою інструкцій у гайді Control flow.

Підготовка вузлів

Основна ідея модулів полягає у створенні окремих графів з вузлами Input і Output. Наступним кроком є створення виділеного вузла Module, який відображатиме порти на основі вузлів відповідного типу, указаного в графі.

Для початку давайте створимо вузол, який слугуватиме нашою точкою входу:

ts
export class InputNode extends ClassicPreset.Node {
  public value = null;

  constructor(public key: string) {
    super("Input");

    this.addOutput("value", new ClassicPreset.Output(socket, "Number"));
  }

  data() {
    return { value: this.value };
  }
}

Визначений користувачем key має вирішальне значення для асоціації його з вхідним портом вузла Module. Крім того, нам потрібно вказати властивість value для введення вхідних даних.

Для того, щоб модуль мав хоч якусь користь, необхідний вузол Output

ts
export class OutputNode extends ClassicPreset.Node {
  constructor(public key: string) {
    super("Output");

    this.addInput("value", new ClassicPreset.Input(socket, "Number"));
  }

  data() {
    return {};
  }
}

У цьому випадку метод data повертає порожній об’єкт, оскільки вхідні дані можна отримати за допомогою методу fetchInputs без необхідності виконання вузла.

Вузол Module, який служить порталом у вкладений граф і відображає вхідні та вихідні порти, є найскладнішим вузлом. Давайте розглянемо спрощений приклад:

ts
export class ModuleNode {
  module: null | Module<Schemes> = null;

  constructor(path: string) {
    super("Module");

    this.setModule(path);
  }

  public async setModule(path: string) {
    this.module = findModule(path);

    await removeNodeConnections(this.id);

    if (this.module) {
      const { inputs, outputs } = this.module.getPorts();

      syncPorts(this, inputs, outputs);
    } else {
      syncPorts(this, [], []);
    }
  }

  async data(inputs: Record<string, any>) {
    const data = await this.module?.exec(inputs);

    return data || {};
  }
}

де

  • findModule повертає об'єкт, що представляє модуль, дозволяючи доступ до його портів для відображення та виконання вкладеного графу
  • syncPorts оновлює вхідні та вихідні порти, видаляючи застарілі та додаючи нові
  • removeNodeConnections видаляє всі підключення, дозволяючи нам видаляти порти, коли нам потрібно перемикати модулі

Майте на увазі, що внесення будь-яких динамічних змін до вузлів, як показано в цьому прикладі за допомогою syncPorts, потребує виклику area.update('node', node.id).

Щоб запобігти конфліктним викликам із кількох вузлів Module, які використовують один і той самий вкладений граф, обов’язково ініціалізуйте новий редактор і механізм у методі module.exec.

Виконання вкладеного графа

Ось спрощений приклад того, як може бути реалізований обробник вкладених графів:

ts
function findModule(path: string) {
  return {
    getPorts() {
      const editor = new NodeEditor<Schemes>();

      await importGraphByPath(path, editor);

      const nodes = editor.getNodes();
      const inputs = nodes
        .filter((n): n is InputNode => n instanceof InputNode)
        .map((n) => n.key);
      const outputs = nodes
        .filter((n): n is OutputNode => n instanceof OutputNode)
        .map((n) => n.key);

      return {
        inputs,
        outputs
      };
    },
    exec: async (inputData: Record<string, any>) => {
      const engine = new DataflowEngine<Schemes>();
      const editor = new NodeEditor<Schemes>();

      editor.use(engine);

      await importGraphByPath(path, editor);

      const nodes = editor.getNodes();

      injectInputs(nodes, inputData);

      return retrieveOutputs(nodes, engine);
    }
  };

де

  • getPorts отримує ключі для вузлів Input і Output і повертає їх
  • importGraphByPath призначений для завантаження основних вузлів і з’єднань для вашого модуля в редактор

Кожен виклик створює новий екземпляр редактора, щоб уникнути конфліктів під час обробки графу.

Наступний метод включає введення вхідних даних вузла Module у вузли Input вкладеного графа.

ts
function injectInputs(nodes: Schemes["Node"][], inputData: Record<string, any>) {
    const inputNodes = nodes.filter(node => node instanceof InputNode);

    inputNodes.forEach((node) => {
      // майте на увазі, що вхідних з’єднань може не бути, і ми припускаємо, що можливе максимум одне з’єднання
      node.value = inputData[node.key] && inputData[node.key][0];
    });
  }

Після введення вхідних даних наступним кроком є отримання вихідних даних із вузлів:

ts
async function retrieveOutputs(nodes: Schemes["Node"][], engine: DataflowEngine<Schemes>) {
  const outputNodes = nodes.filter(node => node instanceof OutputNode);

 // можна обробляти одночасно
  const outputs = await Promise.all(outputNodes.map(async node => {
    const data = await engine.fetchInputs(node.id);

    // ми розглядаємо лише дані з першого з’єднання, оскільки вхідне з’єднання може бути щонайбільше одне
    return [node.key, data.value[0]] as const;
  }));

  return Object.fromEntries(outputs);
}

Перегляньте повний результат на сторінці прикладу Модулі. Крім того, цей підхід реалізовано в додатку Allmatter.