编写布局插件

如何编写自定义布局引擎。

要创建一个名为 xxx 的新布局插件,首先需要提供两个函数:xxx_layoutxxx_cleanup。这些函数的语义将在下面描述。

布局

void xxx_layout(Agraph_t * g)

初始化图。

  • 如果算法将使用通用的边路由代码,它应该调用 setEdgeType (g, ...);

  • 对于每个节点,调用 common_init_nodegv_nodesize

  • 如果算法将使用 spline_edges() 来路由边,节点坐标需要存储在 ND_pos 中,因此需要在这里分配它。这以及上面提到的两个调用都由对 neato_init_node() 的调用来处理。

  • 对于每条边,调用 common_init_edge

  • 算法应该分配它需要的任何其他数据结构。这可能涉及 A*info_t 字段中的字段。此外,这些字段中的每一个都包含一个 void* alg; 子字段,算法可以使用它存储附加数据。一旦我们迁移到 cgraph,这将全部被替换为算法特定的记录。

  • 对图进行布局。完成后,每个节点都应该将其坐标存储在 ND_coord_i(n) 中的点,每个边都应该在其 ED_spl(e) 中描述其布局。(注意:从 2.21 版本开始,ND_coord_i 已被 ND_coord 替换,现在它们是浮点坐标。)

要添加边,有 3 个可用的函数

  1. spline_edges1 (Agraph_t*, int edgeType) 假设节点坐标存储在 ND_coord_i 中,并且 GD_bb 已设置。对于每条边,此函数构造适当的数据并将其存储在 ED_spl 中。
  2. spline_edges0 (Agraph_t*) 假设节点坐标存储在 ND_pos 中,并且 GD_bb 已设置。此函数使用 ratio 属性(如果设置),将 ND_pos 中的值复制到 ND_coord_i(从英寸转换为点);并使用 setEdgeType() 指定的边类型调用 spline_edges1
  3. spline_edges (Agraph_t*) 假设节点坐标存储在 ND_pos 中。此函数计算 g 的边界框并将其存储在 GD_bb 中,然后调用 spline_edges0()

如果算法只适用于连通分量,代码可以使用 pack 库来获取分量,分别对其进行布局,并根据用户规格将它们打包在一起。下面给出了一个典型的模式。可以查看 twopicirconeatofdp 的代码以获取更详细的示例。

Agraph_t **ccs;
Agraph_t *sg;
Agnode_t *c = NULL;
int ncc;
int i;

ccs = ccomps(g, &ncc, 0);
if (ncc == 1) {
    /* layout nodes of g */
    adjustNodes(g);  /* if you need to remove overlaps */
    spline_edges(g); /* generic edge routing code */

} else {
    pack_info pinfo;
    pack_mode pmode = getPackMode(g, l_node);

    for (i = 0; i < ncc; i++) {
        sg = ccs[i];
        /* layout sg */
        adjustNodes(sg);  /* if you need to remove overlaps */
    }
    spline_edges(g);  /* generic edge routing */

    /* initialize packing info, e.g. */
    pinfo.margin = getPack(g, CL_OFFSET, CL_OFFSET);
    pinfo.doSplines = 1;
    pinfo.mode = pmode;
    pinfo.fixed = 0;
    packSubgraphs(ncc, ccs, g, &pinfo);
}
for (i = 0; i < ncc; i++) {
    agdelete(g, ccs[i]);
}

free(ccs);

如果依赖于仅在根图中设置的属性,则在布局子图时要小心。使用连通分量,边可以在打包之前(如上)或在分量打包之后(参见 circo)与每个分量一起添加。

最好检查图有 0 个或 1 个节点,或者没有边的平凡情况。

xxx_layout 结束时,调用

dotneato_postprocess(g);

以下模板在大多数情况下都会起作用,忽略了处理断开连接的图和删除节点重叠的问题

static void
xxx_init_node(node_t * n)
{
  neato_init_node(n);
  /* add algorithm-specific data, if desired */
}

static void
xxx_init_edge(edge_t * e)
{
  common_init_edge(e);
  /* add algorithm-specific data, if desired */
}

static void
xxx_init_node_edge(graph_t * g)
{
  node_t *n;
  edge_t *e;

  for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
      xxx_init_node(n);
  }
  for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
      for (e = agfstout(g, n); e; e = agnxtout(g, e)){          
          xxx_init_edge(e);
      }
  }
}

void
xxx_layout (Agraph_t* g)
{
  xxx_init_node_edge(g);
  /* Set ND_pos(n) for each node n */
  spline_edges(g);
  dotneato_postprocess(g);
}  

清理

void xxx_cleanup(Agraph_t * g)

释放布局中分配的任何资源。

