Skip to content
Snippets Groups Projects
curve_to_mesh_convert.cc 31.14 KiB
/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "BLI_array.hh"
#include "BLI_devirtualize_parameters.hh"
#include "BLI_set.hh"
#include "BLI_task.hh"

#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"

#include "BKE_attribute_math.hh"
#include "BKE_curves.hh"
#include "BKE_geometry_set.hh"
#include "BKE_material.h"
#include "BKE_mesh.h"

#include "BKE_curve_to_mesh.hh"

namespace blender::bke {

static void mark_edges_sharp(MutableSpan<MEdge> edges)
{
  for (MEdge &edge : edges) {
    edge.flag |= ME_SHARP;
  }
}

static void fill_mesh_topology(const int vert_offset,
                               const int edge_offset,
                               const int poly_offset,
                               const int loop_offset,
                               const int main_point_num,
                               const int profile_point_num,
                               const bool main_cyclic,
                               const bool profile_cyclic,
                               const bool fill_caps,
                               MutableSpan<MEdge> edges,
                               MutableSpan<MLoop> loops,
                               MutableSpan<MPoly> polys)
{
  const int main_segment_num = curves::segments_num(main_point_num, main_cyclic);
  const int profile_segment_num = curves::segments_num(profile_point_num, profile_cyclic);

  if (profile_point_num == 1) {
    for (const int i : IndexRange(main_point_num - 1)) {
      MEdge &edge = edges[edge_offset + i];
      edge.v1 = vert_offset + i;
      edge.v2 = vert_offset + i + 1;
      edge.flag = ME_LOOSEEDGE;
    }

    if (main_cyclic && main_segment_num > 1) {
      MEdge &edge = edges[edge_offset + main_segment_num - 1];
      edge.v1 = vert_offset + main_point_num - 1;
      edge.v2 = vert_offset;
      edge.flag = ME_LOOSEEDGE;
    }
    return;
  }

  /* Add the edges running along the length of the curve, starting at each profile vertex. */
  const int main_edges_start = edge_offset;
  for (const int i_profile : IndexRange(profile_point_num)) {
    const int profile_edge_offset = main_edges_start + i_profile * main_segment_num;
    for (const int i_ring : IndexRange(main_segment_num)) {
      const int i_next_ring = (i_ring == main_point_num - 1) ? 0 : i_ring + 1;

      const int ring_vert_offset = vert_offset + profile_point_num * i_ring;
      const int next_ring_vert_offset = vert_offset + profile_point_num * i_next_ring;
      MEdge &edge = edges[profile_edge_offset + i_ring];
      edge.v1 = ring_vert_offset + i_profile;
      edge.v2 = next_ring_vert_offset + i_profile;
      edge.flag = ME_EDGEDRAW | ME_EDGERENDER;
    }
  }

  /* Add the edges running along each profile ring. */
  const int profile_edges_start = main_edges_start + profile_point_num * main_segment_num;
  for (const int i_ring : IndexRange(main_point_num)) {
    const int ring_vert_offset = vert_offset + profile_point_num * i_ring;

    const int ring_edge_offset = profile_edges_start + i_ring * profile_segment_num;
    for (const int i_profile : IndexRange(profile_segment_num)) {
      const int i_next_profile = (i_profile == profile_point_num - 1) ? 0 : i_profile + 1;

      MEdge &edge = edges[ring_edge_offset + i_profile];
      edge.v1 = ring_vert_offset + i_profile;
      edge.v2 = ring_vert_offset + i_next_profile;
      edge.flag = ME_EDGEDRAW | ME_EDGERENDER;
    }
  }

  /* Calculate poly and corner indices. */
  for (const int i_ring : IndexRange(main_segment_num)) {
    const int i_next_ring = (i_ring == main_point_num - 1) ? 0 : i_ring + 1;

    const int ring_vert_offset = vert_offset + profile_point_num * i_ring;
    const int next_ring_vert_offset = vert_offset + profile_point_num * i_next_ring;

    const int ring_edge_start = profile_edges_start + profile_segment_num * i_ring;
    const int next_ring_edge_offset = profile_edges_start + profile_segment_num * i_next_ring;

    const int ring_poly_offset = poly_offset + i_ring * profile_segment_num;
    const int ring_loop_offset = loop_offset + i_ring * profile_segment_num * 4;

    for (const int i_profile : IndexRange(profile_segment_num)) {
      const int ring_segment_loop_offset = ring_loop_offset + i_profile * 4;
      const int i_next_profile = (i_profile == profile_point_num - 1) ? 0 : i_profile + 1;

      const int main_edge_start = main_edges_start + main_segment_num * i_profile;
      const int next_main_edge_start = main_edges_start + main_segment_num * i_next_profile;

      MPoly &poly = polys[ring_poly_offset + i_profile];
      poly.loopstart = ring_segment_loop_offset;
      poly.totloop = 4;
      poly.flag = ME_SMOOTH;

      MLoop &loop_a = loops[ring_segment_loop_offset];
      loop_a.v = ring_vert_offset + i_profile;
      loop_a.e = ring_edge_start + i_profile;
      MLoop &loop_b = loops[ring_segment_loop_offset + 1];
      loop_b.v = ring_vert_offset + i_next_profile;
      loop_b.e = next_main_edge_start + i_ring;
      MLoop &loop_c = loops[ring_segment_loop_offset + 2];
      loop_c.v = next_ring_vert_offset + i_next_profile;
      loop_c.e = next_ring_edge_offset + i_profile;
      MLoop &loop_d = loops[ring_segment_loop_offset + 3];
      loop_d.v = next_ring_vert_offset + i_profile;
      loop_d.e = main_edge_start + i_ring;
    }
  }

  const bool has_caps = fill_caps && !main_cyclic && profile_cyclic && profile_point_num > 2;
  if (has_caps) {
    const int poly_num = main_segment_num * profile_segment_num;
    const int cap_loop_offset = loop_offset + poly_num * 4;
    const int cap_poly_offset = poly_offset + poly_num;

    MPoly &poly_start = polys[cap_poly_offset];
    poly_start.loopstart = cap_loop_offset;
    poly_start.totloop = profile_segment_num;
    MPoly &poly_end = polys[cap_poly_offset + 1];
    poly_end.loopstart = cap_loop_offset + profile_segment_num;
    poly_end.totloop = profile_segment_num;

    const int last_ring_index = main_point_num - 1;
    const int last_ring_vert_offset = vert_offset + profile_point_num * last_ring_index;
    const int last_ring_edge_offset = profile_edges_start + profile_segment_num * last_ring_index;

    for (const int i : IndexRange(profile_segment_num)) {
      const int i_inv = profile_segment_num - i - 1;
      MLoop &loop_start = loops[cap_loop_offset + i];
      loop_start.v = vert_offset + i_inv;
      loop_start.e = profile_edges_start +
                     ((i == (profile_segment_num - 1)) ? (profile_segment_num - 1) : (i_inv - 1));
      MLoop &loop_end = loops[cap_loop_offset + profile_segment_num + i];
      loop_end.v = last_ring_vert_offset + i;
      loop_end.e = last_ring_edge_offset + i;
    }

    mark_edges_sharp(edges.slice(profile_edges_start, profile_segment_num));
    mark_edges_sharp(edges.slice(last_ring_edge_offset, profile_segment_num));
  }
}

static void mark_bezier_vector_edges_sharp(const int profile_point_num,
                                           const int main_segment_num,
                                           const Span<int> control_point_offsets,
                                           const Span<int8_t> handle_types_left,
                                           const Span<int8_t> handle_types_right,
                                           MutableSpan<MEdge> edges)
{
  const int main_edges_start = 0;
  if (curves::bezier::point_is_sharp(handle_types_left, handle_types_right, 0)) {
    mark_edges_sharp(edges.slice(main_edges_start, main_segment_num));
  }

  for (const int i : IndexRange(profile_point_num).drop_front(1)) {
    if (curves::bezier::point_is_sharp(handle_types_left, handle_types_right, i)) {
      mark_edges_sharp(edges.slice(
          main_edges_start + main_segment_num * control_point_offsets[i - 1], main_segment_num));
    }
  }
}

static void fill_mesh_positions(const int main_point_num,
                                const int profile_point_num,
                                const Span<float3> main_positions,
                                const Span<float3> profile_positions,
                                const Span<float3> tangents,
                                const Span<float3> normals,
                                const Span<float> radii,
                                MutableSpan<MVert> mesh_positions)
{
  if (profile_point_num == 1) {
    for (const int i_ring : IndexRange(main_point_num)) {
      float4x4 point_matrix = float4x4::from_normalized_axis_data(
          main_positions[i_ring], normals[i_ring], tangents[i_ring]);
      if (!radii.is_empty()) {
        point_matrix.apply_scale(radii[i_ring]);
      }

      MVert &vert = mesh_positions[i_ring];
      copy_v3_v3(vert.co, point_matrix * profile_positions.first());
    }
  }
  else {
    for (const int i_ring : IndexRange(main_point_num)) {
      float4x4 point_matrix = float4x4::from_normalized_axis_data(
          main_positions[i_ring], normals[i_ring], tangents[i_ring]);
      if (!radii.is_empty()) {
        point_matrix.apply_scale(radii[i_ring]);
      }

      const int ring_vert_start = i_ring * profile_point_num;
      for (const int i_profile : IndexRange(profile_point_num)) {
        MVert &vert = mesh_positions[ring_vert_start + i_profile];
        copy_v3_v3(vert.co, point_matrix * profile_positions[i_profile]);
      }
    }
  }
}

struct CurvesInfo {
  const CurvesGeometry &main;
  const CurvesGeometry &profile;

