Merged changes in the trunk up to revision 51448.
Conflicts resolved: source/blender/blenkernel/CMakeLists.txt source/blender/blenloader/intern/readfile.c source/blender/editors/mesh/editmesh_tools.c source/blender/makesrna/intern/rna_main_api.c
This commit is contained in:
@@ -233,7 +233,7 @@ void bmo_bevel_exec(BMesh *bm, BMOperator *op)
|
||||
}
|
||||
|
||||
#if 0
|
||||
//a bit of cleaner code that, alas, doens't work.
|
||||
/* a bit of cleaner code that, alas, doens't work. */
|
||||
/* build edge tag */
|
||||
BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
|
||||
if (BMO_elem_flag_test(bm, e->v1, BEVEL_FLAG) || BMO_elem_flag_test(bm, e->v2, BEVEL_FLAG)) {
|
||||
|
||||
@@ -199,7 +199,7 @@ void bmo_join_triangles_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
BMIter iter, liter;
|
||||
BMOIter siter;
|
||||
BMFace *f1, *f2;
|
||||
BMFace *f;
|
||||
BMLoop *l;
|
||||
BMEdge *e;
|
||||
BLI_array_declare(jedges);
|
||||
@@ -213,15 +213,16 @@ void bmo_join_triangles_exec(BMesh *bm, BMOperator *op)
|
||||
int i, totedge;
|
||||
|
||||
/* flag all edges of all input face */
|
||||
BMO_ITER (f1, &siter, bm, op, "faces", BM_FACE) {
|
||||
BMO_elem_flag_enable(bm, f1, FACE_INPUT);
|
||||
BM_ITER_ELEM (l, &liter, f1, BM_LOOPS_OF_FACE) {
|
||||
BMO_ITER (f, &siter, bm, op, "faces", BM_FACE) {
|
||||
BMO_elem_flag_enable(bm, f, FACE_INPUT);
|
||||
BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
|
||||
BMO_elem_flag_enable(bm, l->e, EDGE_MARK);
|
||||
}
|
||||
}
|
||||
|
||||
/* unflag edges that are invalid; e.g. aren't surrounded by triangle */
|
||||
BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
|
||||
BMFace *f1, *f2;
|
||||
if (!BMO_elem_flag_test(bm, e, EDGE_MARK))
|
||||
continue;
|
||||
|
||||
@@ -300,6 +301,8 @@ void bmo_join_triangles_exec(BMesh *bm, BMOperator *op)
|
||||
}
|
||||
|
||||
BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
|
||||
BMFace *f1, *f2;
|
||||
|
||||
if (!BMO_elem_flag_test(bm, e, EDGE_CHOSEN))
|
||||
continue;
|
||||
|
||||
@@ -310,6 +313,8 @@ void bmo_join_triangles_exec(BMesh *bm, BMOperator *op)
|
||||
|
||||
BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
|
||||
if (BMO_elem_flag_test(bm, e, EDGE_MARK)) {
|
||||
BMFace *f1, *f2;
|
||||
|
||||
/* ok, this edge wasn't merged, check if it's
|
||||
* in a 2-tri-pair island, and if so merg */
|
||||
|
||||
|
||||
663
source/blender/bmesh/operators/bmo_symmetrize.c
Normal file
663
source/blender/bmesh/operators/bmo_symmetrize.c
Normal file
@@ -0,0 +1,663 @@
|
||||
/*
|
||||
* ***** BEGIN GPL LICENSE BLOCK *****
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* Contributor(s): Nicholas Bishop
|
||||
*
|
||||
* ***** END GPL LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "BLI_array.h"
|
||||
#include "BLI_math.h"
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
#include "bmesh.h"
|
||||
#include "intern/bmesh_operators_private.h"
|
||||
|
||||
enum {
|
||||
SYMM_OUTPUT_GEOM = (1 << 0)
|
||||
};
|
||||
|
||||
/* Note: don't think there's much need to make these user-adjustable? */
|
||||
#define SYMM_AXIS_THRESHOLD 0.00002f
|
||||
#define SYMM_VERT_THRESHOLD 0.00002f
|
||||
|
||||
typedef enum {
|
||||
/* Coordinate lies on the side being copied from */
|
||||
SYMM_SIDE_KEEP,
|
||||
/* Coordinate lies on the side being copied from and within the
|
||||
* axis threshold */
|
||||
SYMM_SIDE_AXIS,
|
||||
/* Coordinate lies on the side being copied to */
|
||||
SYMM_SIDE_KILL
|
||||
} SymmSide;
|
||||
|
||||
typedef struct {
|
||||
BMesh *bm;
|
||||
BMOperator *op;
|
||||
|
||||
int axis;
|
||||
BMO_SymmDirection direction;
|
||||
|
||||
/* Maps from input vertices to their mirrors. If the vertex
|
||||
* doesn't have a mirror, it's not in this map. If the vertex is
|
||||
* within the axis threshold, it's mapped to itself. */
|
||||
GHash *vert_symm_map;
|
||||
|
||||
/* Edges that cross the symmetry plane and are asymmetric get
|
||||
* split. This map goes from input edges to output vertices. If an
|
||||
* edge is not split, it's not in this map. */
|
||||
GHash *edge_split_map;
|
||||
} Symm;
|
||||
|
||||
/* Return which side the coordinate lies on */
|
||||
static SymmSide symm_co_side(const Symm *symm,
|
||||
const float *co)
|
||||
{
|
||||
float comp = co[symm->axis];
|
||||
if (ELEM3(symm->direction,
|
||||
BMO_SYMMETRIZE_NEGATIVE_X,
|
||||
BMO_SYMMETRIZE_NEGATIVE_Y,
|
||||
BMO_SYMMETRIZE_NEGATIVE_Z))
|
||||
{
|
||||
comp = -comp;
|
||||
}
|
||||
|
||||
if (comp >= 0) {
|
||||
if (comp < SYMM_AXIS_THRESHOLD)
|
||||
return SYMM_SIDE_AXIS;
|
||||
else
|
||||
return SYMM_SIDE_KEEP;
|
||||
}
|
||||
else
|
||||
return SYMM_SIDE_KILL;
|
||||
}
|
||||
|
||||
/* Output vertices and the vert_map array */
|
||||
static void symm_verts_mirror(Symm *symm)
|
||||
{
|
||||
BMOIter oiter;
|
||||
BMVert *src_v, *dst_v;
|
||||
|
||||
symm->vert_symm_map = BLI_ghash_ptr_new(AT);
|
||||
|
||||
BMO_ITER (src_v, &oiter, symm->bm, symm->op, "input", BM_VERT) {
|
||||
SymmSide side = symm_co_side(symm, src_v->co);
|
||||
float co[3];
|
||||
|
||||
switch (side) {
|
||||
case SYMM_SIDE_KEEP:
|
||||
/* The vertex is outside the axis area; output its mirror */
|
||||
copy_v3_v3(co, src_v->co);
|
||||
co[symm->axis] = -co[symm->axis];
|
||||
|
||||
dst_v = BM_vert_create(symm->bm, co, src_v);
|
||||
BMO_elem_flag_enable(symm->bm, dst_v, SYMM_OUTPUT_GEOM);
|
||||
BLI_ghash_insert(symm->vert_symm_map, src_v, dst_v);
|
||||
break;
|
||||
|
||||
case SYMM_SIDE_AXIS:
|
||||
/* The vertex is within the axis area, snap to center */
|
||||
src_v->co[symm->axis] = 0;
|
||||
/* Vertex isn't copied, map to itself */
|
||||
BLI_ghash_insert(symm->vert_symm_map, src_v, src_v);
|
||||
break;
|
||||
|
||||
case SYMM_SIDE_KILL:
|
||||
/* The vertex does not lie in the half-space being
|
||||
* copied from, nothing to do */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int symm_edge_crosses_axis(const Symm *symm, const BMEdge *e)
|
||||
{
|
||||
const int sides[2] = {symm_co_side(symm, e->v1->co),
|
||||
symm_co_side(symm, e->v2->co)};
|
||||
|
||||
return ((sides[0] != SYMM_SIDE_AXIS) &&
|
||||
(sides[1] != SYMM_SIDE_AXIS) &&
|
||||
(sides[0] != sides[1]));
|
||||
}
|
||||
|
||||
/* Output edge split vertices for asymmetric edges and the edge_splits
|
||||
* mapping array */
|
||||
static void symm_split_asymmetric_edges(Symm *symm)
|
||||
{
|
||||
BMOIter oiter;
|
||||
BMEdge *e;
|
||||
|
||||
symm->edge_split_map = BLI_ghash_ptr_new(AT);
|
||||
|
||||
BMO_ITER (e, &oiter, symm->bm, symm->op, "input", BM_EDGE) {
|
||||
float flipped[3];
|
||||
|
||||
copy_v3_v3(flipped, e->v1->co);
|
||||
flipped[symm->axis] = -flipped[symm->axis];
|
||||
|
||||
if (symm_edge_crosses_axis(symm, e) &&
|
||||
(!compare_v3v3(e->v2->co, flipped, SYMM_VERT_THRESHOLD)))
|
||||
{
|
||||
/* Endpoints lie on opposite sides and are asymmetric */
|
||||
|
||||
BMVert *v;
|
||||
float lambda = 0, edge_dir[3], co[3];
|
||||
float plane_co[3][3][3] = {
|
||||
/* axis == 0 */
|
||||
{{0, 0, 0}, {0, 1, 0}, {0, 0, 1}},
|
||||
/* axis == 1 */
|
||||
{{0, 0, 0}, {1, 0, 0}, {0, 0, 1}},
|
||||
/* axis == 2 */
|
||||
{{0, 0, 0}, {1, 0, 0}, {0, 1, 0}},
|
||||
};
|
||||
int r;
|
||||
|
||||
/* Find intersection of edge with symmetry plane */
|
||||
sub_v3_v3v3(edge_dir, e->v2->co, e->v1->co);
|
||||
normalize_v3(edge_dir);
|
||||
r = isect_ray_plane_v3(e->v1->co,
|
||||
edge_dir,
|
||||
plane_co[symm->axis][0],
|
||||
plane_co[symm->axis][1],
|
||||
plane_co[symm->axis][2],
|
||||
&lambda, TRUE);
|
||||
BLI_assert(r);
|
||||
|
||||
madd_v3_v3v3fl(co, e->v1->co, edge_dir, lambda);
|
||||
co[symm->axis] = 0;
|
||||
|
||||
/* Edge is asymmetric, split it with a new vertex */
|
||||
v = BM_vert_create(symm->bm, co, e->v1);
|
||||
BMO_elem_flag_enable(symm->bm, v, SYMM_OUTPUT_GEOM);
|
||||
BLI_ghash_insert(symm->edge_split_map, e, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void symm_mirror_edges(Symm *symm)
|
||||
{
|
||||
BMOIter oiter;
|
||||
BMEdge *e;
|
||||
|
||||
BMO_ITER (e, &oiter, symm->bm, symm->op, "input", BM_EDGE) {
|
||||
BMVert *v1 = NULL, *v2 = NULL;
|
||||
BMEdge *e_new;
|
||||
|
||||
v1 = BLI_ghash_lookup(symm->vert_symm_map, e->v1);
|
||||
v2 = BLI_ghash_lookup(symm->vert_symm_map, e->v2);
|
||||
|
||||
if (v1 && v2) {
|
||||
e_new = BM_edge_create(symm->bm, v1, v2, e, TRUE);
|
||||
BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
|
||||
}
|
||||
else if (v1 || v2) {
|
||||
if (BLI_ghash_haskey(symm->edge_split_map, e)) {
|
||||
BMVert *v_split = BLI_ghash_lookup(symm->edge_split_map, e);
|
||||
|
||||
/* Output the keep side of the split edge */
|
||||
if (!v1) {
|
||||
e_new = BM_edge_create(symm->bm, v_split, e->v2, e, TRUE);
|
||||
BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
|
||||
v1 = v_split;
|
||||
}
|
||||
else {
|
||||
e_new = BM_edge_create(symm->bm, e->v1, v_split, e, TRUE);
|
||||
BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
|
||||
v2 = v_split;
|
||||
}
|
||||
|
||||
/* Output the kill side of the split edge */
|
||||
e_new = BM_edge_create(symm->bm, v1, v2, e, TRUE);
|
||||
BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/****************************** SymmPoly ******************************/
|
||||
|
||||
typedef struct {
|
||||
/* Indices into the source mvert array (or -1 if not in that array) */
|
||||
BMVert **src_verts;
|
||||
/* Indices into the destination mvert array, these are vertices
|
||||
* created by an edge split (-1 for vertices not created by edge
|
||||
* split) */
|
||||
BMVert **edge_verts;
|
||||
|
||||
/* Number of vertices in the polygon */
|
||||
int len;
|
||||
|
||||
/* True only if none of the polygon's edges were split */
|
||||
int already_symmetric;
|
||||
} SymmPoly;
|
||||
|
||||
static void symm_poly_with_splits(const Symm *symm,
|
||||
BMFace *f,
|
||||
SymmPoly *out)
|
||||
{
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
int i;
|
||||
|
||||
/* Count vertices and check for edge splits */
|
||||
out->len = f->len;
|
||||
out->already_symmetric = TRUE;
|
||||
BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
|
||||
if (BLI_ghash_haskey(symm->edge_split_map, l->e)) {
|
||||
out->len++;
|
||||
out->already_symmetric = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
|
||||
BMVert *split = BLI_ghash_lookup(symm->edge_split_map, l->e);
|
||||
|
||||
out->src_verts[i] = l->v;
|
||||
out->edge_verts[i] = NULL;
|
||||
i++;
|
||||
|
||||
if (split) {
|
||||
out->src_verts[i] = NULL;
|
||||
out->edge_verts[i] = split;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const float *symm_poly_co(const SymmPoly *sp, int v)
|
||||
{
|
||||
if (sp->src_verts[v])
|
||||
return sp->src_verts[v]->co;
|
||||
else if (sp->edge_verts[v])
|
||||
return sp->edge_verts[v]->co;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static SymmSide symm_poly_co_side(const Symm *symm,
|
||||
const SymmPoly *sp,
|
||||
int v)
|
||||
{
|
||||
return symm_co_side(symm, symm_poly_co(sp, v));
|
||||
}
|
||||
|
||||
/* Return the index of the vertex in the destination array at corner
|
||||
* 'v' of the polygon, or -1 if not in that array */
|
||||
static BMVert *symm_poly_dst(const SymmPoly *sp, int v)
|
||||
{
|
||||
if (sp->edge_verts[v])
|
||||
return sp->edge_verts[v];
|
||||
else if (sp->src_verts[v])
|
||||
return sp->src_verts[v];
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Same as above, but returns the index of the mirror if available, or
|
||||
* the same index if on the axis, or -1 otherwise */
|
||||
static BMVert *symm_poly_mirror_dst(const Symm *symm,
|
||||
const SymmPoly *sp,
|
||||
int v)
|
||||
{
|
||||
if (sp->edge_verts[v])
|
||||
return sp->edge_verts[v];
|
||||
else if (sp->src_verts[v]) {
|
||||
if (BLI_ghash_haskey(symm->vert_symm_map, sp->src_verts[v]))
|
||||
return BLI_ghash_lookup(symm->vert_symm_map, sp->src_verts[v]);
|
||||
else
|
||||
return sp->src_verts[v];
|
||||
}
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int symm_poly_next_crossing(const Symm *symm,
|
||||
const SymmPoly *sp,
|
||||
int start,
|
||||
int *l1,
|
||||
int *l2)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < sp->len; i++) {
|
||||
(*l1) = (start + i) % sp->len;
|
||||
(*l2) = ((*l1) + 1) % sp->len;
|
||||
|
||||
if ((symm_poly_co_side(symm, sp, *l1) == SYMM_SIDE_KILL) ^
|
||||
(symm_poly_co_side(symm, sp, *l2) == SYMM_SIDE_KILL))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
BLI_assert(!"symm_poly_next_crossing failed");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static BMFace *symm_face_create_v(BMesh *bm, BMVert **fv, BMEdge **fe, int len)
|
||||
{
|
||||
BMFace *f_new;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
int j = (i + 1) % len;
|
||||
fe[i] = BM_edge_exists(fv[i], fv[j]);
|
||||
if (!fe[i]) {
|
||||
fe[i] = BM_edge_create(bm, fv[i], fv[j], NULL, FALSE);
|
||||
BMO_elem_flag_enable(bm, fe[i], SYMM_OUTPUT_GEOM);
|
||||
}
|
||||
}
|
||||
f_new = BM_face_create(bm, fv, fe, len, TRUE);
|
||||
BM_face_select_set(bm, f_new, TRUE);
|
||||
BMO_elem_flag_enable(bm, f_new, SYMM_OUTPUT_GEOM);
|
||||
return f_new;
|
||||
}
|
||||
|
||||
static void symm_mesh_output_poly_zero_splits(Symm *symm,
|
||||
SymmPoly *sp,
|
||||
BMVert **fv,
|
||||
BMEdge **fe,
|
||||
int segment_len,
|
||||
int start)
|
||||
{
|
||||
int i, j;
|
||||
|
||||
j = 0;
|
||||
|
||||
/* Output the keep side of the input polygon */
|
||||
for (i = 0; i < segment_len; i++) {
|
||||
const int offset = (start + i) % sp->len;
|
||||
BLI_assert(sp->src_verts[offset]);
|
||||
fv[j++] = sp->src_verts[offset];
|
||||
}
|
||||
|
||||
/* Output the kill side of the polygon */
|
||||
for (i = segment_len - 1; i >= 0; i--) {
|
||||
const int offset = (start + i) % sp->len;
|
||||
|
||||
if (symm_poly_co_side(symm, sp, offset) == SYMM_SIDE_KEEP) {
|
||||
BLI_assert(sp->src_verts[offset]);
|
||||
fv[j++] = BLI_ghash_lookup(symm->vert_symm_map,
|
||||
sp->src_verts[offset]);
|
||||
}
|
||||
}
|
||||
|
||||
symm_face_create_v(symm->bm, fv, fe, j);
|
||||
}
|
||||
|
||||
static void symm_mesh_output_poly_with_splits(Symm *symm,
|
||||
SymmPoly *sp,
|
||||
BMVert **fv,
|
||||
BMEdge **fe,
|
||||
int segment_len,
|
||||
int start)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Output the keep side of the input polygon */
|
||||
|
||||
for (i = 0; i < segment_len; i++) {
|
||||
const int offset = (start + i) % sp->len;
|
||||
BMVert *v = symm_poly_dst(sp, offset);
|
||||
|
||||
BLI_assert(v);
|
||||
|
||||
fv[i] = v;
|
||||
}
|
||||
|
||||
symm_face_create_v(symm->bm, fv, fe, segment_len);
|
||||
|
||||
/* Output the kill side of the input polygon */
|
||||
|
||||
for (i = 0; i < segment_len; i++) {
|
||||
const int offset = (start + i) % sp->len;
|
||||
BMVert *v = symm_poly_mirror_dst(symm, sp, offset);
|
||||
|
||||
fv[segment_len - i - 1] = v;
|
||||
|
||||
}
|
||||
|
||||
symm_face_create_v(symm->bm, fv, fe, segment_len);
|
||||
}
|
||||
|
||||
static void symm_mirror_polygons(Symm *symm)
|
||||
{
|
||||
BMOIter oiter;
|
||||
BMFace *f;
|
||||
BMVert **pv = NULL;
|
||||
BMVert **fv = NULL;
|
||||
BMEdge **fe = NULL;
|
||||
BLI_array_declare(pv);
|
||||
BLI_array_declare(fv);
|
||||
BLI_array_declare(fe);
|
||||
|
||||
BMO_ITER (f, &oiter, symm->bm, symm->op, "input", BM_FACE) {
|
||||
BMIter iter;
|
||||
BMLoop *l;
|
||||
int mirror_all = TRUE, ignore_all = TRUE;
|
||||
|
||||
/* Check if entire polygon can be mirrored or ignored */
|
||||
BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
|
||||
const SymmSide side = symm_co_side(symm, l->v->co);
|
||||
if (side == SYMM_SIDE_KILL)
|
||||
mirror_all = FALSE;
|
||||
else if (side == SYMM_SIDE_KEEP)
|
||||
ignore_all = FALSE;
|
||||
}
|
||||
|
||||
if (mirror_all) {
|
||||
int i;
|
||||
|
||||
/* Make a mirrored copy of the polygon */
|
||||
|
||||
BLI_array_empty(fv);
|
||||
BLI_array_empty(fe);
|
||||
BLI_array_grow_items(fv, f->len);
|
||||
BLI_array_grow_items(fe, f->len);
|
||||
|
||||
i = f->len;
|
||||
BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
|
||||
i--;
|
||||
|
||||
if (symm_co_side(symm, l->v->co) == SYMM_SIDE_KEEP)
|
||||
fv[i] = BLI_ghash_lookup(symm->vert_symm_map, l->v);
|
||||
else
|
||||
fv[i] = l->v;
|
||||
}
|
||||
|
||||
symm_face_create_v(symm->bm, fv, fe, f->len);
|
||||
}
|
||||
else if (ignore_all) {
|
||||
BM_face_kill(symm->bm, f);
|
||||
}
|
||||
else {
|
||||
SymmPoly sp;
|
||||
int l1, l2, l3, l4;
|
||||
int double_l2, double_l3;
|
||||
int segment_len;
|
||||
|
||||
BLI_array_empty(pv);
|
||||
BLI_array_grow_items(pv, f->len * 4);
|
||||
sp.src_verts = pv;
|
||||
sp.edge_verts = pv + f->len * 2;
|
||||
symm_poly_with_splits(symm, f, &sp);
|
||||
|
||||
/* Find first loop edge crossing the axis */
|
||||
symm_poly_next_crossing(symm, &sp, 0, &l1, &l2);
|
||||
|
||||
/* If crossing isn't kill to keep, find the next one */
|
||||
if (symm_poly_co_side(symm, &sp, l1) != SYMM_SIDE_KILL) {
|
||||
symm_poly_next_crossing(symm, &sp, l2, &l1, &l2);
|
||||
}
|
||||
|
||||
/* Find next crossing (keep to kill) */
|
||||
symm_poly_next_crossing(symm, &sp, l2, &l3, &l4);
|
||||
|
||||
if (l2 == l3)
|
||||
segment_len = 0;
|
||||
else if (l2 < l3)
|
||||
segment_len = l3 - l2 + 1;
|
||||
else
|
||||
segment_len = (sp.len - l2 + 1) + l3;
|
||||
|
||||
double_l2 = symm_poly_co_side(symm, &sp, l2) == SYMM_SIDE_KEEP;
|
||||
double_l3 = symm_poly_co_side(symm, &sp, l3) == SYMM_SIDE_KEEP;
|
||||
|
||||
/* Calculate number of new polygons/loops */
|
||||
if (segment_len == 0) {
|
||||
}
|
||||
else if (sp.already_symmetric) {
|
||||
int new_loops;
|
||||
|
||||
if (double_l2 && double_l3)
|
||||
new_loops = segment_len * 2;
|
||||
else if (!double_l2 && !double_l3)
|
||||
new_loops = segment_len * 2 - 2;
|
||||
else
|
||||
new_loops = segment_len * 2 - 1;
|
||||
|
||||
BLI_array_empty(fv);
|
||||
BLI_array_empty(fe);
|
||||
BLI_array_grow_items(fv, new_loops);
|
||||
BLI_array_grow_items(fe, new_loops);
|
||||
|
||||
symm_mesh_output_poly_zero_splits(symm, &sp,
|
||||
fv, fe,
|
||||
segment_len, l2);
|
||||
BM_face_kill(symm->bm, f);
|
||||
}
|
||||
else if (!double_l2 && !double_l3) {
|
||||
BLI_array_empty(fv);
|
||||
BLI_array_empty(fe);
|
||||
BLI_array_grow_items(fv, segment_len);
|
||||
BLI_array_grow_items(fe, segment_len);
|
||||
|
||||
symm_mesh_output_poly_with_splits(symm, &sp,
|
||||
fv, fe,
|
||||
segment_len,
|
||||
l2);
|
||||
|
||||
BM_face_kill(symm->bm, f);
|
||||
}
|
||||
else {
|
||||
BLI_array_empty(fv);
|
||||
BLI_array_empty(fe);
|
||||
BLI_array_grow_items(fv, segment_len);
|
||||
BLI_array_grow_items(fe, segment_len);
|
||||
|
||||
symm_mesh_output_poly_with_splits(symm, &sp,
|
||||
fv, fe,
|
||||
segment_len,
|
||||
l2);
|
||||
|
||||
BM_face_kill(symm->bm, f);
|
||||
|
||||
/* Output bridge triangle */
|
||||
|
||||
BLI_array_empty(fv);
|
||||
BLI_array_empty(fe);
|
||||
BLI_array_grow_items(fv, 3);
|
||||
BLI_array_grow_items(fe, 3);
|
||||
|
||||
if (double_l2) {
|
||||
fv[0] = symm_poly_dst(&sp, l2);
|
||||
fv[1] = symm_poly_mirror_dst(symm, &sp, l2);
|
||||
fv[2] = symm_poly_dst(&sp, l3);
|
||||
}
|
||||
else if (double_l3) {
|
||||
fv[0] = symm_poly_dst(&sp, l3);
|
||||
fv[1] = symm_poly_mirror_dst(symm, &sp, l3);
|
||||
fv[2] = symm_poly_dst(&sp, l2);
|
||||
}
|
||||
|
||||
BLI_assert(fv[0] && fv[1] && fv[2]);
|
||||
|
||||
symm_face_create_v(symm->bm, fv, fe, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BLI_array_free(pv);
|
||||
BLI_array_free(fv);
|
||||
BLI_array_free(fe);
|
||||
}
|
||||
|
||||
/* Remove unused edges and vertices from the side being copied to */
|
||||
static void symm_kill_unused(Symm *symm)
|
||||
{
|
||||
BMOIter oiter;
|
||||
BMEdge *e;
|
||||
BMVert *v;
|
||||
|
||||
/* Kill unused edges */
|
||||
BMO_ITER (e, &oiter, symm->bm, symm->op, "input", BM_EDGE) {
|
||||
const int crosses = symm_edge_crosses_axis(symm, e);
|
||||
const int symmetric = (crosses &&
|
||||
(!BLI_ghash_haskey(symm->edge_split_map, e)));
|
||||
|
||||
if (((symm_co_side(symm, e->v1->co) == SYMM_SIDE_KILL) ||
|
||||
(symm_co_side(symm, e->v2->co) == SYMM_SIDE_KILL)) &&
|
||||
!symmetric)
|
||||
{
|
||||
/* The edge might be used by a face outside the input set */
|
||||
if (BM_edge_face_count(e) == 0)
|
||||
BM_edge_kill(symm->bm, e);
|
||||
}
|
||||
}
|
||||
|
||||
/* Kill unused vertices */
|
||||
BMO_ITER (v, &oiter, symm->bm, symm->op, "input", BM_VERT) {
|
||||
if (symm_co_side(symm, v->co) == SYMM_SIDE_KILL) {
|
||||
if (BM_vert_edge_count(v) == 0)
|
||||
BM_vert_kill(symm->bm, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void bmo_symmetrize_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
Symm symm;
|
||||
BMO_SymmDirection direction = BMO_slot_int_get(op, "direction");
|
||||
|
||||
symm.bm = bm;
|
||||
symm.op = op;
|
||||
symm.axis = (ELEM(direction,
|
||||
BMO_SYMMETRIZE_NEGATIVE_X,
|
||||
BMO_SYMMETRIZE_POSITIVE_X) ? 0 :
|
||||
ELEM(direction,
|
||||
BMO_SYMMETRIZE_NEGATIVE_Y,
|
||||
BMO_SYMMETRIZE_POSITIVE_Y) ? 1 :
|
||||
ELEM(direction,
|
||||
BMO_SYMMETRIZE_NEGATIVE_Z,
|
||||
BMO_SYMMETRIZE_POSITIVE_Z) ? 2 : 0);
|
||||
symm.direction = direction;
|
||||
|
||||
symm_verts_mirror(&symm);
|
||||
symm_split_asymmetric_edges(&symm);
|
||||
symm_mirror_edges(&symm);
|
||||
symm_mirror_polygons(&symm);
|
||||
symm_kill_unused(&symm);
|
||||
|
||||
BLI_ghash_free(symm.vert_symm_map, NULL, NULL);
|
||||
BLI_ghash_free(symm.edge_split_map, NULL, NULL);
|
||||
|
||||
BMO_slot_buffer_from_enabled_flag(bm, op, "geomout", BM_ALL,
|
||||
SYMM_OUTPUT_GEOM);
|
||||
}
|
||||
348
source/blender/bmesh/operators/bmo_unsubdivide.c
Normal file
348
source/blender/bmesh/operators/bmo_unsubdivide.c
Normal file
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* ***** BEGIN GPL LICENSE BLOCK *****
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* Contributor(s): Campbell Barton
|
||||
*
|
||||
* ***** END GPL LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
/** \file blender/bmesh/operators/bmo_unsubdivide.c
|
||||
* \ingroup bmesh
|
||||
*/
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "BLI_math.h"
|
||||
|
||||
#include "bmesh.h"
|
||||
|
||||
#include "intern/bmesh_operators_private.h" /* own include */
|
||||
|
||||
|
||||
static int bm_vert_dissolve_fan_test(BMVert *v)
|
||||
{
|
||||
/* check if we should walk over these verts */
|
||||
BMIter iter;
|
||||
BMEdge *e;
|
||||
|
||||
unsigned int tot_edge = 0;
|
||||
unsigned int tot_edge_boundary = 0;
|
||||
unsigned int tot_edge_manifold = 0;
|
||||
unsigned int tot_edge_wire = 0;
|
||||
|
||||
BM_ITER_ELEM (e, &iter, v, BM_EDGES_OF_VERT) {
|
||||
if (BM_edge_is_boundary(e)) {
|
||||
tot_edge_boundary++;
|
||||
}
|
||||
else if (BM_edge_is_manifold(e)) {
|
||||
tot_edge_manifold++;
|
||||
}
|
||||
else if (BM_edge_is_wire(e)) {
|
||||
tot_edge_wire++;
|
||||
}
|
||||
tot_edge++;
|
||||
}
|
||||
|
||||
if ((tot_edge == 4) && (tot_edge_boundary == 0) && (tot_edge_manifold == 4)) {
|
||||
return TRUE;
|
||||
}
|
||||
else if ((tot_edge == 3) && (tot_edge_boundary == 0) && (tot_edge_manifold == 3)) {
|
||||
return TRUE;
|
||||
}
|
||||
else if ((tot_edge == 3) && (tot_edge_boundary == 2) && (tot_edge_manifold == 1)) {
|
||||
return TRUE;
|
||||
}
|
||||
else if ((tot_edge == 2) && (tot_edge_wire == 2)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static int bm_vert_dissolve_fan(BMesh *bm, BMVert *v)
|
||||
{
|
||||
/* collapse under 2 conditions.
|
||||
* - vert connects to 4 manifold edges (and 4 faces).
|
||||
* - vert connecrs to 1 manifold edge, 2 boundary edges (and 2 faces).
|
||||
*
|
||||
* This covers boundary verts of a quad grid and center verts.
|
||||
* note that surrounding faces dont have to be quads.
|
||||
*/
|
||||
|
||||
BMIter iter;
|
||||
BMEdge *e;
|
||||
|
||||
unsigned int tot_loop = 0;
|
||||
unsigned int tot_edge = 0;
|
||||
unsigned int tot_edge_boundary = 0;
|
||||
unsigned int tot_edge_manifold = 0;
|
||||
unsigned int tot_edge_wire = 0;
|
||||
|
||||
BM_ITER_ELEM (e, &iter, v, BM_EDGES_OF_VERT) {
|
||||
if (BM_edge_is_boundary(e)) {
|
||||
tot_edge_boundary++;
|
||||
}
|
||||
else if (BM_edge_is_manifold(e)) {
|
||||
tot_edge_manifold++;
|
||||
}
|
||||
else if (BM_edge_is_wire(e)) {
|
||||
tot_edge_wire++;
|
||||
}
|
||||
tot_edge++;
|
||||
}
|
||||
|
||||
if (tot_edge == 2) {
|
||||
/* check for 2 wire verts only */
|
||||
if (tot_edge_wire == 2) {
|
||||
return (BM_vert_collapse_edge(bm, v->e, v, TRUE) != NULL);
|
||||
}
|
||||
}
|
||||
else if (tot_edge == 4) {
|
||||
/* check for 4 faces surrounding */
|
||||
if (tot_edge_boundary == 0 && tot_edge_manifold == 4) {
|
||||
/* good to go! */
|
||||
tot_loop = 4;
|
||||
}
|
||||
}
|
||||
else if (tot_edge == 3) {
|
||||
/* check for 2 faces surrounding at a boundary */
|
||||
if (tot_edge_boundary == 2 && tot_edge_manifold == 1) {
|
||||
/* good to go! */
|
||||
tot_loop = 2;
|
||||
}
|
||||
else if (tot_edge_boundary == 0 && tot_edge_manifold == 3) {
|
||||
/* good to go! */
|
||||
tot_loop = 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (tot_loop) {
|
||||
BMLoop *f_loop[4];
|
||||
unsigned int i;
|
||||
|
||||
/* ensure there are exactly tot_loop loops */
|
||||
BLI_assert(BM_iter_at_index(bm, BM_LOOPS_OF_VERT, v, tot_loop) == NULL);
|
||||
BM_iter_as_array(bm, BM_LOOPS_OF_VERT, v, (void **)f_loop, tot_loop);
|
||||
|
||||
for (i = 0; i < tot_loop; i++) {
|
||||
BMLoop *l = f_loop[i];
|
||||
if (l->f->len > 3) {
|
||||
BLI_assert(l->prev->v != l->next->v);
|
||||
BM_face_split(bm, l->f, l->prev->v, l->next->v, NULL, NULL, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
return BM_vert_dissolve(bm, v);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
enum {
|
||||
VERT_INDEX_DO_COLLAPSE = -1,
|
||||
VERT_INDEX_INIT = 0,
|
||||
VERT_INDEX_IGNORE = 1
|
||||
};
|
||||
|
||||
// #define USE_WALKER /* gives uneven results, disable for now */
|
||||
// #define USE_ALL_VERTS
|
||||
|
||||
/* - BMVert.flag & BM_ELEM_TAG: shows we touched this vert
|
||||
* - BMVert.index == -1: shows we will remove this vert
|
||||
*/
|
||||
void bmo_unsubdivide_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
#ifdef USE_WALKER
|
||||
# define ELE_VERT_TAG 1
|
||||
#else
|
||||
BMVert **vert_seek_a = MEM_mallocN(sizeof(BMVert *) * bm->totvert, __func__);
|
||||
BMVert **vert_seek_b = MEM_mallocN(sizeof(BMVert *) * bm->totvert, __func__);
|
||||
unsigned vert_seek_a_tot = 0;
|
||||
unsigned vert_seek_b_tot = 0;
|
||||
#endif
|
||||
|
||||
BMVert *v;
|
||||
BMIter iter;
|
||||
|
||||
const unsigned int offset = 0;
|
||||
const unsigned int nth = 2;
|
||||
|
||||
const int iterations = maxi(1, BMO_slot_int_get(op, "iterations"));
|
||||
int iter_step;
|
||||
|
||||
#ifdef USE_ALL_VERTS
|
||||
(void)op;
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
BM_elem_flag_enable(v, BM_ELEM_TAG);
|
||||
}
|
||||
#else /* USE_ALL_VERTS */
|
||||
BMOpSlot *vinput = BMO_slot_get(op, "verts");
|
||||
BMVert **vinput_arr = (BMVert **)vinput->data.p;
|
||||
int v_index;
|
||||
|
||||
/* tag verts */
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
BM_elem_flag_disable(v, BM_ELEM_TAG);
|
||||
}
|
||||
for (v_index = 0; v_index < vinput->len; v_index++) {
|
||||
v = vinput_arr[v_index];
|
||||
BM_elem_flag_enable(v, BM_ELEM_TAG);
|
||||
}
|
||||
#endif /* USE_ALL_VERTS */
|
||||
|
||||
|
||||
for (iter_step = 0; iter_step < iterations; iter_step++) {
|
||||
int iter_done;
|
||||
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (BM_elem_flag_test(v, BM_ELEM_TAG) && bm_vert_dissolve_fan_test(v)) {
|
||||
#ifdef USE_WALKER
|
||||
BMO_elem_flag_enable(bm, v, ELE_VERT_TAG);
|
||||
#endif
|
||||
BM_elem_index_set(v, VERT_INDEX_INIT); /* set_dirty! */
|
||||
}
|
||||
else {
|
||||
BM_elem_index_set(v, VERT_INDEX_IGNORE); /* set_dirty! */
|
||||
}
|
||||
}
|
||||
/* done with selecting tagged verts */
|
||||
|
||||
|
||||
/* main loop, keep tagging until we can't tag any more islands */
|
||||
while (TRUE) {
|
||||
#ifdef USE_WALKER
|
||||
BMWalker walker;
|
||||
#else
|
||||
unsigned int depth = 1;
|
||||
unsigned int i;
|
||||
#endif
|
||||
BMVert *v_first = NULL;
|
||||
BMVert *v;
|
||||
|
||||
/* we could avoid iterating from the start each time */
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (v->e && (BM_elem_index_get(v) == VERT_INDEX_INIT)) {
|
||||
#ifdef USE_WALKER
|
||||
if (BMO_elem_flag_test(bm, v, ELE_VERT_TAG))
|
||||
#endif
|
||||
{
|
||||
/* check again incase the topology changed */
|
||||
if (bm_vert_dissolve_fan_test(v)) {
|
||||
v_first = v;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v_first == NULL) {
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_WALKER
|
||||
/* Walk over selected elements starting at active */
|
||||
BMW_init(&walker, bm, BMW_CONNECTED_VERTEX,
|
||||
ELE_VERT_TAG, BMW_MASK_NOP, BMW_MASK_NOP,
|
||||
BMW_FLAG_NOP, /* don't use BMW_FLAG_TEST_HIDDEN here since we want to desel all */
|
||||
BMW_NIL_LAY);
|
||||
|
||||
BLI_assert(walker.order == BMW_BREADTH_FIRST);
|
||||
for (v = BMW_begin(&walker, v_first); v != NULL; v = BMW_step(&walker)) {
|
||||
/* Deselect elements that aren't at "nth" depth from active */
|
||||
if (BM_elem_index_get(v) == VERT_INDEX_INIT) {
|
||||
if ((offset + BMW_current_depth(&walker)) % nth) {
|
||||
/* tag for removal */
|
||||
BM_elem_index_set(v, VERT_INDEX_DO_COLLAPSE); /* set_dirty! */
|
||||
}
|
||||
else {
|
||||
/* works better to allow these verts to be checked again */
|
||||
//BM_elem_index_set(v, VERT_INDEX_IGNORE); /* set_dirty! */
|
||||
}
|
||||
}
|
||||
}
|
||||
BMW_end(&walker);
|
||||
#else
|
||||
|
||||
BM_elem_index_set(v_first, (offset + depth) % nth ? VERT_INDEX_IGNORE : VERT_INDEX_DO_COLLAPSE); /* set_dirty! */
|
||||
|
||||
vert_seek_b_tot = 0;
|
||||
vert_seek_b[vert_seek_b_tot++] = v_first;
|
||||
|
||||
while (TRUE) {
|
||||
BMEdge *e;
|
||||
|
||||
if ((offset + depth) % nth) {
|
||||
vert_seek_a_tot = 0;
|
||||
for (i = 0; i < vert_seek_b_tot; i++) {
|
||||
v = vert_seek_b[i];
|
||||
BLI_assert(BM_elem_index_get(v) == VERT_INDEX_IGNORE);
|
||||
BM_ITER_ELEM (e, &iter, v, BM_EDGES_OF_VERT) {
|
||||
BMVert *v_other = BM_edge_other_vert(e, v);
|
||||
if (BM_elem_index_get(v_other) == VERT_INDEX_INIT) {
|
||||
BM_elem_index_set(v_other, VERT_INDEX_DO_COLLAPSE); /* set_dirty! */
|
||||
vert_seek_a[vert_seek_a_tot++] = v_other;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vert_seek_a_tot == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
vert_seek_b_tot = 0;
|
||||
for (i = 0; i < vert_seek_a_tot; i++) {
|
||||
v = vert_seek_a[i];
|
||||
BLI_assert(BM_elem_index_get(v) == VERT_INDEX_DO_COLLAPSE);
|
||||
BM_ITER_ELEM (e, &iter, v, BM_EDGES_OF_VERT) {
|
||||
BMVert *v_other = BM_edge_other_vert(e, v);
|
||||
if (BM_elem_index_get(v_other) == VERT_INDEX_INIT) {
|
||||
BM_elem_index_set(v_other, VERT_INDEX_IGNORE); /* set_dirty! */
|
||||
vert_seek_b[vert_seek_b_tot++] = v_other;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vert_seek_b_tot == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
#endif /* USE_WALKER */
|
||||
|
||||
}
|
||||
|
||||
/* now we tagged all verts -1 for removal, lets loop over and rebuild faces */
|
||||
iter_done = FALSE;
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (BM_elem_index_get(v) == VERT_INDEX_DO_COLLAPSE) {
|
||||
iter_done |= bm_vert_dissolve_fan(bm, v);
|
||||
}
|
||||
}
|
||||
|
||||
if (iter_done == FALSE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bm->elem_index_dirty |= BM_VERT;
|
||||
|
||||
#ifndef USE_WALKER
|
||||
MEM_freeN(vert_seek_a);
|
||||
MEM_freeN(vert_seek_b);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -1033,10 +1033,9 @@ void bmo_similar_verts_exec(BMesh *bm, BMOperator *op)
|
||||
|
||||
void bmo_rotate_uvs_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
BMOIter fs_iter; /* selected faces iterator */
|
||||
BMFace *fs; /* current face */
|
||||
BMIter l_iter; /* iteration loop */
|
||||
// int n;
|
||||
BMOIter fs_iter; /* selected faces iterator */
|
||||
BMFace *fs; /* current face */
|
||||
BMIter l_iter; /* iteration loop */
|
||||
|
||||
int dir = BMO_slot_int_get(op, "dir");
|
||||
|
||||
@@ -1091,7 +1090,6 @@ void bmo_rotate_uvs_exec(BMesh *bm, BMOperator *op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**************************************************************************** *
|
||||
@@ -1140,10 +1138,9 @@ void bmo_reverse_uvs_exec(BMesh *bm, BMOperator *op)
|
||||
|
||||
void bmo_rotate_colors_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
BMOIter fs_iter; /* selected faces iterator */
|
||||
BMFace *fs; /* current face */
|
||||
BMIter l_iter; /* iteration loop */
|
||||
// int n;
|
||||
BMOIter fs_iter; /* selected faces iterator */
|
||||
BMFace *fs; /* current face */
|
||||
BMIter l_iter; /* iteration loop */
|
||||
|
||||
int dir = BMO_slot_int_get(op, "dir");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user