Fix #79163 bug related to the bevel operation producing disconnected UVs for new bevel faces. This change replaces previous approach using scattered and selective usage of functions: bev_merge_uvs, bev_merge_edge_uvs and bev_merge_end_uvs with one coherent technique for all stages of the bevel operation. It is utilizing a concept of loop (BMLoop) buckets to keep track of UV vertices that should be merged at the end of bevel operation by a single call to bevel_merge_uvs function. This approach doesn't touch initial UV position calculation done by interpolation algorithm in bev_create_ngon function and keeps the concept of representative faces (called frep, facerep or rep_face in code) to help decide to which bucket specific loops should be assigned. This is from PR https://projects.blender.org/blender/blender/pulls/139595, which has more explanation and discussion.
This commit is contained in:
committed by
Howard Trickey
parent
35bcbad7e9
commit
870f75b790
@@ -17,13 +17,14 @@
|
||||
#include "DNA_modifier_types.h"
|
||||
|
||||
#include "BLI_alloca.h"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_math_base_safe.h"
|
||||
#include "BLI_math_geom.h"
|
||||
#include "BLI_math_matrix.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
#include "BLI_math_vector.h"
|
||||
#include "BLI_memarena.h"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_utildefines.h"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "BKE_customdata.hh"
|
||||
#include "BKE_deform.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_mesh_mapping.hh"
|
||||
|
||||
#include "eigen_capi.h"
|
||||
|
||||
@@ -39,6 +41,8 @@
|
||||
|
||||
#include "./intern/bmesh_private.hh"
|
||||
|
||||
using blender::Map;
|
||||
using blender::Set;
|
||||
using blender::Vector;
|
||||
|
||||
// #define BEVEL_DEBUG_TIME
|
||||
@@ -197,6 +201,19 @@ struct MathLayerInfo {
|
||||
bool has_math_layers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Auxiliary structure representing bevel face created by `bev_create_ngon` function. It holds
|
||||
* reference to both newly create ngon and a representative face (from the original mesh) it is
|
||||
* attached to. This information helps with merging UVs - bevel faces that share the same
|
||||
* `attached_frep` pointer should have their neighboring UV verts connected.
|
||||
*/
|
||||
struct UVFace {
|
||||
/** `BMesh` face which this `UVFace` represents. */
|
||||
BMFace *f;
|
||||
/** `BMFace` of the original mesh to which bevel face `f` is attached in UV space. */
|
||||
BMFace *attached_frep;
|
||||
};
|
||||
|
||||
/**
|
||||
* An element in a cyclic boundary of a Vertex Mesh (VMesh), placed on each side of beveled edges
|
||||
* where each profile starts, or on each side of a miter.
|
||||
@@ -313,12 +330,22 @@ enum AngleKind {
|
||||
ANGLE_LARGER = 1,
|
||||
};
|
||||
|
||||
/** Container for loops representing UV verts which should be merged together in a UV map. */
|
||||
using UVVertBucket = Set<BMLoop *>;
|
||||
|
||||
/** Mapping of vertex to UV vert buckets (i.e. loops belonging to that key `BMVert`). */
|
||||
using UVVertMap = Map<BMVert *, Vector<UVVertBucket>>;
|
||||
|
||||
/** Bevel parameters and state. */
|
||||
struct BevelParams {
|
||||
/** Records BevVerts made: key BMVert*, value BevVert* */
|
||||
GHash *vert_hash;
|
||||
/** Records new faces: key BMFace*, value one of {VERT/EDGE/RECON}_POLY. */
|
||||
GHash *face_hash;
|
||||
/** Records `UVFace` made: key `BMFace*`, value `UVFace*`. */
|
||||
GHash *uv_face_hash;
|
||||
/** Container which keeps track of UV vert connectivity in different UV maps. */
|
||||
Vector<UVVertMap> uv_vert_maps;
|
||||
/**
|
||||
* Use for all allocations while bevel runs.
|
||||
* \note If we need to free we can switch to `BLI_mempool`.
|
||||
@@ -535,6 +562,12 @@ static BevVert *find_bevvert(BevelParams *bp, BMVert *bmv)
|
||||
return static_cast<BevVert *>(BLI_ghash_lookup(bp->vert_hash, bmv));
|
||||
}
|
||||
|
||||
/* Find the `UVFace` corresponding to `bmf` face. */
|
||||
static UVFace *find_uv_face(BevelParams *bp, BMFace *bmf)
|
||||
{
|
||||
return static_cast<UVFace *>(BLI_ghash_lookup(bp->uv_face_hash, bmf));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the EdgeHalf representing the other end of e->e.
|
||||
* \return other end's BevVert in *r_bvother, if r_bvother is provided. That may not have
|
||||
@@ -604,6 +637,202 @@ static bool edges_face_connected_at_vert(BMEdge *bme1, BMEdge *bme2)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and register new `UVFace` object based on a new face (`BMFace *fnew`); and assign proper
|
||||
* representative face from either `frep` or `frep_arr` arguments.
|
||||
*/
|
||||
static UVFace *register_uv_face(BevelParams *bp, BMFace *fnew, BMFace *frep, BMFace **frep_arr)
|
||||
{
|
||||
UVFace *uv_face = (UVFace *)BLI_memarena_alloc(bp->mem_arena, sizeof(UVFace));
|
||||
uv_face->f = fnew;
|
||||
uv_face->attached_frep = nullptr;
|
||||
if (frep_arr && frep_arr[0]) {
|
||||
/* Choosing first face from `frep_arr` is an arbitrary choice but for our algorithm it doesn't
|
||||
* matter. Usually the difference in `frep` and `frep_arr` is that the latter is used when
|
||||
* loops' custom data for a new bevel face is interpolated between multiple original mesh faces
|
||||
* on top of which this new bevel face is being constructed; in such case original faces
|
||||
* _should_ be already connected in UV space (i.e. no seam) and we handle such scenarios when
|
||||
* setting proper value for `is_orig_uv_verts_connected` variable. */
|
||||
uv_face->attached_frep = frep_arr[0];
|
||||
}
|
||||
else if (frep) {
|
||||
uv_face->attached_frep = frep;
|
||||
}
|
||||
|
||||
BLI_ghash_insert(bp->uv_face_hash, fnew, uv_face);
|
||||
return uv_face;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UV vert map with new loops (`BMLoop`) from a face (`uv_face->f`) to keep track of proper
|
||||
* UV connectivity. This data will help with merging UV verts later. Loops stored in the same UV
|
||||
* vert bucket will be merged together (their UV positions).
|
||||
*/
|
||||
static void update_uv_vert_map(BevelParams *bp,
|
||||
UVFace *uv_face,
|
||||
BMVert *bv,
|
||||
Map<BMVert *, BMVert *> *nv_bv_map)
|
||||
{
|
||||
if (!uv_face || !uv_face->attached_frep) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (UVVertMap &uv_vert_map : bp->uv_vert_maps) {
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
BM_ITER_ELEM (l, &iter, uv_face->f, BM_LOOPS_OF_FACE) {
|
||||
Vector<UVVertBucket> *uv_vert_buckets = uv_vert_map.lookup_ptr(l->v);
|
||||
if (!uv_vert_buckets) {
|
||||
/* New vertex (and a corresponding loop) needs to be registered. No need for further UV
|
||||
* connectivity search, we just create a new bucket and move on. */
|
||||
uv_vert_map.add(l->v, Vector<UVVertBucket>{{l}});
|
||||
continue;
|
||||
}
|
||||
|
||||
/* `orig_v` should always point to a vertex which takes part in a bevel operation and comes
|
||||
* from the original mesh. This vertex is equivalent to what is stored in the `BevVert::v`
|
||||
* field. */
|
||||
BMVert *orig_v = nv_bv_map ? nv_bv_map->lookup(l->v) : bv;
|
||||
BMLoop *orig_l = BM_face_vert_share_loop(uv_face->attached_frep, orig_v);
|
||||
BLI_assert(orig_l != nullptr);
|
||||
|
||||
bool is_bucket_found = false;
|
||||
BMIter iter2;
|
||||
BMLoop *l2;
|
||||
BM_ITER_ELEM (l2, &iter2, l->v, BM_LOOPS_OF_VERT) {
|
||||
if (l == l2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UVFace *uv_face2 = find_uv_face(bp, l2->f);
|
||||
if (!uv_face2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BMLoop *orig_l2 = BM_face_vert_share_loop(uv_face2->attached_frep, orig_v);
|
||||
BLI_assert(orig_l2 != nullptr);
|
||||
|
||||
bool is_orig_uv_verts_connected = false;
|
||||
Vector<UVVertBucket> &orig_uv_vert_buckets = uv_vert_map.lookup(orig_v);
|
||||
for (UVVertBucket &orig_uv_vert_bucket : orig_uv_vert_buckets) {
|
||||
if (orig_uv_vert_bucket.contains(orig_l) && orig_uv_vert_bucket.contains(orig_l2)) {
|
||||
is_orig_uv_verts_connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add loop `l` to the existing bucket containing neighboring loop `l2` if either of those
|
||||
* conditions are met:
|
||||
* 1. Neighboring faces (represented by `UVFace` objects) have the same representative face
|
||||
* attached to them.
|
||||
* 2. If representative faces attached to faces containing both loops (`l` and `l2`) are
|
||||
* different but otherwise connected in UV space (`orig_l` and `orig_l2` are
|
||||
* overlapping) for original input mesh. */
|
||||
if (uv_face->attached_frep == uv_face2->attached_frep || is_orig_uv_verts_connected) {
|
||||
for (UVVertBucket &uv_vert_bucket : *uv_vert_buckets) {
|
||||
if (uv_vert_bucket.contains(l2)) {
|
||||
uv_vert_bucket.add(l);
|
||||
is_bucket_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_bucket_found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_bucket_found) {
|
||||
uv_vert_buckets->append(UVVertBucket{l});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine UV vert connectivity based on provided `BMVert *v`. If UV loop data is available,
|
||||
* iterate through loops of vertex `v`, fetching UV position for each loop and checking against
|
||||
* already evaluated ones. If UV coords are overlapping (delta smaller than `STD_UV_CONNECT_LIMIT`)
|
||||
* then add those loops to the same UV vert bucket. If UV verts are not overlapping they will end
|
||||
* up in separate buckets. Those buckets are later utilized during UV merging process, i.e. UV
|
||||
* verts which will end up in the same bucket will be merged together.
|
||||
*/
|
||||
static void determine_uv_vert_connectivity(BevelParams *bp, BMesh *bm, BMVert *v)
|
||||
{
|
||||
int num_uv_layers = CustomData_number_of_layers(&bm->ldata, CD_PROP_FLOAT2);
|
||||
BLI_assert(bp->uv_vert_maps.size() == num_uv_layers);
|
||||
|
||||
for (int i = 0; i < num_uv_layers; ++i) {
|
||||
int uv_data_offset = CustomData_get_n_offset(&bm->ldata, CD_PROP_FLOAT2, i);
|
||||
Vector<UVVertBucket> uv_vert_buckets;
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, uv_data_offset);
|
||||
bool is_overlap_found = false;
|
||||
for (UVVertBucket &uv_vert_bucket : uv_vert_buckets) {
|
||||
for (BMLoop *l2 : uv_vert_bucket) {
|
||||
float *luv2 = BM_ELEM_CD_GET_FLOAT_P(l2, uv_data_offset);
|
||||
if (compare_v2v2(luv, luv2, STD_UV_CONNECT_LIMIT)) {
|
||||
uv_vert_bucket.add(l);
|
||||
is_overlap_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_overlap_found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_overlap_found) {
|
||||
uv_vert_buckets.append(UVVertBucket{l});
|
||||
}
|
||||
}
|
||||
|
||||
/* For now `determine_uv_vert_connectivity` is expected to be run at the beginning of the bevel
|
||||
* operation, determining connectivity for each vertex `v` (from the original mesh). We expect
|
||||
* `bp->uv_vert_maps[i]` to not contain vertex `v` at this point in time. */
|
||||
BLI_assert(bp->uv_vert_maps[i].contains(v) == false);
|
||||
bp->uv_vert_maps[i].add_new(v, uv_vert_buckets);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge UVs based on data gathered in `bm->uv_vert_maps`. If UV verts are in the same bucket,
|
||||
* merge them together. Note that this function exists purely because of imperfections in initial
|
||||
* UV position calculations using interpolation approach (`BM_loop_interp_from_face` function).
|
||||
*/
|
||||
static void bevel_merge_uvs(BevelParams *bp, BMesh *bm)
|
||||
{
|
||||
int num_uv_layers = CustomData_number_of_layers(&bm->ldata, CD_PROP_FLOAT2);
|
||||
BLI_assert(bp->uv_vert_maps.size() == num_uv_layers);
|
||||
|
||||
for (int i = 0; i < num_uv_layers; ++i) {
|
||||
int uv_data_offset = CustomData_get_n_offset(&bm->ldata, CD_PROP_FLOAT2, i);
|
||||
for (Vector<UVVertBucket> &uv_vert_buckets : bp->uv_vert_maps[i].values()) {
|
||||
for (UVVertBucket &uv_vert_bucket : uv_vert_buckets) {
|
||||
/* Using face weights instead of mean average because it produces slightly better results,
|
||||
* although this is purely empirical and subjective. */
|
||||
float weight_sum = 0.0f;
|
||||
float uv[2] = {0.0f, 0.0f};
|
||||
for (BMLoop *l : uv_vert_bucket) {
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, uv_data_offset);
|
||||
float weighted_luv[2] = {0.0f, 0.0f};
|
||||
float face_area = BM_face_calc_area(l->f);
|
||||
mul_v2_v2fl(weighted_luv, luv, face_area);
|
||||
add_v2_v2(uv, weighted_luv);
|
||||
weight_sum += face_area;
|
||||
}
|
||||
if (uv_vert_bucket.size() > 1 && weight_sum > 0.0f) {
|
||||
mul_v2_fl(uv, 1.0f / weight_sum);
|
||||
for (BMLoop *l : uv_vert_bucket) {
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, uv_data_offset);
|
||||
copy_v2_v2(luv, uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a good representative face (for materials, etc.) for faces
|
||||
* created around/near BoundVert v.
|
||||
@@ -675,18 +904,24 @@ static BMFace *boundvert_rep_face(BoundVert *v, BMFace **r_fother)
|
||||
*
|
||||
* \note ALL face creation goes through this function, this is important to keep!
|
||||
*/
|
||||
static BMFace *bev_create_ngon(BMesh *bm,
|
||||
static BMFace *bev_create_ngon(BevelParams *bp,
|
||||
BMesh *bm,
|
||||
BMVert **vert_arr,
|
||||
const int totv,
|
||||
BMFace **face_arr,
|
||||
BMFace *facerep,
|
||||
BMEdge **snap_edge_arr,
|
||||
BMVert *bv,
|
||||
Map<BMVert *, BMVert *> *nv_bv_map,
|
||||
int mat_nr,
|
||||
bool do_interp)
|
||||
{
|
||||
BMFace *f = BM_face_create_verts(bm, vert_arr, totv, facerep, BM_CREATE_NOP, true);
|
||||
if (!f) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if ((facerep || (face_arr && face_arr[0])) && f) {
|
||||
if ((facerep || (face_arr && face_arr[0]))) {
|
||||
BM_elem_attrs_copy(bm, facerep ? facerep : face_arr[0], f);
|
||||
if (do_interp) {
|
||||
int i = 0;
|
||||
@@ -722,20 +957,22 @@ static BMFace *bev_create_ngon(BMesh *bm,
|
||||
}
|
||||
}
|
||||
|
||||
/* Not essential for bevels own internal logic,
|
||||
* this is done so the operator can select newly created geometry. */
|
||||
if (f) {
|
||||
BM_elem_flag_enable(f, BM_ELEM_TAG);
|
||||
BMIter iter;
|
||||
BMEdge *bme;
|
||||
BM_ITER_ELEM (bme, &iter, f, BM_EDGES_OF_FACE) {
|
||||
/* Not essential for bevels own internal logic, this is done so the operator can select newly
|
||||
* created geometry. */
|
||||
flag_out_edge(bm, bme);
|
||||
}
|
||||
}
|
||||
|
||||
if (mat_nr >= 0) {
|
||||
f->mat_nr = short(mat_nr);
|
||||
}
|
||||
|
||||
UVFace *uv_face = register_uv_face(bp, f, facerep, face_arr);
|
||||
update_uv_vert_map(bp, uv_face, bv, nv_bv_map);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -815,6 +1052,26 @@ static void swap_face_components(int *face_component, int totface, int c1, int c
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize `bp->uv_vert_maps` to the size equal to the number of UV layers.
|
||||
*/
|
||||
static void uv_vert_map_init(BevelParams *bp, BMesh *bm)
|
||||
{
|
||||
int num_uv_layers = CustomData_number_of_layers(&bm->ldata, CD_PROP_FLOAT2);
|
||||
bp->uv_vert_maps.clear();
|
||||
bp->uv_vert_maps.resize(num_uv_layers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove vertex `v` from all UV maps in `bp->uv_vert_maps`.
|
||||
*/
|
||||
static void uv_vert_map_pop(BevelParams *bp, BMVert *v)
|
||||
{
|
||||
for (UVVertMap &uv_vert_map : bp->uv_vert_maps) {
|
||||
uv_vert_map.pop_try(v);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Set up the fields of bp->math_layer_info.
|
||||
* We always set has_math_layers to the correct value.
|
||||
@@ -1022,80 +1279,6 @@ static BMFace *choose_rep_face(BevelParams *bp, BMFace **face, int nfaces)
|
||||
#undef VEC_VALUE_LEN
|
||||
}
|
||||
|
||||
/* Merge (using average) all the UV values for loops of v's faces.
|
||||
* Caller should ensure that no seams are violated by doing this. */
|
||||
static void bev_merge_uvs(BMesh *bm, BMVert *v)
|
||||
{
|
||||
int num_of_uv_layers = CustomData_number_of_layers(&bm->ldata, CD_PROP_FLOAT2);
|
||||
|
||||
for (int i = 0; i < num_of_uv_layers; i++) {
|
||||
int cd_loop_uv_offset = CustomData_get_n_offset(&bm->ldata, CD_PROP_FLOAT2, i);
|
||||
|
||||
if (cd_loop_uv_offset == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
float uv[2] = {0.0f, 0.0f};
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, cd_loop_uv_offset);
|
||||
add_v2_v2(uv, luv);
|
||||
n++;
|
||||
}
|
||||
if (n > 1) {
|
||||
mul_v2_fl(uv, 1.0f / float(n));
|
||||
BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, cd_loop_uv_offset);
|
||||
copy_v2_v2(luv, uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge (using average) the UV values for two specific loops of v: those for faces containing v,
|
||||
* and part of faces that share edge bme. */
|
||||
static void bev_merge_edge_uvs(BMesh *bm, BMEdge *bme, BMVert *v)
|
||||
{
|
||||
int num_of_uv_layers = CustomData_number_of_layers(&bm->ldata, CD_PROP_FLOAT2);
|
||||
|
||||
BMLoop *l1 = nullptr;
|
||||
BMLoop *l2 = nullptr;
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
|
||||
if (l->e == bme) {
|
||||
l1 = l;
|
||||
}
|
||||
else if (l->prev->e == bme) {
|
||||
l2 = l;
|
||||
}
|
||||
}
|
||||
if (l1 == nullptr || l2 == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_of_uv_layers; i++) {
|
||||
int cd_loop_uv_offset = CustomData_get_n_offset(&bm->ldata, CD_PROP_FLOAT2, i);
|
||||
|
||||
if (cd_loop_uv_offset == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
float uv[2] = {0.0f, 0.0f};
|
||||
float *luv = BM_ELEM_CD_GET_FLOAT_P(l1, cd_loop_uv_offset);
|
||||
add_v2_v2(uv, luv);
|
||||
luv = BM_ELEM_CD_GET_FLOAT_P(l2, cd_loop_uv_offset);
|
||||
add_v2_v2(uv, luv);
|
||||
mul_v2_fl(uv, 0.5f);
|
||||
luv = BM_ELEM_CD_GET_FLOAT_P(l1, cd_loop_uv_offset);
|
||||
copy_v2_v2(luv, uv);
|
||||
luv = BM_ELEM_CD_GET_FLOAT_P(l2, cd_loop_uv_offset);
|
||||
copy_v2_v2(luv, uv);
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculate coordinates of a point a distance d from v on e->e and return it in slideco. */
|
||||
static void slide_dist(EdgeHalf *e, BMVert *v, float d, float r_slideco[3])
|
||||
{
|
||||
@@ -2652,21 +2835,6 @@ static void set_bound_vert_seams(BevVert *bv, bool mark_seam, bool mark_sharp)
|
||||
}
|
||||
}
|
||||
|
||||
static int count_bound_vert_seams(BevVert *bv)
|
||||
{
|
||||
if (!bv->any_seam) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ans = 0;
|
||||
for (int i = 0; i < bv->edgecount; i++) {
|
||||
if (bv->edges[i].is_seam) {
|
||||
ans++;
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
/* Is e between two faces with a 180 degree angle between their normals? */
|
||||
static bool eh_on_plane(EdgeHalf *e)
|
||||
{
|
||||
@@ -4940,7 +5108,8 @@ static void build_center_ngon(BevelParams *bp, BMesh *bm, BevVert *bv, int mat_n
|
||||
ve.append(nullptr);
|
||||
}
|
||||
} while ((v = v->next) != vm->boundstart);
|
||||
BMFace *f = bev_create_ngon(bm, vv.data(), vv.size(), vf.data(), frep, ve.data(), mat_nr, true);
|
||||
BMFace *f = bev_create_ngon(
|
||||
bp, bm, vv.data(), vv.size(), vf.data(), frep, ve.data(), bv->v, nullptr, mat_nr, true);
|
||||
record_face_kind(bp, f, F_VERT);
|
||||
}
|
||||
|
||||
@@ -5530,32 +5699,13 @@ static void bevel_build_rings(BevelParams *bp, BMesh *bm, BevVert *bv, BoundVert
|
||||
se[2] = se[1] != nullptr ? se[1] : se[3];
|
||||
}
|
||||
}
|
||||
BMFace *r_f = bev_create_ngon(bm, bmvs, 4, fr, nullptr, se, mat_nr, true);
|
||||
BMFace *r_f = bev_create_ngon(
|
||||
bp, bm, bmvs, 4, fr, nullptr, se, bv->v, nullptr, mat_nr, true);
|
||||
record_face_kind(bp, r_f, F_VERT);
|
||||
}
|
||||
}
|
||||
} while ((bndv = bndv->next) != vm->boundstart);
|
||||
|
||||
/* Fix UVs along center lines if even number of segments. */
|
||||
if (!odd) {
|
||||
bndv = vm->boundstart;
|
||||
do {
|
||||
int i = bndv->index;
|
||||
if (!bndv->any_seam) {
|
||||
for (int ring = 1; ring < ns2; ring++) {
|
||||
BMVert *v_uv = mesh_vert(vm, i, ring, ns2)->v;
|
||||
if (v_uv) {
|
||||
bev_merge_uvs(bm, v_uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ((bndv = bndv->next) != vm->boundstart);
|
||||
BMVert *bmv = mesh_vert(vm, 0, ns2, ns2)->v;
|
||||
if (bp->affect_type == BEVEL_AFFECT_VERTICES || count_bound_vert_seams(bv) <= 1) {
|
||||
bev_merge_uvs(bm, bmv);
|
||||
}
|
||||
}
|
||||
|
||||
/* Center ngon. */
|
||||
if (odd) {
|
||||
if (bp->affect_type == BEVEL_AFFECT_EDGES) {
|
||||
@@ -5563,8 +5713,17 @@ static void bevel_build_rings(BevelParams *bp, BMesh *bm, BevVert *bv, BoundVert
|
||||
if (bv->any_seam) {
|
||||
frep = frep_for_center_poly(bp, bv);
|
||||
}
|
||||
BMFace *cen_f = bev_create_ngon(
|
||||
bm, center_verts, n_bndv, center_face_interps, frep, center_edge_snaps, mat_nr, true);
|
||||
BMFace *cen_f = bev_create_ngon(bp,
|
||||
bm,
|
||||
center_verts,
|
||||
n_bndv,
|
||||
center_face_interps,
|
||||
frep,
|
||||
center_edge_snaps,
|
||||
bv->v,
|
||||
nullptr,
|
||||
mat_nr,
|
||||
true);
|
||||
record_face_kind(bp, cen_f, F_VERT);
|
||||
}
|
||||
else {
|
||||
@@ -5698,12 +5857,15 @@ static void bevel_build_cutoff(BevelParams *bp, BMesh *bm, BevVert *bv)
|
||||
|
||||
/* Create the profile cutoff face for this boundvert. */
|
||||
// repface = boundvert_rep_face(bndv, nullptr);
|
||||
bev_create_ngon(bm,
|
||||
bev_create_ngon(bp,
|
||||
bm,
|
||||
face_bmverts,
|
||||
bp->seg + 2 + build_center_face,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
bv->v,
|
||||
nullptr,
|
||||
bp->mat_nr,
|
||||
true);
|
||||
} while ((bndv = bndv->next) != bv->vmesh->boundstart);
|
||||
@@ -5716,7 +5878,8 @@ static void bevel_build_cutoff(BevelParams *bp, BMesh *bm, BevVert *bv)
|
||||
face_bmverts[i] = mesh_vert(bv->vmesh, i, 1, 0)->v;
|
||||
}
|
||||
|
||||
bev_create_ngon(bm, face_bmverts, n_bndv, nullptr, nullptr, nullptr, bp->mat_nr, true);
|
||||
bev_create_ngon(
|
||||
bp, bm, face_bmverts, n_bndv, nullptr, nullptr, nullptr, bv->v, nullptr, bp->mat_nr, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5780,8 +5943,17 @@ static BMFace *bevel_build_poly(BevelParams *bp, BMesh *bm, BevVert *bv)
|
||||
|
||||
BMFace *f;
|
||||
if (n > 2) {
|
||||
f = bev_create_ngon(
|
||||
bm, bmverts.data(), n, bmfaces.data(), repface, bmedges.data(), bp->mat_nr, true);
|
||||
f = bev_create_ngon(bp,
|
||||
bm,
|
||||
bmverts.data(),
|
||||
n,
|
||||
bmfaces.data(),
|
||||
repface,
|
||||
bmedges.data(),
|
||||
bv->v,
|
||||
nullptr,
|
||||
bp->mat_nr,
|
||||
true);
|
||||
record_face_kind(bp, f, F_VERT);
|
||||
}
|
||||
else {
|
||||
@@ -6568,8 +6740,8 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
{
|
||||
bool do_rebuild = false;
|
||||
Vector<BMVert *, BM_DEFAULT_NGON_STACK_SIZE> vv;
|
||||
Vector<BMVert *, BM_DEFAULT_NGON_STACK_SIZE> vv_fix;
|
||||
Vector<BMEdge *, BM_DEFAULT_NGON_STACK_SIZE> ee;
|
||||
Map<BMVert *, BMVert *> nv_bv_map; /* New vertex to the (original) bevel vertex mapping. */
|
||||
|
||||
BMIter liter;
|
||||
BMLoop *l;
|
||||
@@ -6634,10 +6806,9 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
if (!on_profile_start) {
|
||||
vv.append(v->nv.v);
|
||||
ee.append(bme);
|
||||
nv_bv_map.add(v->nv.v, l->v);
|
||||
}
|
||||
while (v != vend) {
|
||||
/* Check for special case: multi-segment 3rd face opposite a beveled edge with no vmesh. */
|
||||
bool corner3special = (vm->mesh_kind == M_NONE && v->ebev != e && v->ebev != eprev);
|
||||
if (go_ccw) {
|
||||
int i = v->index;
|
||||
int kstart, kend;
|
||||
@@ -6659,9 +6830,7 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
if (bmv) {
|
||||
vv.append(bmv);
|
||||
ee.append(bme); /* TODO: Maybe better edge here. */
|
||||
if (corner3special && v->ebev && !bv->any_seam && k != vm->seg) {
|
||||
vv_fix.append(bmv);
|
||||
}
|
||||
nv_bv_map.add(bmv, l->v);
|
||||
}
|
||||
}
|
||||
v = v->next;
|
||||
@@ -6688,9 +6857,7 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
if (bmv) {
|
||||
vv.append(bmv);
|
||||
ee.append(bme);
|
||||
if (corner3special && v->ebev && !bv->any_seam && k != 0) {
|
||||
vv_fix.append(bmv);
|
||||
}
|
||||
nv_bv_map.add(bmv, l->v);
|
||||
}
|
||||
}
|
||||
v = v->prev;
|
||||
@@ -6701,15 +6868,13 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
else {
|
||||
vv.append(l->v);
|
||||
ee.append(l->e);
|
||||
nv_bv_map.add(l->v, l->v); // We keep the old vertex, i.e. mapping to itself.
|
||||
}
|
||||
}
|
||||
if (do_rebuild) {
|
||||
const int64_t n = vv.size();
|
||||
BMFace *f_new = bev_create_ngon(bm, vv.data(), n, nullptr, f, nullptr, -1, true);
|
||||
|
||||
for (int64_t k = 0; k < vv_fix.size(); k++) {
|
||||
bev_merge_uvs(bm, vv_fix[k]);
|
||||
}
|
||||
BMFace *f_new = bev_create_ngon(
|
||||
bp, bm, vv.data(), n, nullptr, f, nullptr, nullptr, &nv_bv_map, -1, true);
|
||||
|
||||
/* Copy attributes from old edges. */
|
||||
BLI_assert(n == ee.size());
|
||||
@@ -6766,25 +6931,23 @@ static bool bev_rebuild_polygon(BMesh *bm, BevelParams *bp, BMFace *f)
|
||||
return do_rebuild;
|
||||
}
|
||||
|
||||
/* All polygons touching v need rebuilding because beveling v has made new vertices. */
|
||||
static void bevel_rebuild_existing_polygons(BMesh *bm, BevelParams *bp, BMVert *v)
|
||||
/* All polygons touching `v` need rebuilding because beveling `v` has made new vertices. */
|
||||
static void bevel_rebuild_existing_polygons(BMesh *bm,
|
||||
BevelParams *bp,
|
||||
BMVert *v,
|
||||
Set<BMFace *> &rebuilt_orig_faces)
|
||||
{
|
||||
void *faces_stack[BM_DEFAULT_ITER_STACK_SIZE];
|
||||
int faces_len, f_index;
|
||||
BMFace **faces = static_cast<BMFace **>(BM_iter_as_arrayN(
|
||||
bm, BM_FACES_OF_VERT, v, &faces_len, faces_stack, BM_DEFAULT_ITER_STACK_SIZE));
|
||||
|
||||
if (LIKELY(faces != nullptr)) {
|
||||
for (f_index = 0; f_index < faces_len; f_index++) {
|
||||
BMFace *f = faces[f_index];
|
||||
BMIter iter;
|
||||
BMFace *f;
|
||||
BM_ITER_ELEM (f, &iter, v, BM_FACES_OF_VERT) {
|
||||
/* Deletion of original mesh faces that are being rebuild is deferred thus we have to perform
|
||||
* a check against `rebuilt_orig_faces` container - previous calls to
|
||||
* `bevel_rebuild_existing_polygons` could have already rebuilt faces touching vertex `v`. */
|
||||
if (!rebuilt_orig_faces.contains(f)) {
|
||||
if (bev_rebuild_polygon(bm, bp, f)) {
|
||||
BM_face_kill(bm, f);
|
||||
rebuilt_orig_faces.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (faces != (BMFace **)faces_stack) {
|
||||
MEM_freeN(faces);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6838,17 +7001,6 @@ static void bevel_reattach_wires(BMesh *bm, BevelParams *bp, BMVert *v)
|
||||
}
|
||||
}
|
||||
|
||||
static void bev_merge_end_uvs(BMesh *bm, BevVert *bv, EdgeHalf *e)
|
||||
{
|
||||
VMesh *vm = bv->vmesh;
|
||||
|
||||
int nseg = e->seg;
|
||||
int i = e->leftv->index;
|
||||
for (int k = 1; k < nseg; k++) {
|
||||
bev_merge_uvs(bm, mesh_vert(vm, i, 0, k)->v);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Is this BevVert the special case of a weld (no vmesh) where there are
|
||||
* four edges total, two are beveled, and the other two are on opposite sides?
|
||||
@@ -6966,6 +7118,11 @@ static void bevel_build_edge_polygons(BMesh *bm, BevelParams *bp, BMEdge *bme)
|
||||
BMVert *verts[4];
|
||||
verts[0] = bmv1;
|
||||
verts[1] = bmv2;
|
||||
|
||||
Map<BMVert *, BMVert *> nv_bv_map; /* New vertex to the (original) bevel vertex mapping. */
|
||||
nv_bv_map.add(verts[0], bv1->v);
|
||||
nv_bv_map.add(verts[1], bv2->v);
|
||||
|
||||
int odd = nseg % 2;
|
||||
int mid = nseg / 2;
|
||||
BMEdge *center_bme = nullptr;
|
||||
@@ -6981,6 +7138,8 @@ static void bevel_build_edge_polygons(BMesh *bm, BevelParams *bp, BMEdge *bme)
|
||||
for (int k = 1; k <= nseg; k++) {
|
||||
verts[3] = mesh_vert(vm1, i1, 0, k)->v;
|
||||
verts[2] = mesh_vert(vm2, i2, 0, nseg - k)->v;
|
||||
nv_bv_map.add(verts[3], bv1->v);
|
||||
nv_bv_map.add(verts[2], bv2->v);
|
||||
BMFace *r_f;
|
||||
if (odd && k == mid + 1) {
|
||||
if (e1->is_seam) {
|
||||
@@ -6996,11 +7155,13 @@ static void bevel_build_edge_polygons(BMesh *bm, BevelParams *bp, BMEdge *bme)
|
||||
edges[0] = edges[1] = bme;
|
||||
edges[2] = edges[3] = nullptr;
|
||||
}
|
||||
r_f = bev_create_ngon(bm, verts, 4, nullptr, f_choice, edges, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, nullptr, f_choice, edges, nullptr, &nv_bv_map, mat_nr, true);
|
||||
}
|
||||
else {
|
||||
/* Straddles but not a seam: interpolate left half in f1, right half in f2. */
|
||||
r_f = bev_create_ngon(bm, verts, 4, faces, f_choice, nullptr, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, faces, f_choice, nullptr, nullptr, &nv_bv_map, mat_nr, true);
|
||||
}
|
||||
}
|
||||
else if (odd && k == center_adj_k && e1->is_seam) {
|
||||
@@ -7020,24 +7181,28 @@ static void bevel_build_edge_polygons(BMesh *bm, BevelParams *bp, BMEdge *bme)
|
||||
edges[2] = edges[3] = nullptr;
|
||||
f_interp = f2;
|
||||
}
|
||||
r_f = bev_create_ngon(bm, verts, 4, nullptr, f_interp, edges, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, nullptr, f_interp, edges, nullptr, &nv_bv_map, mat_nr, true);
|
||||
}
|
||||
else if (!odd && k == mid) {
|
||||
/* Left poly that touches an even center line on right. */
|
||||
BMEdge *edges[4] = {nullptr, nullptr, bme, bme};
|
||||
r_f = bev_create_ngon(bm, verts, 4, nullptr, f1, edges, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, nullptr, f1, edges, nullptr, &nv_bv_map, mat_nr, true);
|
||||
center_bme = BM_edge_exists(verts[2], verts[3]);
|
||||
BLI_assert(center_bme != nullptr);
|
||||
}
|
||||
else if (!odd && k == mid + 1) {
|
||||
/* Right poly that touches an even center line on left. */
|
||||
BMEdge *edges[4] = {bme, bme, nullptr, nullptr};
|
||||
r_f = bev_create_ngon(bm, verts, 4, nullptr, f2, edges, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, nullptr, f2, edges, nullptr, &nv_bv_map, mat_nr, true);
|
||||
}
|
||||
else {
|
||||
/* Doesn't cross or touch the center line, so interpolate in appropriate f1 or f2. */
|
||||
BMFace *f = (k <= mid) ? f1 : f2;
|
||||
r_f = bev_create_ngon(bm, verts, 4, nullptr, f, nullptr, mat_nr, true);
|
||||
r_f = bev_create_ngon(
|
||||
bp, bm, verts, 4, nullptr, f, nullptr, nullptr, &nv_bv_map, mat_nr, true);
|
||||
}
|
||||
record_face_kind(bp, r_f, F_EDGE);
|
||||
/* Tag the long edges: those out of verts[0] and verts[2]. */
|
||||
@@ -7051,23 +7216,6 @@ static void bevel_build_edge_polygons(BMesh *bm, BevelParams *bp, BMEdge *bme)
|
||||
verts[0] = verts[3];
|
||||
verts[1] = verts[2];
|
||||
}
|
||||
if (!odd) {
|
||||
if (!e1->is_seam) {
|
||||
bev_merge_edge_uvs(bm, center_bme, mesh_vert(vm1, i1, 0, mid)->v);
|
||||
}
|
||||
if (!e2->is_seam) {
|
||||
bev_merge_edge_uvs(bm, center_bme, mesh_vert(vm2, i2, 0, mid)->v);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix UVs along end edge joints. A NOP unless other side built already. */
|
||||
/* TODO: If some seam, may want to do selective merge. */
|
||||
if (!bv1->any_seam && bv1->vmesh->mesh_kind == M_NONE) {
|
||||
bev_merge_end_uvs(bm, bv1, e1);
|
||||
}
|
||||
if (!bv2->any_seam && bv2->vmesh->mesh_kind == M_NONE) {
|
||||
bev_merge_end_uvs(bm, bv2, e2);
|
||||
}
|
||||
|
||||
/* Copy edge data to first and last edge. */
|
||||
BMEdge *bme1 = BM_edge_exists(bmv1, bmv2);
|
||||
@@ -7552,8 +7700,7 @@ static float geometry_collide_offset(BevelParams *bp, EdgeHalf *eb)
|
||||
* The intersection of these two corner vectors is the collapse point.
|
||||
* The length of edge B divided by the projection of these vectors onto edge B
|
||||
* is the number of 'offsets' that can be accommodated. */
|
||||
float offsets_projected_on_B = safe_divide(ka + cos1 * kb, sin1) +
|
||||
safe_divide(kc + cos2 * kb, sin2);
|
||||
float offsets_projected_on_B = (ka + cos1 * kb) / sin1 + (kc + cos2 * kb) / sin2;
|
||||
if (offsets_projected_on_B > BEVEL_EPSILON) {
|
||||
offsets_projected_on_B = bp->offset * (len_v3v3(vb->co, vc->co) / offsets_projected_on_B);
|
||||
if (offsets_projected_on_B > BEVEL_EPSILON) {
|
||||
@@ -7803,8 +7950,10 @@ void BM_mesh_bevel(BMesh *bm,
|
||||
|
||||
bp.face_hash = BLI_ghash_ptr_new(__func__);
|
||||
BLI_ghash_flag_set(bp.face_hash, GHASH_FLAG_ALLOW_DUPES);
|
||||
bp.uv_face_hash = BLI_ghash_ptr_new(__func__);
|
||||
|
||||
math_layer_info_init(&bp, bm);
|
||||
uv_vert_map_init(&bp, bm);
|
||||
|
||||
/* Analyze input vertices, sorting edges and assigning initial new vertex positions. */
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
@@ -7813,6 +7962,7 @@ void BM_mesh_bevel(BMesh *bm,
|
||||
if (!limit_offset && bv) {
|
||||
build_boundary(&bp, bv, true);
|
||||
}
|
||||
determine_uv_vert_connectivity(&bp, bm, v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7875,20 +8025,28 @@ void BM_mesh_bevel(BMesh *bm,
|
||||
}
|
||||
|
||||
/* Rebuild face polygons around affected vertices. */
|
||||
Set<BMFace *> rebuilt_orig_faces;
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (BM_elem_flag_test(v, BM_ELEM_TAG)) {
|
||||
bevel_rebuild_existing_polygons(bm, &bp, v);
|
||||
bevel_rebuild_existing_polygons(bm, &bp, v, rebuilt_orig_faces);
|
||||
bevel_reattach_wires(bm, &bp, v);
|
||||
}
|
||||
}
|
||||
|
||||
for (BMFace *f : rebuilt_orig_faces) {
|
||||
BM_face_kill(bm, f);
|
||||
}
|
||||
|
||||
BM_ITER_MESH_MUTABLE (v, v_next, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (BM_elem_flag_test(v, BM_ELEM_TAG)) {
|
||||
BLI_assert(find_bevvert(&bp, v) != nullptr);
|
||||
uv_vert_map_pop(&bp, v);
|
||||
BM_vert_kill(bm, v);
|
||||
}
|
||||
}
|
||||
|
||||
bevel_merge_uvs(&bp, bm);
|
||||
|
||||
if (bp.harden_normals) {
|
||||
bevel_harden_normals(&bp, bm);
|
||||
}
|
||||
@@ -7924,6 +8082,7 @@ void BM_mesh_bevel(BMesh *bm,
|
||||
/* Primary free. */
|
||||
BLI_ghash_free(bp.vert_hash, nullptr, nullptr);
|
||||
BLI_ghash_free(bp.face_hash, nullptr, nullptr);
|
||||
BLI_ghash_free(bp.uv_face_hash, nullptr, nullptr);
|
||||
BLI_memarena_free(bp.mem_arena);
|
||||
|
||||
#ifdef BEVEL_DEBUG_TIME
|
||||
|
||||
BIN
tests/files/modeling/bevel_regression.blend
(Stored with Git LFS)
BIN
tests/files/modeling/bevel_regression.blend
(Stored with Git LFS)
Binary file not shown.
BIN
tests/files/modeling/modifiers.blend
(Stored with Git LFS)
BIN
tests/files/modeling/modifiers.blend
(Stored with Git LFS)
Binary file not shown.
@@ -52,7 +52,7 @@ def main():
|
||||
SpecMeshTest('Cube_test_11', 'Cube_test', 'Cube_result_11',
|
||||
[OperatorSpecEditMode('bevel',
|
||||
{'offset': 0.4, 'segments': 5, 'profile': 1.0}, 'EDGE', {8, 10, 7}, )]),
|
||||
SpecMeshTest("test 12", 'Cube_test', 'Cube_result_12',
|
||||
SpecMeshTest("Cube_test_12", 'Cube_test', 'Cube_result_12',
|
||||
[OperatorSpecEditMode('bevel',
|
||||
{'offset': 0.4, 'segments': 8}, 'EDGE',
|
||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, )]),
|
||||
@@ -217,7 +217,7 @@ def main():
|
||||
SpecMeshTest('Tray', 'Tray', 'Tray_result_1',
|
||||
[OperatorSpecEditMode('bevel',
|
||||
{'offset': 0.01, 'segments': 2}, 'EDGE', {0, 1, 6, 7, 12, 14, 16, 17}, )]),
|
||||
SpecMeshTest("test 73", 'Bumptop', 'Bumptop_result_1',
|
||||
SpecMeshTest("Bumptop", 'Bumptop', 'Bumptop_result_1',
|
||||
[OperatorSpecEditMode('bevel',
|
||||
{'offset': 0.1, 'segments': 4}, 'EDGE',
|
||||
{33, 4, 38, 8, 41, 10, 42, 12, 14, 17, 24, 31}, )]),
|
||||
@@ -228,7 +228,7 @@ def main():
|
||||
[OperatorSpecEditMode('bevel',
|
||||
{'offset': 0.05, 'segments': 2}, 'EDGE', {19, 20, 23, 15}, )]),
|
||||
# 75
|
||||
SpecMeshTest("test 77", 'Cube_hn_test', 'Cube_hn_result_1',
|
||||
SpecMeshTest("Cube_hn_test", 'Cube_hn_test', 'Cube_hn_result_1',
|
||||
[OperatorSpecEditMode('bevel', {'offset': 0.2, 'harden_normals': True}, 'EDGE', {8}, )]),
|
||||
SpecMeshTest('Blocksteps_test_1', 'Blocksteps_test', 'Blocksteps_result_1',
|
||||
[OperatorSpecEditMode('bevel',
|
||||
|
||||
Reference in New Issue
Block a user