  /* Make sure these are spans because they are potentially accessed many times. */
  VArraySpan<bool> main_cyclic;
  VArraySpan<bool> profile_cyclic;
};
static CurvesInfo get_curves_info(const CurvesGeometry &main, const CurvesGeometry &profile)
{
  return {main, profile, main.cyclic(), profile.cyclic()};
}

struct ResultOffsets {
  /** The total number of curve combinations. */
  int total;

  /** Offsets into the result mesh for each combination. */
  Array<int> vert;
  Array<int> edge;
  Array<int> loop;
  Array<int> poly;

  /* The indices of the main and profile curves that form each combination. */
  Array<int> main_indices;
  Array<int> profile_indices;
};
static ResultOffsets calculate_result_offsets(const CurvesInfo &info, const bool fill_caps)
{
  ResultOffsets result;
  result.total = info.main.curves_num() * info.profile.curves_num();
  result.vert.reinitialize(result.total + 1);
  result.edge.reinitialize(result.total + 1);
  result.loop.reinitialize(result.total + 1);
  result.poly.reinitialize(result.total + 1);

  result.main_indices.reinitialize(result.total);
  result.profile_indices.reinitialize(result.total);

  info.main.ensure_evaluated_offsets();
  info.profile.ensure_evaluated_offsets();

  int mesh_index = 0;
  int vert_offset = 0;
  int edge_offset = 0;
  int loop_offset = 0;
  int poly_offset = 0;
  for (const int i_main : info.main.curves_range()) {
    const bool main_cyclic = info.main_cyclic[i_main];
    const int main_point_num = info.main.evaluated_points_for_curve(i_main).size();
    const int main_segment_num = curves::segments_num(main_point_num, main_cyclic);
    for (const int i_profile : info.profile.curves_range()) {
      result.vert[mesh_index] = vert_offset;
      result.edge[mesh_index] = edge_offset;
      result.loop[mesh_index] = loop_offset;
      result.poly[mesh_index] = poly_offset;

      result.main_indices[mesh_index] = i_main;
      result.profile_indices[mesh_index] = i_profile;

      const bool profile_cyclic = info.profile_cyclic[i_profile];
      const int profile_point_num = info.profile.evaluated_points_for_curve(i_profile).size();
      const int profile_segment_num = curves::segments_num(profile_point_num, profile_cyclic);

      const bool has_caps = fill_caps && !main_cyclic && profile_cyclic && profile_point_num > 2;
      const int tube_face_num = main_segment_num * profile_segment_num;

      vert_offset += main_point_num * profile_point_num;

      /* Add the ring edges, with one ring for every curve vertex, and the edge loops
       * that run along the length of the curve, starting on the first profile. */
      edge_offset += main_point_num * profile_segment_num + main_segment_num * profile_point_num;

      /* Add two cap N-gons for every ending. */
      poly_offset += tube_face_num + (has_caps ? 2 : 0);

      /* All faces on the tube are quads, and all cap faces are N-gons with an edge for each
       * profile edge. */
      loop_offset += tube_face_num * 4 + (has_caps ? profile_segment_num * 2 : 0);

      mesh_index++;
    }
  }

  result.vert.last() = vert_offset;
  result.edge.last() = edge_offset;
  result.loop.last() = loop_offset;
  result.poly.last() = poly_offset;

  return result;
}

static eAttrDomain get_attribute_domain_for_mesh(const AttributeAccessor &mesh_attributes,
                                                 const AttributeIDRef &attribute_id)
{
  /* Only use a different domain if it is builtin and must only exist on one domain. */
  if (!mesh_attributes.is_builtin(attribute_id)) {
    return ATTR_DOMAIN_POINT;
  }

  std::optional<AttributeMetaData> meta_data = mesh_attributes.lookup_meta_data(attribute_id);
  if (!meta_data) {
    return ATTR_DOMAIN_POINT;
  }

  return meta_data->domain;
}

static bool should_add_attribute_to_mesh(const AttributeAccessor &curve_attributes,
                                         const AttributeAccessor &mesh_attributes,
                                         const AttributeIDRef &id)
{

  /* The position attribute has special non-generic evaluation. */
  if (id.is_named() && id.name() == "position") {
    return false;
  }
  /* Don't propagate built-in curves attributes that are not built-in on meshes. */
  if (curve_attributes.is_builtin(id) && !mesh_attributes.is_builtin(id)) {
    return false;
  }
  if (!id.should_be_kept()) {
    return false;
  }
  return true;
}

static GSpan evaluated_attribute_if_necessary(const GVArray &src,
                                              const CurvesGeometry &curves,
                                              const std::array<int, CURVE_TYPES_NUM> &type_counts,
                                              Vector<std::byte> &buffer)
{
  if (type_counts[CURVE_TYPE_POLY] == curves.curves_num() && src.is_span()) {
    return src.get_internal_span();
  }
  buffer.reinitialize(curves.evaluated_points_num() * src.type().size());
  GMutableSpan eval{src.type(), buffer.data(), curves.evaluated_points_num()};
  curves.interpolate_to_evaluated(src.get_internal_span(), eval);
  return eval;
}

/** Information at a specific combination of main and profile curves. */
struct CombinationInfo {
  int i_main;
  int i_profile;

