--- a
+++ b/R/3-1.layout.R
@@ -0,0 +1,1032 @@
+# ========3.layout========
+#' Layout coordinates
+#'
+#' @param go igraph or metanet
+#' @param method
+#'  (1) as_line(), as_arc(), as_polygon(), as_polyarc(), as_polycircle(), as_circle_tree();
+#'  (2) as_star(), as_tree(), in_circle(), nicely(), on_grid(), on_sphere(),randomly(), with_dh(), with_fr(), with_gem(), with_graphopt(), with_kk(),with_lgl(), with_mds(),. see \code{\link[igraph]{layout_}};
+#'  (3) a character, "auto","backbone","centrality","circlepack","dendrogram",
+#'  "eigen","focus","hive","igraph","linear","manual","matrix",
+#'  "partition","pmds","stress","treemap","unrooted". see \code{\link[ggraph]{create_layout}}
+#' @param order_by order nodes according to a node attribute
+#' @param order_ls manual the discrete variable with a vector, or continuous variable with "desc" to decreasing
+#' @param seed random seed
+#' @param line_curved consider line curved, only for some layout methods like as_line(), as_polygon().default:0
+#' @param rescale logical, scale the X, Y to (-1,1)
+#' @param ... add
+#'
+#' @aliases c_net_lay
+#' @family layout
+#' @return coors object: coordinates for nodes, columns: name, X, Y; curved for edges, columns: from, to, curved;
+#' @export
+#' @examples
+#' library(igraph)
+#' c_net_layout(co_net) -> coors
+#' c_net_plot(co_net, coors)
+#' c_net_plot(co_net, c_net_layout(co_net, in_circle()), vertex.size = 2)
+#' c_net_plot(co_net, c_net_layout(co_net, in_circle(), order_by = "v_class"), vertex.size = 2)
+#' c_net_plot(co_net, c_net_layout(co_net, in_circle(), order_by = "size", order_ls = "desc"))
+#' c_net_plot(co_net, c_net_layout(co_net, as_polygon(3)))
+c_net_layout <- function(go, method = igraph::nicely(), order_by = NULL, order_ls = NULL,
+                         seed = 1234, line_curved = 0.5, rescale = TRUE, ...) {
+  set.seed(seed)
+  name <- x <- y <- NULL
+  if ("igraph_layout_spec" %in% class(method)) {
+    coors <- igraph::layout_(go, method)
+  } else if ("poly" %in% class(method)) {
+    coors <- method(go, group2 = order_by, group2_order = order_ls)
+  } else if ("layout" %in% class(method)) {
+    coors <- method(go)
+  } else if (is.character(method)) {
+    message("Use method from `ggraph`: ", method)
+    lib_ps("ggraph", library = FALSE)
+    data <- ggraph::create_layout(clean_igraph(go), layout = method, ...)
+    coors <- data %>% dplyr::select(name, x, y)
+    colnames(coors) <- c("name", "X", "Y")
+  } else {
+    stop("No valid method")
+  }
+
+  if (inherits(coors, "coors")) {
+    if (rescale) {
+      coors <- rescale_coors(coors)
+    }
+    return(coors)
+  }
+
+  # order
+  if (is.matrix(coors)) {
+    get_v(go) -> tmp_v
+    coors <- order_tmp_v_name(tmp_v, coors, order_by = order_by, order_ls = order_ls)
+  }
+
+  curved <- NULL
+  # if line type, need to consider edge.curved
+  if ("line" %in% class(method)) {
+    tmp_e <- data.frame(igraph::as_data_frame(go))[, c("from", "to")]
+    if (nrow(tmp_e) > 0) {
+      curved <- data.frame(tmp_e, curved = line_curved, row.names = NULL)
+    }
+  }
+  coors <- data.frame(coors, row.names = NULL)
+  coors <- as_coors(coors, curved)
+  if (rescale) {
+    coors <- rescale_coors(coors)
+  }
+  return(coors)
+}
+
+order_tmp_v_name <- function(tmp_v, coors_mat, order_by = NULL, order_ls = NULL) {
+  if (is.null(order_by)) {
+    coors <- data.frame(name = tmp_v$name, X = coors_mat[, 1], Y = coors_mat[, 2], row.names = NULL)
+  } else {
+    ordervec <- tmp_v[, order_by]
+    if (is.numeric(ordervec)) {
+      name <- tmp_v[order(ordervec, decreasing = is.null(order_ls)), "name"]
+    } else {
+      ordervec <- pcutils::change_fac_lev(ordervec, order_ls)
+      name <- tmp_v[order(ordervec), "name"]
+    }
+    coors <- data.frame(name = name, X = coors_mat[, 1], Y = coors_mat[, 2], row.names = NULL)
+  }
+  return(coors)
+}
+
+is_layout <- \(x){
+  any(class(x) %in% c("igraph_layout_spec", "layout"))
+}
+
+
+#' Transfer to a coors object
+#'
+#' @param coors data.frame
+#' @param curved line curved
+#'
+#' @return coors object
+#' @export
+#'
+as_coors <- function(coors, curved = NULL) {
+  if (inherits(coors, "coors")) {
+    if (!is.null(curved)) attributes(coors)$curved <- curved
+    return(coors)
+  }
+  if (!identical(colnames(coors), c("name", "X", "Y"))) {
+    stop("The input should be a data frame with columns: name, X, Y")
+  }
+  attributes(coors)$class <- c("coors", class(coors))
+  attributes(coors)$curved <- curved
+  return(coors)
+}
+
+
+get_coors <- \(coors, go, ...){
+  edge_curved <- NULL
+  # 1.如果coors是NULL,去graph_attr找一下,没有的话就默认nicely计算
+  if (is.null(coors)) {
+    if (is.null(igraph::graph_attr(go, "coors"))) {
+      coors <- c_net_layout(go, igraph::nicely(), ...)
+    } else {
+      coors <- igraph::graph_attr(go, "coors")
+    }
+  }
+  # 2.如果是layout函数,那就计算
+  if (is_layout(coors)) coors <- c_net_layout(go, coors, ...)
+  # 3.如果是一个提供的coors对象(list),那就把curved传到edge里,coors导出为df
+  if (inherits(coors, "coors")) {
+    if (!is.null(attributes(coors)$curved)) {
+      edge_curved <- dplyr::left_join(get_e(go), attributes(coors)$curved, by = c("from", "to"), suffix = c(".x", "")) %>%
+        dplyr::select("from", "to", "curved")
+      edge_curved[is.na(edge_curved)] <- 0
+      edge_curved <- data.frame(edge_curved, row.names = NULL)
+    }
+  }
+  # 4.如果是matrix,变成df
+  if (is.data.frame(coors)) {
+    if (!"name" %in% colnames(coors)) coors <- as.matrix(coors)
+  }
+  if (is.matrix(coors)) coors <- data.frame(name = V(go)$name, X = coors[, 1], Y = coors[, 2], row.names = NULL)
+  # 5.如果是df了,那就对齐name用于下一步的绘图,
+  if (is.data.frame(coors)) {
+    coors <- coors[match(V(go)$name, coors$name), ]
+    coors <- as_coors(coors, edge_curved)
+    return(coors)
+  }
+  stop("coors wrong!")
+}
+
+rescale_coors <- function(coors, keep_asp = TRUE) {
+  stopifnot(inherits(coors, "coors"))
+  if (keep_asp) {
+    diff_x <- diff(range(coors$X))
+    diff_y <- diff(range(coors$Y))
+    if (diff_x > diff_y) {
+      coors$X <- mmscale(coors$X, -1, 1)
+      coors$Y <- mmscale(coors$Y, -1 * diff_y / diff_x, 1 * diff_y / diff_x)
+    } else {
+      coors$X <- mmscale(coors$X, -1 * diff_x / diff_y, 1 * diff_x / diff_y)
+      coors$Y <- mmscale(coors$Y, -1, 1)
+    }
+  } else {
+    coors$X <- mmscale(coors$X, -1, 1)
+    coors$Y <- mmscale(coors$Y, -1, 1)
+  }
+  coors
+}
+
+combine_coors <- function(..., list = NULL) {
+  name <- from <- to <- NULL
+  list <- c(list(...), list)
+  if (!all(vapply(list, \(i)inherits(i, "coors"), logical(1)))) stop("some input are not coors object")
+  coors <- lapply(list, \(i)i) %>% do.call(rbind, .)
+  curved <- lapply(list, \(i)attributes(i)$curved) %>% do.call(rbind, .)
+  if (any(duplicated(coors$name))) {
+    warning("some duplicated name in coors")
+    coors <- dplyr::distinct(coors, name, .keep_all = TRUE)
+  }
+  if (!is.null(curved)) {
+    curved2 <- dplyr::distinct(curved, from, to, .keep_all = TRUE)
+    if (nrow(curved2) != nrow(curved)) warning("some duplicates in attributes(i)$curved")
+    curved <- curved2
+  }
+  coors <- as_coors(coors, curved)
+  return(coors)
+}
+
+#' Transform the layout of a 'coors' object
+#'
+#' This function applies various transformations to a 'coors' object, including
+#' scaling, aspect ratio adjustment, rotation, mirroring, and pseudo-3D perspective transformation.
+#'
+#' @param coors An object of class 'coors', containing node coordinates.
+#' @param scale A numeric value to scale the layout (default = 1).
+#' @param aspect_ratio A numeric value to adjust the Y-axis scaling (default = 1).
+#' @param rotation A numeric value in degrees to rotate the layout (default = 0).
+#' @param mirror_x A logical value indicating whether to mirror along the X-axis (default = FALSE).
+#' @param mirror_y A logical value indicating whether to mirror along the Y-axis (default = FALSE).
+#' @param shear_x A numeric value to apply a shear transformation in the X direction (default = 0).
+#' @param shear_y A numeric value to apply a shear transformation in the Y direction (default = 0).
+#'
+#' @return A transformed 'coors' object with updated coordinates.
+#' @export
+transform_coors <- function(coors, scale = 1, aspect_ratio = 1,
+                            rotation = 0, mirror_x = FALSE, mirror_y = FALSE,
+                            shear_x = 0, shear_y = 0) {
+  stopifnot(inherits(coors, "coors"))
+
+  # 复制原始数据
+  new_coor <- coors
+
+  # 放大/缩小
+  new_coor$X <- new_coor$X * scale
+  new_coor$Y <- new_coor$Y * scale * aspect_ratio
+
+  # 旋转(角度转换为弧度)
+  theta <- rotation * pi / 180
+  new_x <- new_coor$X * cos(theta) - new_coor$Y * sin(theta)
+  new_y <- new_coor$X * sin(theta) + new_coor$Y * cos(theta)
+  new_coor$X <- new_x
+  new_coor$Y <- new_y
+
+  # 透视投影(shear变换)
+  new_coor$X <- new_coor$X + shear_x * new_coor$Y
+  new_coor$Y <- new_coor$Y + shear_y * new_coor$X
+
+  # 镜像变换
+  if (mirror_x) new_coor$X <- -new_coor$X
+  if (mirror_y) new_coor$Y <- -new_coor$Y
+
+  coors <- new_coor
+  return(coors)
+}
+
+#' Layout as a line
+#'
+#' @param angle anticlockwise rotation angle
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_line()(co_net)
+#' c_net_plot(co_net, coors = as_line(pi / 2))
+as_line <- function(angle = 0) {
+  fun <- \(go){
+    nv <- length(V(go))
+    data.frame(
+      x = seq(-cos(angle), cos(angle), len = nv),
+      y = seq(-sin(angle), sin(angle), len = nv)
+    ) %>%
+      as.matrix() %>%
+      round(., 4)
+  }
+  class(fun) <- c("line", "layout", "function")
+  fun
+}
+
+#' Layout as a arc
+#'
+#' @param angle anticlockwise rotation angle
+#' @param arc the radian of arc
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_arc()(co_net)
+#' c_net_plot(co_net, coors = as_arc(pi / 2))
+as_arc <- function(angle = 0, arc = pi) {
+  fun <- \(go){
+    # (0,0) is the midpoint of circle
+    nv <- length(V(go))
+    theta <- seq(-arc / 2 + angle, arc / 2 + angle, len = nv)
+    coor <- data.frame(x = cos(theta), y = sin(theta))
+    as.matrix(coor) %>% round(., 4)
+  }
+  class(fun) <- c("layout", "function")
+  fun
+}
+
+#' Layout as a polygon
+#'
+#' @param n how many edges of this polygon
+#' @param line_curved line_curved 0~0.5
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_polygon()(co_net)
+as_polygon <- function(n = 3, line_curved = 0.5) {
+  fun <- \(go, group2 = NULL, group2_order = NULL){
+    V(go)$poly_group <- rep(paste0("omic", seq_len(n)), len = length(go))
+    if (n < 2) stop("n should bigger than 1")
+    g_layout_polygon(go,
+      group = "poly_group", group2 = group2, group2_order = group2_order,
+      line_curved = line_curved
+    ) -> oridata
+    oridata
+  }
+  class(fun) <- c("poly", "layout", "function")
+  fun
+}
+
+#' Layout as a polyarc
+#'
+#' @param n how many arcs of this poly_arc
+#' @param space the space between each arc, default: pi/3
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_polyarc()(co_net)
+as_polyarc <- \(n = 3, space = pi / 3){
+  fun <- \(go, group2 = NULL, group2_order = NULL){
+    V(go)$poly_group <- rep(paste0("omic", 1:n), len = length(go))
+    if (n < 2) stop("n should bigger than 1")
+    g_layout_polyarc(go, "poly_group", space = space, group2 = group2, group2_order = group2_order) -> oridata
+    oridata
+  }
+  class(fun) <- c("poly", "layout", "function")
+  fun
+}
+
+#' Layout as a polycircle
+#'
+#' @param n how many circles of this polycircle
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_polycircle()(co_net)
+as_polycircle <- \(n = 5){
+  fun <- \(go, group2 = NULL, group2_order = NULL){
+    V(go)$poly_group <- rep(paste0("omic", 1:n), distribute_points_in_concentric_circles(length(go), n))
+    if (n < 2) stop("n should bigger than 1")
+    g_layout_polycircle(go, "poly_group", group2 = group2, group2_order = group2_order) -> oridata
+    oridata
+  }
+  class(fun) <- c("poly", "layout", "function")
+  fun
+}
+
+
+distribute_points_in_concentric_circles <- function(n, k = NULL, mode = c("linear", "perimeter")) {
+  mode <- match.arg(mode)
+
+  if (is.null(k)) {
+    # 自动决定层数:sqrt(n) 是个常用经验
+    k <- ceiling(sqrt(n))
+  }
+
+  layer_weights <- switch(mode,
+    linear = 1:k, # 权重 1, 2, ..., k
+    perimeter = 1:k # 半径与层号成比例,周长也是
+  )
+
+  # 归一化
+  weight_sum <- sum(layer_weights)
+  raw_counts <- n * layer_weights / weight_sum
+
+  # 四舍五入并微调使总和为n
+  counts <- round(raw_counts)
+  diff_n <- n - sum(counts)
+  if (diff_n != 0) {
+    # 按最大余数或最小绝对值误差来调整
+    adjust_order <- order(abs(raw_counts - counts), decreasing = TRUE)
+    for (i in seq_len(abs(diff_n))) {
+      idx <- adjust_order[i]
+      counts[idx] <- counts[idx] + sign(diff_n)
+    }
+  }
+
+  return(counts)
+}
+
+#' Layout as a multi_layer
+#'
+#' @param n how many arcs of this multi_layer
+#' @param layout see method in \code{\link[MetaNet]{c_net_layout}}
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+#' @examples
+#' as_multi_layer()(co_net)
+as_multi_layer <- \(n = 3, layout = on_grid()){
+  fun <- \(go, group2 = NULL, group2_order = NULL){
+    V(go)$poly_group <- rep(paste0("omic", 1:n), len = length(go))
+    if (n < 2) stop("n should bigger than 1")
+    g_layout_multi_layer(go, layout = layout, group = "poly_group", group2 = group2, group2_order = group2_order) -> oridata
+    oridata
+  }
+  class(fun) <- c("poly", "layout", "function")
+  fun
+}
+
+#' Layout as a multi_layer
+#'
+#' @param n how many arcs of this multi_layer
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+as_poly_sector <- \(n = 3){
+  fun <- \(go, group2 = NULL, group2_order = NULL){
+    V(go)$poly_group <- rep(paste0("omic", 1:n), len = length(go))
+    if (n < 2) stop("n should bigger than 1")
+    g_layout_poly_sector(go, group = "poly_group", group2 = group2, group2_order = group2_order) -> oridata
+    oridata
+  }
+  class(fun) <- c("poly", "layout", "function")
+  fun
+}
+
+
+#' Layout as a circle_tree
+#'
+#' @return A two-column matrix, each row giving the coordinates of a vertex, according to the ids of the vertex ids.
+#' @export
+#' @family layout
+as_circle_tree <- \(){
+  fun <- \(go){
+    name <- x <- y <- NULL
+    lib_ps("ggraph", library = FALSE)
+    data <- ggraph::create_layout(clean_igraph(go), layout = "igraph", algorithm = "tree", circular = TRUE)
+    coors <- data %>% dplyr::select(name, x, y)
+    colnames(coors) <- c("name", "X", "Y")
+    coors
+  }
+  class(fun) <- c("layout", "function")
+  fun
+}
+
+big_layout <- \(zoom1, layout1, nodeGroup){
+  c_net_update(igraph::make_ring(nlevels(nodeGroup$group))) -> tmp_da_net
+  da <- get_coors(layout1, tmp_da_net)
+  da$name <- NULL
+
+  if (!all(levels(nodeGroup$group) %in% rownames(da))) rownames(da) <- levels(nodeGroup$group)
+
+  # center of each group
+  scale_f <- (ceiling(max(da) - min(da)))
+  scale_f <- ifelse(scale_f == 0, 1, scale_f / 2)
+  da <- da / scale_f * zoom1
+  colnames(da) <- c("X", "Y")
+  return(da)
+}
+
+#' Layout with group
+#'
+#' @param go igraph or metanet object
+#' @param group group name (default: module)
+#' @param zoom1 big network layout size
+#' @param zoom2 average sub_network layout size, or numeric vector, or "auto"
+#' @param layout1 layout1 method, one of
+#'      (1) a dataframe or matrix: rowname is group, two columns are X and Y
+#'      (2) function: layout method for \code{\link{c_net_layout}} default: in_circle()
+#' @param layout2 one of functions: layout method for \code{\link{c_net_layout}}, or a list of functions.
+#' @param show_big_layout show the big layout to help you adjust.
+#' @param ... add
+#' @param group_order group_order
+#' @param rescale logical, scale the X, Y to (-1,1)
+#'
+#' @return coors
+#' @export
+#' @family g_layout
+#' @examples
+#' \donttest{
+#' data("c_net")
+#' module_detect(co_net, method = "cluster_fast_greedy") -> co_net_modu
+#' g_layout(co_net_modu, group = "module", zoom1 = 30, zoom2 = "auto", layout2 = as_line()) -> oridata
+#' plot(co_net_modu, coors = oridata)
+#' }
+g_layout <- function(go, group = "module", group_order = NULL, layout1 = in_circle(), zoom1 = 20, layout2 = in_circle(),
+                     zoom2 = 3, show_big_layout = FALSE, rescale = TRUE, ...) {
+  name <- NULL
+
+  stopifnot(is_igraph(go))
+  if (!group %in% igraph::vertex_attr_names(go)) stop("no group named ", group, " !")
+  get_v(go) %>% dplyr::select(name, !!group) -> nodeGroup
+  colnames(nodeGroup) <- c("ID", "group")
+  nodeGroup$group <- factor(nodeGroup$group)
+  stopifnot(nlevels(nodeGroup$group) > 1)
+
+  if (!is.null(group_order)) nodeGroup$group <- pcutils::change_fac_lev(nodeGroup$group, group_order)
+
+  da <- big_layout(zoom1, layout1, nodeGroup)
+  if (show_big_layout) {
+    message("Big layout:")
+    print(da)
+    graphics::plot.new()
+    plot(da, pch = 21, bty = "n", bg = get_cols(nrow(da), "col2"), main = "Big layout coordinates")
+    graphics::text(da$X * 0.8, da$Y * 0.9, rownames(da))
+    return(invisible())
+  }
+
+  # layout of vertexes in one group
+  {
+    layoutls <- list()
+    if (is_layout(layout2)) {
+      layoutls <- rep(list(layout2), nlevels(nodeGroup$group))
+    } else if (all(class(layout2) == "list")) {
+      if (is.null(names(layout2))) {
+        layoutls <- rep(layout2, len = nlevels(nodeGroup$group))
+      } else {
+        for (i in levels(nodeGroup$group)) {
+          if (i %in% names(layout2)) {
+            layoutls[[i]] <- layout2[[i]]
+          } else {
+            layoutls[[i]] <- igraph::in_circle()
+            warning("layout of ", i, " not set, use in_circle()")
+          }
+        }
+      }
+    }
+    if (is.null(names(layoutls))) names(layoutls) <- levels(nodeGroup$group)
+
+    zoom2 <- rep(zoom2, nlevels(nodeGroup$group))
+    if (zoom2[1] == "auto") zoom2 <- ceiling((table(nodeGroup$group))^(1 / 3))
+    names(zoom2) <- levels(nodeGroup$group)
+  }
+
+  # get coors
+  all_coors <- setNames(vector("list", nlevels(nodeGroup$group)), levels(nodeGroup$group))
+  for (i in levels(nodeGroup$group)) {
+    nodeGroup[nodeGroup[, "group"] == i, "ID"] -> tmpid
+    igraph::subgraph(go, tmpid) -> tmp_net
+
+    get_coors(layoutls[[i]], tmp_net, ...) -> coors
+    data <- coors
+    if ("igraph_layout_spec" %in% class(layoutls[[i]])) {
+      data[, c("X", "Y")] <- igraph::norm_coords(as.matrix(data[, c("X", "Y")]))
+    }
+    data[, "X"] <- data[, "X"] * zoom2[i] + da[i, "X"]
+    data[, "Y"] <- data[, "Y"] * zoom2[i] + da[i, "Y"]
+
+    coors <- data
+    all_coors[[i]] <- coors
+  }
+  coors <- combine_coors(list = all_coors)
+  if (rescale) {
+    coors <- rescale_coors(coors)
+  }
+  return(coors)
+}
+
+#' Layout with group as a polygon
+#'
+#' @param go igraph
+#' @param group group name (default:v_group)
+#' @param group2 group2 name, will order nodes in each group according to group2_order
+#' @param group2_order group2_order
+#' @param line_curved line_curved 0~1
+#' @param group_order group_order
+#'
+#' @return coors
+#' @export
+#' @family g_layout
+#' @examples
+#' g_layout_polygon(multi1) -> oridata
+#' c_net_plot(multi1, oridata)
+#' g_layout_polyarc(multi1, group2 = "v_class", group2_order = c(LETTERS[4:1])) -> oridata
+#' c_net_plot(multi1, oridata)
+#' g_layout_polycircle(co_net2, group2 = "v_class") -> oridata
+#' c_net_plot(co_net2, oridata)
+#' g_layout_multi_layer(co_net2, group2 = "v_class") -> oridata
+#' c_net_plot(co_net2, oridata)
+g_layout_polygon <- function(go, group = "v_group", group_order = NULL, group2 = NULL, group2_order = NULL, line_curved = 0.5) {
+  n <- length(unique(igraph::vertex.attributes(go)[[group]]))
+
+  if (n < 2) stop("n should bigger than 1")
+
+  get_v(go) %>% dplyr::pull(!!group) -> group1
+  group1_level <- table(group1)
+  V(go)$`_internal_group` <- group1
+
+  angle_ls <- -pi / 2 + (seq(0, n - 1, 1)) * 2 * pi / n
+  names(angle_ls) <- pcutils::change_fac_lev(names(group1_level), group_order) %>% levels()
+
+  layout2_ls <- list()
+  for (i in names(group1_level)) {
+    c_net_filter(go, `_internal_group` == i) %>%
+      c_net_layout(
+        method = as_line(angle_ls[i]), order_by = group2,
+        order_ls = group2_order, rescale = FALSE
+      ) -> layout2_ls[[i]]
+  }
+
+  g_layout(go,
+    group_order = group_order,
+    group = group, zoom1 = 1, zoom2 = 0.9 * (ifelse(n > 2, tan(pi / n), 2)),
+    layout2 = layout2_ls, order_by = group2, order_ls = group2_order
+  ) -> oridata
+
+  if (is.data.frame(oridata$curved)) oridata$curved$curved <- line_curved
+  oridata
+}
+
+
+#' Layout with group as a polyarc
+#'
+#' @param space the space between each arc, default: pi/4
+#' @param scale_node_num scale with the node number in each group
+#'
+#' @rdname g_layout_polygon
+#' @export
+g_layout_polyarc <- function(go, group = "v_group", group_order = NULL,
+                             group2 = NULL, group2_order = NULL, space = pi / 4,
+                             scale_node_num = TRUE) {
+  get_v(go) -> tmp_v
+  group1 <- as.factor(tmp_v[, group])
+  n <- nlevels(group1)
+  if (n < 2) stop("n should bigger than 1")
+
+  if (!is.null(group_order)) group1 <- pcutils::change_fac_lev(group1, group_order)
+  # consider each group numbers!!!
+  g_num <- table(group1)
+  sep <- space / n
+
+  if (scale_node_num) {
+    arc_r <- (2 * pi - space) * as.numeric(g_num) / length(group1)
+  } else {
+    arc_r <- rep((2 * pi - space) / n, n)
+  }
+
+  names(arc_r) <- levels(group1)
+
+  # coordinate
+  coors <- data.frame()
+  theta1 <- 0
+  for (i in names(arc_r)) {
+    tmp_t <- seq(theta1, theta1 + arc_r[i], len = g_num[i])
+    tmp_v1 <- tmp_v[tmp_v[, group] == i, ]
+    tmp_coor <- order_tmp_v_name(tmp_v1, data.frame(X = cos(tmp_t), Y = sin(tmp_t)), group2, group2_order)
+
+    coors <- rbind(coors, tmp_coor)
+    theta1 <- theta1 + arc_r[i] + sep
+  }
+  coors <- as_coors(coors)
+  coors <- rescale_coors(coors)
+  coors
+}
+
+
+#' Layout with group as a polycircle
+#'
+#' @rdname g_layout_polygon
+#' @export
+g_layout_polycircle <- function(go, group = "v_group", group_order = NULL, group2 = NULL, group2_order = NULL) {
+  name <- NULL
+  n <- length(unique(igraph::vertex.attributes(go)[[group]]))
+  if (n < 2) stop("n should bigger than 1")
+  get_v(go) %>% dplyr::select(name, !!group) -> nodeGroup
+
+  if (is.null(group_order)) {
+    group_order <- table(nodeGroup[, group]) %>%
+      sort() %>%
+      names()
+  }
+
+  g_layout(go,
+    group_order = group_order,
+    group = group, layout1 = matrix(0, nrow = n, ncol = 2),
+    zoom1 = 1, zoom2 = 1:n,
+    layout2 = igraph::in_circle(), order_by = group2, order_ls = group2_order
+  ) -> oridata
+  oridata
+}
+
+#' Layout with group as a multi_layer
+#'
+#' @param layout see method in \code{\link[MetaNet]{c_net_layout}}
+#'
+#' @rdname g_layout_polygon
+#' @export
+g_layout_multi_layer <- function(go, layout = igraph::in_circle(), group = "v_group", group_order = NULL,
+                                 group2 = NULL, group2_order = NULL, scale_node_num = TRUE) {
+  n <- length(unique(igraph::vertex.attributes(go)[[group]]))
+  if (n < 2) stop("n should bigger than 1")
+
+  get_v(go) %>% dplyr::pull(!!group) -> group1
+  group1_level <- table(group1)
+
+  V(go)$`_internal_group` <- group1
+
+  layout2_ls <- list()
+  for (i in names(group1_level)) {
+    c_net_filter(go, `_internal_group` == i) %>%
+      c_net_layout(method = layout, order_by = group2, order_ls = group2_order) %>%
+      transform_coors(shear_x = 3, aspect_ratio = 0.2) -> layout2_ls[[i]]
+  }
+
+  g_layout(go,
+    group_order = group_order,
+    group = group, layout1 = data.frame(X = 0, Y = seq(-1, 1, length = length(group1_level))),
+    zoom1 = 1, zoom2 = group1_level / mean(group1_level),
+    layout2 = layout2_ls
+  ) -> oridata
+  oridata
+}
+
+#' Layout with group nicely
+#'
+#' @param go igraph or metanet
+#' @param group group name (default: module)
+#' @param mode circlepack, treemap, backbone, stress
+#' @param ... add
+#'
+#' @export
+#' @return coors
+#' @family g_layout
+#' @examples
+#' \donttest{
+#' data("c_net")
+#' module_detect(co_net, method = "cluster_fast_greedy") -> co_net_modu
+#' if (requireNamespace("ggraph")) {
+#'   plot(co_net_modu, coors = g_layout_nice(co_net_modu, group = "module"))
+#'   plot(co_net_modu, coors = g_layout_nice(co_net_modu, group = "module", mode = "treemap"))
+#' }
+#' }
+g_layout_nice <- function(go, group = "module", mode = "circlepack", ...) {
+  name <- x <- y <- NULL
+  lib_ps("ggraph", library = FALSE)
+  stopifnot(is_igraph(go))
+
+  mode <- match.arg(mode, c("circlepack", "treemap", "backbone", "stress"))
+
+  if (!group %in% vertex_attr_names(go)) stop("no group named ", group, " !")
+  get_v(go) %>% dplyr::select(name, !!group) -> nodeGroup
+  colnames(nodeGroup) <- c("ID", "group")
+  nodeGroup$group <- as.factor(nodeGroup$group)
+
+  edge <- data.frame(from = paste("group_", nodeGroup$group, sep = ""), to = nodeGroup$ID)
+
+  directed <- TRUE
+  if (mode %in% c("backbone")) directed <- FALSE
+
+  mygraph <- igraph::graph_from_data_frame(edge, directed = directed)
+  data <- ggraph::create_layout(mygraph, layout = mode, ...)
+  coors <- data %>% dplyr::select(name, x, y)
+  colnames(coors) <- c("name", "X", "Y")
+  coors <- as_coors(coors)
+
+  coors <- rescale_coors(coors)
+  return(coors)
+}
+
+#' @rdname g_layout_nice
+#' @export
+g_layout_circlepack <- \(go, group = "module", ...){
+  g_layout_nice(go, group = group, mode = "circlepack", ...)
+}
+
+#' @rdname g_layout_nice
+#' @export
+g_layout_treemap <- \(go, group = "module", ...){
+  g_layout_nice(go, group = group, mode = "treemap", ...)
+}
+
+#' @rdname g_layout_nice
+#' @export
+g_layout_backbone <- \(go, group = "module", ...){
+  g_layout_nice(go, group = group, mode = "backbone", ...)
+}
+
+#' @rdname g_layout_nice
+#' @export
+g_layout_stress <- \(go, group = "module", ...){
+  g_layout_nice(go, group = group, mode = "stress", ...)
+}
+
+
+#' Generate spatial layout using `spatstat`
+#'
+#' @param go igraph or metanet object
+#' @param win A spatstat window object (owin), e.g. disc(), owin(poly=...); Or sf object.
+#' @param type Type of distribution: "random", "regular"
+#' @param mode "surface", "boundary"
+#' @param jitter for surface-regular, defalut 0
+#' @param curved Optional curved attribute for coors
+#' @param order_by order nodes according to a node attribute
+#' @param order_ls manual the discrete variable with a vector, or continuous variable with "desc" to decreasing
+#' @param seed random seed
+#' @param rescale rescale the coordinates to (0,1)
+#'
+#' @return A coors object (data.frame with class "coors" and attribute "curved")
+#' @export
+#'
+#' @examples
+#' \donttest{
+#' if (requireNamespace("spatstat.geom") && requireNamespace("spatstat.random")) {
+#'   poly_x <- c(0, 2, 2, 0)
+#'   poly_y <- c(0, 0, 1, 1)
+#'   win_poly <- spatstat.geom::owin(poly = list(x = poly_x, y = poly_y))
+#'   plot(win_poly)
+#'   coors1 <- spatstat_layout(co_net, win_poly, type = "regular", mode = "surface")
+#'   plot(co_net, coors = coors1)
+#'   coors2 <- spatstat_layout(co_net2, win_poly, type = "random", mode = "boundary")
+#'   plot(co_net2, coors = coors2)
+#'
+#'   if (requireNamespace("sf")) {
+#'     nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
+#'     poly <- nc[1, ]
+#'     coors <- spatstat_layout(go = multi1, win = poly, type = "regular", mode = "surface")
+#'     plot(multi1, coors = coors)
+#'   }
+#' }
+#' }
+spatstat_layout <- function(go, win,
+                            type = c("random", "regular"),
+                            mode = c("surface", "boundary"),
+                            jitter = 0,
+                            curved = NULL, order_by = NULL, order_ls = NULL,
+                            seed = 1234, rescale = TRUE) {
+  lib_ps("spatstat.geom", library = FALSE)
+  lib_ps("spatstat.random", library = FALSE)
+
+  type <- match.arg(type)
+  mode <- match.arg(mode)
+
+  if (inherits(win, "sf")) {
+    win <- sf_to_owin(win)
+  }
+  if (!inherits(win, "owin")) stop("Input 'win' must be a spatstat 'owin' object.")
+
+  n_nodes <- length(go)
+  set.seed(seed)
+  pts <- switch(mode,
+    surface = switch(type,
+      random = generate_random_points(n_nodes, win),
+      regular = generate_regular_points(n_nodes, win, jitter = jitter)
+    ),
+    boundary = switch(type,
+      random = generate_boundary_points_random(n_nodes, win),
+      regular = generate_boundary_points_regular(n_nodes, win)
+    )
+  )
+
+  coors <- data.frame(X = pts$x, Y = pts$y) %>%
+    dplyr::arrange_all() %>%
+    as.matrix()
+  # order
+  get_v(go) -> tmp_v
+  coors <- order_tmp_v_name(tmp_v, coors, order_by = order_by, order_ls = order_ls)
+
+  coors <- as_coors(coors, curved = curved)
+  if (rescale) coors <- rescale_coors(coors)
+  coors
+}
+
+generate_regular_points <- function(n_nodes, win, max_iter = 20, jitter = 0) {
+  xlen <- diff(win$xrange)
+  ylen <- diff(win$yrange)
+  ratio <- xlen / ylen
+
+  for (factor in seq(1.1, 5, length.out = max_iter)) {
+    ny <- ceiling(sqrt(n_nodes / ratio) * factor)
+    nx <- ceiling(ny * ratio)
+
+    gx <- seq(win$xrange[1], win$xrange[2], length.out = nx)
+    gy <- seq(win$yrange[1], win$yrange[2], length.out = ny)
+    grid <- expand.grid(x = gx, y = gy)
+
+    if (jitter > 0) {
+      gx_step <- mean(diff(gx))
+      gy_step <- mean(diff(gy))
+      grid$x <- grid$x + runif(nrow(grid), -jitter * gx_step, jitter * gx_step)
+      grid$y <- grid$y + runif(nrow(grid), -jitter * gy_step, jitter * gy_step)
+    }
+
+    inside <- spatstat.geom::inside.owin(grid$x, grid$y, win)
+    valid_pts <- grid[inside, ]
+    if (nrow(valid_pts) >= n_nodes) {
+      selected <- valid_pts[sample(nrow(valid_pts), n_nodes), ]
+      return(spatstat.geom::ppp(selected$x, selected$y, window = win))
+    }
+  }
+  stop("Failed to generate enough regularly spaced points inside window after multiple attempts.")
+}
+
+
+generate_random_points <- function(n_nodes, win) {
+  spatstat.random::runifpoint(n_nodes, win = win)
+}
+
+generate_boundary_points_random <- function(n_nodes, win) {
+  bnd <- spatstat.geom::edges(win_poly)
+  pts <- spatstat.random::runifpointOnLines(n_nodes, bnd)
+  spatstat.geom::ppp(pts$x, pts$y, window = win)
+}
+
+generate_boundary_points_regular <- function(n_nodes, win) {
+  bnd <- spatstat.geom::edges(win)
+  len <- spatstat.geom::lengths_psp(bnd)
+  total_len <- sum(len)
+  t_seq <- seq(0, total_len, length.out = n_nodes + 1)[-1]
+
+  pts_x <- numeric(n_nodes)
+  pts_y <- numeric(n_nodes)
+  cum_len <- cumsum(len)
+
+  for (i in seq_along(t_seq)) {
+    t <- t_seq[i]
+    seg_idx <- which(t <= cum_len)[1]
+    t0 <- if (seg_idx == 1) 0 else cum_len[seg_idx - 1]
+    frac <- (t - t0) / len[seg_idx]
+    seg <- bnd[seg_idx]
+    pts_x[i] <- seg$ends$x0 + frac * (seg$ends$x1 - seg$ends$x0)
+    pts_y[i] <- seg$ends$y0 + frac * (seg$ends$y1 - seg$ends$y0)
+  }
+
+  spatstat.geom::ppp(pts_x, pts_y, window = win)
+}
+
+sf_to_owin <- function(sf_obj) {
+  lib_ps("sf", library = FALSE)
+  lib_ps("spatstat.geom", library = FALSE)
+
+  # 确保输入是 sf 对象
+  if (!inherits(sf_obj, "sf")) stop("Input must be an 'sf' object.")
+  if (!sf::st_is(sf_obj, "POLYGON") && !sf::st_is(sf_obj, "MULTIPOLYGON")) {
+    stop("sf object must be a POLYGON or MULTIPOLYGON")
+  }
+
+  # 修复方向问题
+  sf_obj <- sf::st_make_valid(sf_obj) # 处理无效几何问题
+  # 强制外环为逆时针,内环为顺时针
+  sf_obj <- sf::st_set_geometry(sf_obj, sf::st_geometry(sf_obj))
+  sf_obj <- sf::st_simplify(sf_obj) # 简化几何
+
+  # 将 sf 对象转为 polylist 坐标
+  poly_coords <- sf::st_coordinates(sf_obj)[, 1:2]
+  polylist <- list(x = poly_coords[, 1], y = poly_coords[, 2])
+
+  # 创建 spatstat 的 owin 对象
+  win <- spatstat.geom::owin(poly = list(polylist))
+  return(win)
+}
+
+
+create_sector_window <- function(r0 = 0, r1 = 1, theta_start = 0, theta_end = pi) {
+  lib_ps("spatstat.geom", library = FALSE)
+  # theta in radians
+  n <- 100
+  angles <- seq(theta_start, theta_end, length.out = n)
+  x_outer <- r1 * cos(angles)
+  y_outer <- r1 * sin(angles)
+  x_inner <- r0 * cos(rev(angles))
+  y_inner <- r0 * sin(rev(angles))
+  x <- c(x_outer, x_inner)
+  y <- c(y_outer, y_inner)
+  spatstat.geom::owin(poly = list(x = x, y = y))
+}
+
+
+
+#' Layout with group
+#'
+#' @param go igraph
+#' @param group group name (default:v_group)
+#' @param group2 group2 name, will order nodes in each group according to group2_order
+#' @param group2_order group2_order
+#' @param group_order group_order
+#' @param space the space between each arc, default: pi/4
+#' @param scale_node_num scale with the node number in each group
+#' @param type Type of distribution: "random", "regular"
+#' @param mode "surface", "boundary"
+#' @param jitter for surface-regular, defalut 0
+#' @param curved Optional curved attribute for coors
+#'
+#' @return coors
+#' @export
+#' @family g_layout
+g_layout_poly_sector <- function(go,
+                                 group = "v_group", group_order = NULL,
+                                 group2 = NULL, group2_order = NULL,
+                                 space = pi / 4, jitter = 0,
+                                 scale_node_num = TRUE,
+                                 type = c("regular", "random"),
+                                 mode = c("surface", "boundary"),
+                                 curved = NULL) {
+  type <- match.arg(type)
+  mode <- match.arg(mode)
+
+  get_v(go) -> tmp_v
+  group1 <- as.factor(tmp_v[[group]])
+  if (!is.null(group_order)) group1 <- pcutils::change_fac_lev(group1, group_order)
+
+  n <- nlevels(group1)
+  if (n < 2) stop("n should be greater than 1")
+
+  g_num <- table(group1)
+  sep <- space / n
+
+  # 分配每组所占的弧度
+  if (scale_node_num) {
+    arc_r <- (2 * pi - space) * as.numeric(g_num) / sum(g_num)
+  } else {
+    arc_r <- rep((2 * pi - space) / n, n)
+  }
+  names(arc_r) <- levels(group1)
+
+  coors_all <- list()
+  theta1 <- 0
+  for (i in names(arc_r)) {
+    tmp_nodes <- tmp_v[tmp_v[[group]] == i, ]
+    sub_go <- igraph::make_empty_graph(n = nrow(tmp_nodes)) %>%
+      igraph::set_vertex_attr("name", value = tmp_nodes$name)
+
+    win <- create_sector_window(r0 = 0, r1 = 1, theta_start = theta1, theta_end = theta1 + arc_r[i])
+    tmp_layout <- spatstat_layout(sub_go, win = win, type = type, mode = mode, curved = curved, rescale = FALSE, jitter = jitter)
+
+    tmp_layout <- order_tmp_v_name(tmp_nodes, tmp_layout[, c("X", "Y")], group2, group2_order)
+    coors_all[[i]] <- tmp_layout
+    theta1 <- theta1 + arc_r[i] + sep
+  }
+
+  # 合并所有 layout
+  coors_df <- do.call(rbind, coors_all)
+  as_coors(coors_df, curved = curved) %>% rescale_coors()
+}