完成对每个节点和边的 gv_cleanup_nodegv_cleanup_edge 的调用。这将清理 spline 标签、ND_pos、形状并将 A*info_t 归零,因此这些必须最后发生,但如果需要,可以作为显式的 xxx_cleanup_nodexxx_cleanup_edge 的一部分。

最后,你应该执行

if (g != g->root) memset(&(g->u), 0, sizeof(Agraphinfo_t));

这对于图再次进行布局是必要的,因为布局代码假设此结构是干净的。

libgvc 对根图进行最后的清理,释放任何绘制内容,释放其标签,并将根图的 Agraphinfo_t 归零。

以下模板在大多数情况下都会起作用

static void xxx_cleanup_graph(Agraph_t * g)
{
  /* Free any algorithm-specific data attached to the graph */
  if (g != g->root) memset(&(g->u), 0, sizeof(Agraphinfo_t));
}

static void xxx_cleanup_edge (Agedge_t* e)
{
  /* Free any algorithm-specific data attached to the edge */
  gv_cleanup_edge(e);
}

static void xxx_cleanup_node (Agnode_t* n)
{
  /* Free any algorithm-specific data attached to the node */
  gv_cleanup_node(e);
}

void xxx_cleanup(Agraph_t * g)
{
  Agnode_t *n;
  Agedge_t *e;

  for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
      for (e = agfstout(g, n); e; e = agnxtout(g, e)) {
          xxx_cleanup_edge(e);
      }
      xxx_cleanup_node(n);
  }
  xxx_cleanup_graph(g);
}   

大多数布局使用类似于 neato 的辅助例程,因此入口点可以添加到 plugin/neato_layout 中。

添加到 gvlayout_neato_layout.c

gvlayout_engine_t xxxgen_engine = {
    xxx_layout,
    xxx_cleanup,
};

以及该文件中的 gvlayout_neato_typeslayout_type 中的新枚举 LAYOUT_XXX 的行

{LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &neatogen_features},

LAYOUT_XXX

以上允许新布局 piggyback 在 neato 插件之上,但需要重建插件。通常,用户可以(并且可能应该)完全独立地构建布局插件。

为此,在编写 xxx_layoutxxx_cleanup 之后,需要

  1. 添加类型和数据结构

    typedef enum { LAYOUT_XXX } layout_type;
    
    static gvlayout_features_t xxxgen_features = {
        0
    };
    gvlayout_engine_t xxxgen_engine = {
        xxx_layout,
        xxx_cleanup,
    };
    static gvplugin_installed_t gvlayout_xxx_types[] = {
        {LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &xxxgen_features},
        {0, NULL, 0, NULL, NULL}
    };
    static gvplugin_api_t apis[] = {
        {API_layout, &gvlayout_xxx_types},
        {(api_t)0, 0},
    };
    gvplugin_library_t gvplugin_xxx_layout_LTX_library = { "xxx_layout", apis };
    
  2. 将所有这些组合成一个动态库,其名称包含字符串 gvplugin_,并将该库安装在与其他 Graphviz 插件相同的目录中。例如,在 Linux 系统上,dot 布局插件位于库 libgvplugin_dot_layout.so 中。

  3. 运行 dot -c 以重新生成配置文件。

注意

  • 可以在 gvlayout_xxx_types 中添加额外的布局作为额外的行。
  • 显然,大多数名称和字符串可以是任意的。一个约束是 gvplugin_library_t 类型的外部标识符必须以 _LTX_library 结尾。此外,gvlayout_xxx_types 中每个条目中的字符串 xxx 是用于标识布局算法的名称,因此需要与任何其他布局名称不同。
  • 布局算法的功能目前仅限于一个位标志,并且唯一支持的标志是 LAYOUT_USES_RANKDIR,它使布局能够使用 rankdir 属性。

需要对任何静态了解布局算法的应用程序进行更改。

Automake 配置

如果您想将您的代码集成到 Graphviz 软件中并使用其构建系统,请按照以下说明进行操作。您当然可以使用自己的构建软件来构建和安装您的插件。

  1. 将您的软件放在 lib/xxxgen 中,并将上面描述的钩子添加到 gvlayout_neato_layout.c 中。
  2. lib/xxxgen 中,提供一个 Makefile.am(基于 lib/fdpgen/Makefile.am 等简单示例)。
  3. lib/Makefile.am 中,将 xxxgen 添加到 SUBDIRS 中。
  4. configure.ac 中,将 lib/xxxgen/Makefile 添加到 AC_CONFIG_FILES 中。
  5. lib/plugin/neato_layout/Makefile.am 中,在 libgvplugin_neato_layout_C_la_LIBADD 中插入 $(top_builddir)/lib/xxxgen/libxxxgen_C.la
  6. 请记住运行 autogen.sh,因为单独的 configure 可能无法正确猜测。

这也假设您的系统上安装了各种 automake 工具的良好版本。

最后修改时间 2022 年 12 月 6 日:修复错别字 (de77e3d)