  IndexRange main_points;
  IndexRange profile_points;

  bool main_cyclic;
  bool profile_cyclic;

  int main_segment_num;
  int profile_segment_num;

  IndexRange vert_range;
  IndexRange edge_range;
  IndexRange poly_range;
  IndexRange loop_range;
};
template<typename Fn>
static void foreach_curve_combination(const CurvesInfo &info,
                                      const ResultOffsets &offsets,
                                      const Fn &fn)
{
  threading::parallel_for(IndexRange(offsets.total), 512, [&](IndexRange range) {
    for (const int i : range) {
      const int i_main = offsets.main_indices[i];
      const int i_profile = offsets.profile_indices[i];

      const IndexRange main_points = info.main.evaluated_points_for_curve(i_main);
      const IndexRange profile_points = info.profile.evaluated_points_for_curve(i_profile);

      const bool main_cyclic = info.main_cyclic[i_main];
      const bool profile_cyclic = info.profile_cyclic[i_profile];

      /* Pass all information in a struct to avoid repeating arguments in many lambdas.
       * The idea is that inlining `fn` will help avoid accessing unnecessary information,
       * though that may or may not happen in practice. */
      fn(CombinationInfo{i_main,
                         i_profile,
                         main_points,
                         profile_points,
                         main_cyclic,
                         profile_cyclic,
                         curves::segments_num(main_points.size(), main_cyclic),
                         curves::segments_num(profile_points.size(), profile_cyclic),
                         offsets_to_range(offsets.vert.as_span(), i),
                         offsets_to_range(offsets.edge.as_span(), i),
                         offsets_to_range(offsets.poly.as_span(), i),
                         offsets_to_range(offsets.loop.as_span(), i)});
    }
  });
}

template<typename T>
static void copy_main_point_data_to_mesh_verts(const Span<T> src,
                                               const int profile_point_num,
                                               MutableSpan<T> dst)
{
  for (const int i_ring : src.index_range()) {
    const int ring_vert_start = i_ring * profile_point_num;
    dst.slice(ring_vert_start, profile_point_num).fill(src[i_ring]);
  }
}

template<typename T>
static void copy_main_point_data_to_mesh_edges(const Span<T> src,
                                               const int profile_point_num,
                                               const int main_segment_num,
                                               const int profile_segment_num,
                                               MutableSpan<T> dst)
{
  const int edges_start = profile_point_num * main_segment_num;
  for (const int i_ring : src.index_range()) {
    const int ring_edge_start = edges_start + profile_segment_num * i_ring;
    dst.slice(ring_edge_start, profile_segment_num).fill(src[i_ring]);
  }
}

template<typename T>
static void copy_main_point_data_to_mesh_faces(const Span<T> src,
                                               const int main_segment_num,
                                               const int profile_segment_num,
                                               MutableSpan<T> dst)
{
  for (const int i_ring : IndexRange(main_segment_num)) {
    const int ring_face_start = profile_segment_num * i_ring;
    dst.slice(ring_face_start, profile_segment_num).fill(src[i_ring]);
  }
}

static void copy_main_point_domain_attribute_to_mesh(const CurvesInfo &curves_info,
                                                     const ResultOffsets &offsets,
                                                     const eAttrDomain dst_domain,
                                                     const GSpan src_all,
                                                     GMutableSpan dst_all)
{
  attribute_math::convert_to_static_type(src_all.type(), [&](auto dummy) {
    using T = decltype(dummy);
    const Span<T> src = src_all.typed<T>();
    MutableSpan<T> dst = dst_all.typed<T>();
    switch (dst_domain) {
      case ATTR_DOMAIN_POINT:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_main_point_data_to_mesh_verts(
              src.slice(info.main_points), info.profile_points.size(), dst.slice(info.vert_range));
        });
        break;
      case ATTR_DOMAIN_EDGE:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_main_point_data_to_mesh_edges(src.slice(info.main_points),
                                             info.profile_points.size(),
                                             info.main_segment_num,
                                             info.profile_segment_num,
                                             dst.slice(info.edge_range));
        });
        break;
      case ATTR_DOMAIN_FACE:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_main_point_data_to_mesh_faces(src.slice(info.main_points),
                                             info.main_segment_num,
                                             info.profile_segment_num,
                                             dst.slice(info.poly_range));
        });
        break;
      case ATTR_DOMAIN_CORNER:
        /* Unsupported for now, since there are no builtin attributes to convert into. */
        break;
      default:
        BLI_assert_unreachable();
        break;
    }
  });
}

