编写布局插件

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

要创建一个名为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的代码实现。

int ncc;

Agraph_t **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 (int i = 0; i < ncc; i++) {
        Agraph_t *const 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 (int 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)
{
  for (node_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
      xxx_init_node(n);
  }
  for (node_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
      for (edge_t *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。这会清理样条线标签、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)
{
  for (Agnode_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
      for (Agedge_t *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,
};

以及该行

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

gvlayout_neato_types和新的枚举LAYOUT_XXX添加到该文件中的layout_type

上述方法允许新布局直接利用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}
    };
    static gvplugin_api_t apis[] = {
        {API_layout, &gvlayout_xxx_types},
        {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 文件中,将 $(top_builddir)/lib/xxxgen/libxxxgen_C.la 插入到 libgvplugin_neato_layout_C_la_LIBADD 中。
  6. 记得运行 autogen.sh,因为单独运行 configure 可能会猜错配置。

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

Last modified December 30, 2024: 修复“laying of”拼写错误 (197a4c5)