template<typename T>
static void copy_profile_point_data_to_mesh_verts(const Span<T> src,
                                                  const int main_point_num,
                                                  MutableSpan<T> dst)
{
  for (const int i_ring : IndexRange(main_point_num)) {
    const int profile_vert_start = i_ring * src.size();
    for (const int i_profile : src.index_range()) {
      dst[profile_vert_start + i_profile] = src[i_profile];
    }
  }
}

template<typename T>
static void copy_profile_point_data_to_mesh_edges(const Span<T> src,
                                                  const int main_segment_num,
                                                  MutableSpan<T> dst)
{
  for (const int i_profile : src.index_range()) {
    const int profile_edge_offset = i_profile * main_segment_num;
    dst.slice(profile_edge_offset, main_segment_num).fill(src[i_profile]);
  }
}

template<typename T>
static void copy_profile_point_data_to_mesh_faces(const Span<T> src,
                                                  const int main_segment_num,
                                                  const int profile_segment_num,
                                                  MutableSpan<T> dst)
{
  for (const int i_ring : IndexRange(main_segment_num)) {
    const int profile_face_start = i_ring * profile_segment_num;
    for (const int i_profile : IndexRange(profile_segment_num)) {
      dst[profile_face_start + i_profile] = src[i_profile];
    }
  }
}

static void copy_profile_point_domain_attribute_to_mesh(const CurvesInfo &curves_info,
                                                        const ResultOffsets &offsets,
                                                        const eAttrDomain dst_domain,
                                                        const GSpan src_all,
                                                        GMutableSpan dst_all)
{
  attribute_math::convert_to_static_type(src_all.type(), [&](auto dummy) {
    using T = decltype(dummy);
    const Span<T> src = src_all.typed<T>();
    MutableSpan<T> dst = dst_all.typed<T>();
    switch (dst_domain) {
      case ATTR_DOMAIN_POINT:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_profile_point_data_to_mesh_verts(
              src.slice(info.profile_points), info.main_points.size(), dst.slice(info.vert_range));
        });
        break;
      case ATTR_DOMAIN_EDGE:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_profile_point_data_to_mesh_edges(
              src.slice(info.profile_points), info.main_segment_num, dst.slice(info.edge_range));
        });
        break;
      case ATTR_DOMAIN_FACE:
        foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
          copy_profile_point_data_to_mesh_faces(src.slice(info.profile_points),
                                                info.main_segment_num,
                                                info.profile_segment_num,
                                                dst.slice(info.poly_range));
        });
        break;
      case ATTR_DOMAIN_CORNER:
        /* Unsupported for now, since there are no builtin attributes to convert into. */
        break;
      default:
        BLI_assert_unreachable();
        break;
    }
  });
}

template<typename T>
static void copy_indices_to_offset_ranges(const VArray<T> &src,
                                          const Span<int> curve_indices,
                                          const Span<int> mesh_offsets,
                                          MutableSpan<T> dst)
{
  /* This unnecessarily instantiates the "is single" case (which should be handled elsewhere if
   * it's ever used for attributes), but the alternative is duplicating the function for spans and
   * other virtual arrays. */
  devirtualize_varray(src, [&](const auto &src) {
    threading::parallel_for(curve_indices.index_range(), 512, [&](IndexRange range) {
      for (const int i : range) {
        dst.slice(offsets_to_range(mesh_offsets, i)).fill(src[curve_indices[i]]);
      }
    });
  });
}

static void copy_curve_domain_attribute_to_mesh(const ResultOffsets &mesh_offsets,
                                                const Span<int> curve_indices,
                                                const eAttrDomain dst_domain,
                                                const GVArray &src,
                                                GMutableSpan dst)
{
  Span<int> offsets;
  switch (dst_domain) {
    case ATTR_DOMAIN_POINT:
      offsets = mesh_offsets.vert;
      break;
    case ATTR_DOMAIN_EDGE:
      offsets = mesh_offsets.edge;
      break;
    case ATTR_DOMAIN_FACE:
      offsets = mesh_offsets.poly;
      break;
    case ATTR_DOMAIN_CORNER:
      offsets = mesh_offsets.loop;
      break;
    default:
      BLI_assert_unreachable();
      return;
  }
  attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
    using T = decltype(dummy);
    copy_indices_to_offset_ranges(src.typed<T>(), curve_indices, offsets, dst.typed<T>());
  });
}

Mesh *curve_to_mesh_sweep(const CurvesGeometry &main,
                          const CurvesGeometry &profile,
                          const bool fill_caps)
{
  const CurvesInfo curves_info = get_curves_info(main, profile);

  const ResultOffsets offsets = calculate_result_offsets(curves_info, fill_caps);
  if (offsets.vert.last() == 0) {
    return nullptr;
  }

  Mesh *mesh = BKE_mesh_new_nomain(
      offsets.vert.last(), offsets.edge.last(), 0, offsets.loop.last(), offsets.poly.last());
  mesh->flag |= ME_AUTOSMOOTH;
  mesh->smoothresh = DEG2RADF(180.0f);
  MutableSpan<MVert> verts(mesh->mvert, mesh->totvert);
  MutableSpan<MEdge> edges(mesh->medge, mesh->totedge);
  MutableSpan<MLoop> loops(mesh->mloop, mesh->totloop);
  MutableSpan<MPoly> polys(mesh->mpoly, mesh->totpoly);

  foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
    fill_mesh_topology(info.vert_range.start(),
                       info.edge_range.start(),
                       info.poly_range.start(),
                       info.loop_range.start(),
                       info.main_points.size(),
                       info.profile_points.size(),
                       info.main_cyclic,
                       info.profile_cyclic,
                       fill_caps,
                       edges,
                       loops,
                       polys);
  });

  const Span<float3> main_positions = main.evaluated_positions();
  const Span<float3> tangents = main.evaluated_tangents();
  const Span<float3> normals = main.evaluated_normals();
  const Span<float3> profile_positions = profile.evaluated_positions();

  Vector<std::byte> eval_buffer;

  const AttributeAccessor main_attributes = main.attributes();
  const AttributeAccessor profile_attributes = profile.attributes();

  Span<float> radii = {};
  if (main_attributes.contains("radius")) {
    radii = evaluated_attribute_if_necessary(
                main_attributes.lookup_or_default<float>("radius", ATTR_DOMAIN_POINT, 1.0f),
                main,
                main.curve_type_counts(),
                eval_buffer)
                .typed<float>();
  }

  foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
    fill_mesh_positions(info.main_points.size(),
                        info.profile_points.size(),
                        main_positions.slice(info.main_points),
                        profile_positions.slice(info.profile_points),
                        tangents.slice(info.main_points),
                        normals.slice(info.main_points),
                        radii.is_empty() ? radii : radii.slice(info.main_points),
                        verts.slice(info.vert_range));
  });

  if (profile.curve_type_counts()[CURVE_TYPE_BEZIER] > 0) {
    const VArray<int8_t> curve_types = profile.curve_types();
    const VArraySpan<int8_t> handle_types_left{profile.handle_types_left()};
    const VArraySpan<int8_t> handle_types_right{profile.handle_types_right()};

    foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
      if (curve_types[info.i_profile] == CURVE_TYPE_BEZIER) {
        const IndexRange points = profile.points_for_curve(info.i_profile);
        mark_bezier_vector_edges_sharp(points.size(),
                                       info.main_segment_num,
                                       profile.bezier_evaluated_offsets_for_curve(info.i_profile),
                                       handle_types_left.slice(points),
                                       handle_types_right.slice(points),
                                       edges.slice(info.edge_range));
      }
    });
  }

  Set<AttributeIDRef> main_attributes_set;

  MutableAttributeAccessor mesh_attributes = bke::mesh_attributes_for_write(*mesh);

  main_attributes.for_all([&](const AttributeIDRef &id, const AttributeMetaData meta_data) {
    if (!should_add_attribute_to_mesh(main_attributes, mesh_attributes, id)) {
      return true;
    }
    main_attributes_set.add_new(id);

    const eAttrDomain src_domain = meta_data.domain;
    const eCustomDataType type = meta_data.data_type;
    GVArray src = main_attributes.lookup(id, src_domain, type);

    const eAttrDomain dst_domain = get_attribute_domain_for_mesh(mesh_attributes, id);
    GSpanAttributeWriter dst = mesh_attributes.lookup_or_add_for_write_only_span(
        id, dst_domain, type);
    if (!dst) {
      return true;
    }

    if (src_domain == ATTR_DOMAIN_POINT) {
      copy_main_point_domain_attribute_to_mesh(
          curves_info,
          offsets,
          dst_domain,
          evaluated_attribute_if_necessary(src, main, main.curve_type_counts(), eval_buffer),
          dst.span);
    }
    else if (src_domain == ATTR_DOMAIN_CURVE) {
      copy_curve_domain_attribute_to_mesh(
          offsets, offsets.main_indices, dst_domain, src, dst.span);
    }

    dst.finish();
    return true;
  });

  profile_attributes.for_all([&](const AttributeIDRef &id, const AttributeMetaData meta_data) {
    if (main_attributes.contains(id)) {
      return true;
    }
    if (!should_add_attribute_to_mesh(profile_attributes, mesh_attributes, id)) {
      return true;
    }
    const eAttrDomain src_domain = meta_data.domain;
    const eCustomDataType type = meta_data.data_type;
    GVArray src = profile_attributes.lookup(id, src_domain, type);

    const eAttrDomain dst_domain = get_attribute_domain_for_mesh(mesh_attributes, id);
    GSpanAttributeWriter dst = mesh_attributes.lookup_or_add_for_write_only_span(
        id, dst_domain, type);
    if (!dst) {
      return true;
    }

    if (src_domain == ATTR_DOMAIN_POINT) {
      copy_profile_point_domain_attribute_to_mesh(
          curves_info,
          offsets,
          dst_domain,
          evaluated_attribute_if_necessary(src, profile, profile.curve_type_counts(), eval_buffer),
          dst.span);
    }
    else if (src_domain == ATTR_DOMAIN_CURVE) {
      copy_curve_domain_attribute_to_mesh(
          offsets, offsets.profile_indices, dst_domain, src, dst.span);
    }

    dst.finish();
    return true;
  });

  return mesh;
}

static CurvesGeometry get_curve_single_vert()
{
  CurvesGeometry curves(1, 1);
  curves.offsets_for_write().last() = 1;
  curves.positions_for_write().fill(float3(0));
  curves.fill_curve_types(CURVE_TYPE_POLY);

  return curves;
}

Mesh *curve_to_wire_mesh(const CurvesGeometry &curve)
{
  static const CurvesGeometry vert_curve = get_curve_single_vert();
  return curve_to_mesh_sweep(curve, vert_curve, false);
}

}  // namespace blender::bke