Files
test/source/blender/editors/uvedit/uvedit_ops.cc

2813 lines
85 KiB
C++

/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup eduv
*/
#include <cmath>
#include <cstdlib>
#include <cstring>
#include "MEM_guardedalloc.h"
#include "DNA_image_types.h"
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "DNA_node_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"
#include "BLI_bounds.hh"
#include "BLI_kdtree.h"
#include "BLI_math_base.hh"
#include "BLI_math_geom.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BLI_utildefines.h"
#include "BLT_translation.hh"
#include "BKE_context.hh"
#include "BKE_customdata.hh"
#include "BKE_editmesh.hh"
#include "BKE_layer.hh"
#include "BKE_main_invariants.hh"
#include "BKE_material.hh"
#include "BKE_mesh_mapping.hh"
#include "BKE_mesh_types.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_image.hh"
#include "ED_mesh.hh"
#include "ED_node.hh"
#include "ED_screen.hh"
#include "ED_uvedit.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "WM_api.hh"
#include "WM_message.hh"
#include "WM_types.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "UI_view2d.hh"
#include "uvedit_intern.hh"
using namespace blender;
/* -------------------------------------------------------------------- */
/** \name State Testing
* \{ */
bool ED_uvedit_test(Object *obedit)
{
BMEditMesh *em;
int ret;
if (!obedit) {
return false;
}
if (obedit->type != OB_MESH) {
return false;
}
em = BKE_editmesh_from_object(obedit);
ret = EDBM_uv_check(em);
return ret;
}
static int UNUSED_FUNCTION(ED_operator_uvmap_mesh)(bContext *C)
{
Object *ob = CTX_data_active_object(C);
if (ob && ob->type == OB_MESH) {
Mesh *mesh = static_cast<Mesh *>(ob->data);
if (CustomData_get_layer(&mesh->corner_data, CD_PROP_FLOAT2) != nullptr) {
return 1;
}
}
return 0;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Object Active Image
* \{ */
static bool is_image_texture_node(bNode *node)
{
return ELEM(node->type_legacy, SH_NODE_TEX_IMAGE, SH_NODE_TEX_ENVIRONMENT);
}
bool ED_object_get_active_image(Object *ob,
int mat_nr,
Image **r_ima,
ImageUser **r_iuser,
const bNode **r_node,
const bNodeTree **r_ntree)
{
Material *ma = DEG_is_evaluated(ob) ? BKE_object_material_get_eval(ob, mat_nr) :
BKE_object_material_get(ob, mat_nr);
bNodeTree *ntree = ma ? ma->nodetree : nullptr;
bNode *node = (ntree) ? bke::node_get_active_texture(*ntree) : nullptr;
if (node && is_image_texture_node(node)) {
if (r_ima) {
*r_ima = (Image *)node->id;
}
if (r_iuser) {
if (node->type_legacy == SH_NODE_TEX_IMAGE) {
*r_iuser = &((NodeTexImage *)node->storage)->iuser;
}
else if (node->type_legacy == SH_NODE_TEX_ENVIRONMENT) {
*r_iuser = &((NodeTexEnvironment *)node->storage)->iuser;
}
else {
*r_iuser = nullptr;
}
}
if (r_node) {
*r_node = node;
}
if (r_ntree) {
*r_ntree = ntree;
}
return true;
}
if (r_ima) {
*r_ima = nullptr;
}
if (r_iuser) {
*r_iuser = nullptr;
}
if (r_node) {
*r_node = node;
}
if (r_ntree) {
*r_ntree = ntree;
}
return false;
}
void ED_object_assign_active_image(Main *bmain, Object *ob, int mat_nr, Image *ima)
{
Material *ma = BKE_object_material_get(ob, mat_nr);
bNode *node = ma ? bke::node_get_active_texture(*ma->nodetree) : nullptr;
if (node && is_image_texture_node(node)) {
node->id = &ima->id;
BKE_main_ensure_invariants(*bmain, ma->nodetree->id);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Live Unwrap Utilities
* \{ */
void uvedit_live_unwrap_update(SpaceImage *sima, Scene *scene, Object *obedit)
{
if (sima && (sima->flag & SI_LIVE_UNWRAP)) {
ED_uvedit_live_unwrap_begin(scene, obedit, nullptr);
ED_uvedit_live_unwrap_re_solve();
ED_uvedit_live_unwrap_end(false);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Geometric Utilities
* \{ */
void ED_uvedit_foreach_uv(const Scene *scene,
BMesh *bm,
const bool skip_invisible,
const bool selected,
FunctionRef<void(float[2])> user_fn)
{
/* Check selection for quick return. */
const bool synced_selection = (scene->toolsettings->uv_flag & UV_FLAG_SELECT_SYNC) != 0;
if (synced_selection && bm->totvertsel == (selected ? 0 : bm->totvert)) {
return;
}
BMFace *efa;
BMLoop *l;
BMIter iter, liter;
const BMUVOffsets offsets = BM_uv_map_offsets_get(bm);
BM_ITER_MESH (efa, &iter, bm, BM_FACES_OF_MESH) {
if (skip_invisible && !uvedit_face_visible_test(scene, efa)) {
continue;
}
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (uvedit_uv_select_test(scene, bm, l, offsets) == selected) {
float *luv = BM_ELEM_CD_GET_FLOAT_P(l, offsets.uv);
user_fn(luv);
}
}
}
}
void ED_uvedit_foreach_uv_multi(const Scene *scene,
const Span<Object *> objects_edit,
const bool skip_invisible,
const bool skip_nonselected,
FunctionRef<void(float[2])> user_fn)
{
for (Object *obedit : objects_edit) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
ED_uvedit_foreach_uv(scene, em->bm, skip_invisible, skip_nonselected, user_fn);
}
}
bool ED_uvedit_minmax_multi(const Scene *scene,
const Span<Object *> objects_edit,
float r_min[2],
float r_max[2])
{
bool changed = false;
INIT_MINMAX2(r_min, r_max);
ED_uvedit_foreach_uv_multi(scene, objects_edit, true, true, [&](float luv[2]) {
minmax_v2v2_v2(r_min, r_max, luv);
changed = true;
});
return changed;
}
void ED_uvedit_select_all(const ToolSettings *ts, BMesh *bm)
{
BMFace *efa;
BMLoop *l;
BMIter iter, liter;
BM_ITER_MESH (efa, &iter, bm, BM_FACES_OF_MESH) {
uvedit_face_select_set_no_sync(ts, bm, efa, true);
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
uvedit_vert_select_set_no_sync(ts, bm, l, true);
uvedit_edge_select_set_no_sync(ts, bm, l, true);
}
}
}
static bool uvedit_median_multi(const Scene *scene, const Span<Object *> objects_edit, float co[2])
{
uint sel = 0;
zero_v2(co);
ED_uvedit_foreach_uv_multi(scene, objects_edit, true, true, [&](float luv[2]) {
add_v2_v2(co, luv);
sel++;
});
mul_v2_fl(co, 1.0f / float(sel));
return (sel != 0);
}
bool ED_uvedit_center_multi(const Scene *scene,
Span<Object *> objects_edit,
float cent[2],
char mode)
{
bool changed = false;
if (mode == V3D_AROUND_CENTER_BOUNDS) { /* bounding box */
float min[2], max[2];
if (ED_uvedit_minmax_multi(scene, objects_edit, min, max)) {
mid_v2_v2v2(cent, min, max);
changed = true;
}
}
else {
if (uvedit_median_multi(scene, objects_edit, cent)) {
changed = true;
}
}
return changed;
}
bool ED_uvedit_center_from_pivot_ex(const SpaceImage *sima,
Scene *scene,
ViewLayer *view_layer,
float r_center[2],
char mode,
bool *r_has_select)
{
bool changed = false;
switch (mode) {
case V3D_AROUND_CURSOR: {
copy_v2_v2(r_center, sima->cursor);
changed = true;
if (r_has_select != nullptr) {
Vector<Object *> objects =
BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
*r_has_select = uvedit_select_is_any_selected_multi(scene, objects);
}
break;
}
default: {
Vector<Object *> objects =
BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
changed = ED_uvedit_center_multi(scene, objects, r_center, mode);
if (r_has_select != nullptr) {
*r_has_select = changed;
}
break;
}
}
return changed;
}
enum class UVMoveType {
Dynamic = 0,
Pixel = 1,
Udim = 2,
};
enum class UVMoveDirection {
X = 0,
Y = 1,
};
static wmOperatorStatus uv_move_on_axis_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
UVMoveType type = UVMoveType(RNA_enum_get(op->ptr, "type"));
UVMoveDirection axis = UVMoveDirection(RNA_enum_get(op->ptr, "axis"));
int distance = RNA_int_get(op->ptr, "distance");
int size[2];
ED_space_image_get_size(sima, &size[0], &size[1]);
float distance_final;
if (type == UVMoveType::Dynamic) {
distance_final = float(distance) / sima->custom_grid_subdiv[int(axis)];
}
else if (type == UVMoveType::Pixel) {
distance_final = float(distance) / size[int(axis)];
}
else {
distance_final = distance;
}
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
if (em->bm->totvertsel == 0) {
continue;
}
ED_uvedit_foreach_uv(
scene, em->bm, true, true, [&axis, &distance_final, &changed](float luv[2]) {
luv[int(axis)] += distance_final;
changed = true;
});
if (changed) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
return OPERATOR_FINISHED;
}
static void UV_OT_move_on_axis(wmOperatorType *ot)
{
static const EnumPropertyItem shift_items[] = {
{int(UVMoveType::Dynamic), "DYNAMIC", 0, "Dynamic", "Move by dynamic grid"},
{int(UVMoveType::Pixel), "PIXEL", 0, "Pixel", "Move by pixel"},
{int(UVMoveType::Udim), "UDIM", 0, "UDIM", "Move by UDIM"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem axis_items[] = {
{int(UVMoveDirection::X), "X", 0, "X axis", "Move vertices on the X axis"},
{int(UVMoveDirection::Y), "Y", 0, "Y axis", "Move vertices on the Y axis"},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Move on Axis";
ot->description = "Move UVs on an axis";
ot->idname = "UV_OT_move_on_axis";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_move_on_axis_exec;
ot->poll = ED_operator_uvedit;
/* properties */
RNA_def_enum(ot->srna, "type", shift_items, int(UVMoveType::Udim), "Type", "Move Type");
RNA_def_enum(
ot->srna, "axis", axis_items, int(UVMoveDirection::X), "Axis", "Axis to move UVs on");
RNA_def_int(ot->srna,
"distance",
1,
INT_MIN,
INT_MAX,
"Distance",
"Distance to move UVs",
INT_MIN,
INT_MAX);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Weld Align Operator
* \{ */
enum eUVWeldAlign {
UV_STRAIGHTEN,
UV_STRAIGHTEN_X,
UV_STRAIGHTEN_Y,
UV_ALIGN_AUTO,
UV_ALIGN_X,
UV_ALIGN_Y,
UV_WELD,
};
enum class UVAlignPositionMode {
Mean = 0,
Min = 1,
Max = 2,
};
static bool uvedit_uv_align_weld(Scene *scene,
BMesh *bm,
const eUVWeldAlign tool,
const float cent[2])
{
bool changed = false;
ED_uvedit_foreach_uv(scene, bm, true, true, [&](float luv[2]) {
if (ELEM(tool, UV_ALIGN_X, UV_WELD)) {
if (luv[0] != cent[0]) {
luv[0] = cent[0];
changed = true;
}
}
if (ELEM(tool, UV_ALIGN_Y, UV_WELD)) {
if (luv[1] != cent[1]) {
luv[1] = cent[1];
changed = true;
}
}
});
return changed;
}
/** Bitwise-or together, then choose loop with highest value. */
enum eUVEndPointPrecedence {
UVEP_INVALID = 0,
UVEP_SELECTED = (1 << 0),
UVEP_PINNED = (1 << 1), /* i.e. Pinned verts are preferred to selected. */
};
ENUM_OPERATORS(eUVEndPointPrecedence, UVEP_PINNED);
static eUVEndPointPrecedence uvedit_line_update_get_precedence(const bool pinned)
{
eUVEndPointPrecedence precedence = UVEP_SELECTED;
if (pinned) {
precedence |= UVEP_PINNED;
}
return precedence;
}
/**
* Helper to find two endpoints (`a` and `b`) which have higher precedence, and are far apart.
* Note that is only a heuristic and won't always find the best two endpoints.
*/
static bool uvedit_line_update_endpoint(const float *luv,
const bool pinned,
float uv_a[2],
eUVEndPointPrecedence *prec_a,
float uv_b[2],
eUVEndPointPrecedence *prec_b)
{
eUVEndPointPrecedence flags = uvedit_line_update_get_precedence(pinned);
float len_sq_a = len_squared_v2v2(uv_a, luv);
float len_sq_b = len_squared_v2v2(uv_b, luv);
/* Caching the value of `len_sq_ab` is unlikely to be faster than recalculating.
* Profile before optimizing. */
float len_sq_ab = len_squared_v2v2(uv_a, uv_b);
if ((*prec_a < flags && 0.0f < len_sq_b) || (*prec_a == flags && len_sq_ab < len_sq_b)) {
*prec_a = flags;
copy_v2_v2(uv_a, luv);
return true;
}
if ((*prec_b < flags && 0.0f < len_sq_a) || (*prec_b == flags && len_sq_ab < len_sq_a)) {
*prec_b = flags;
copy_v2_v2(uv_b, luv);
return true;
}
return false;
}
/**
* Find two end extreme points to specify a line, then straighten `len` elements
* by moving UVs on the X-axis, Y-axis, or the closest point on the line segment.
*/
static bool uvedit_uv_straighten_elements(const UvElement *element,
const int len,
const BMUVOffsets &offsets,
const eUVWeldAlign tool)
{
float uv_start[2];
float uv_end[2];
eUVEndPointPrecedence prec_start = UVEP_INVALID;
eUVEndPointPrecedence prec_end = UVEP_INVALID;
/* Find start and end of line. */
for (int i = 0; i < 10; i++) { /* Heuristic to prevent infinite loop. */
bool update = false;
for (int j = 0; j < len; j++) {
float *luv = BM_ELEM_CD_GET_FLOAT_P(element[j].l, offsets.uv);
bool pinned = BM_ELEM_CD_GET_BOOL(element[j].l, offsets.pin);
update |= uvedit_line_update_endpoint(luv, pinned, uv_start, &prec_start, uv_end, &prec_end);
}
if (!update) {
break;
}
}
if (prec_start == UVEP_INVALID || prec_end == UVEP_INVALID) {
return false; /* Unable to find two endpoints. */
}
float a = 0.0f; /* Similar to "slope". */
eUVWeldAlign tool_local = tool;
if (tool_local == UV_STRAIGHTEN_X) {
if (uv_start[1] == uv_end[1]) {
/* Caution, different behavior outside line segment. */
tool_local = UV_STRAIGHTEN;
}
else {
a = (uv_end[0] - uv_start[0]) / (uv_end[1] - uv_start[1]);
}
}
else if (tool_local == UV_STRAIGHTEN_Y) {
if (uv_start[0] == uv_end[0]) {
/* Caution, different behavior outside line segment. */
tool_local = UV_STRAIGHTEN;
}
else {
a = (uv_end[1] - uv_start[1]) / (uv_end[0] - uv_start[0]);
}
}
bool changed = false;
for (int j = 0; j < len; j++) {
float *luv = BM_ELEM_CD_GET_FLOAT_P(element[j].l, offsets.uv);
/* Projection of point (x, y) over line (x1, y1, x2, y2) along X axis:
* new_y = (y2 - y1) / (x2 - x1) * (x - x1) + y1
* Maybe this should be a BLI func? Or is it already existing?
* Could use interp_v2_v2v2, but not sure it's worth it here. */
if (tool_local == UV_STRAIGHTEN_X) {
luv[0] = a * (luv[1] - uv_start[1]) + uv_start[0];
}
else if (tool_local == UV_STRAIGHTEN_Y) {
luv[1] = a * (luv[0] - uv_start[0]) + uv_start[1];
}
else {
closest_to_line_segment_v2(luv, luv, uv_start, uv_end);
}
changed = true; /* TODO: Did the UV actually move? */
}
return changed;
}
/**
* Group selected UVs into islands, then apply uvedit_uv_straighten_elements to each island.
*/
static bool uvedit_uv_straighten(Scene *scene, BMesh *bm, eUVWeldAlign tool)
{
const BMUVOffsets offsets = BM_uv_map_offsets_get(bm);
if (offsets.uv == -1) {
return false;
}
UvElementMap *element_map = BM_uv_element_map_create(bm, scene, true, false, true, true);
if (element_map == nullptr) {
return false;
}
bool changed = false;
for (int i = 0; i < element_map->total_islands; i++) {
changed |= uvedit_uv_straighten_elements(element_map->storage + element_map->island_indices[i],
element_map->island_total_uvs[i],
offsets,
tool);
}
BM_uv_element_map_free(element_map);
return changed;
}
enum class UVAlignInitialPosition {
BoundingBox = 0,
UVTileGrid = 1,
ActiveUDIM = 2,
Cursor = 3,
};
enum class UVAlignIslandAxis {
X = 0,
Y = 1,
};
enum class UVAlignIslandMode {
Max = 0,
Min = 1,
Center = 2,
None = 3,
};
enum UVAlignIslandOrder {
LargeToSmall = 0,
SmallToLarge = 1,
Fixed = 2,
};
struct UVAlignIslandBounds {
Bounds<float2> bounds;
int index;
};
/**
* \param position: The position to begin placing islands on,
* this is written to so multiple objects will placing non-overlapping islands.
*/
static bool uvedit_uv_islands_arrange(const Scene *scene,
BMesh *bm,
const UVAlignIslandAxis axis,
const UVAlignIslandMode align,
const UVAlignIslandOrder order,
const float margin,
float2 &position)
{
bool changed = false;
UvElementMap *element_map = BM_uv_element_map_create(bm, scene, true, false, true, true);
if (element_map == nullptr) {
return changed;
}
const BMUVOffsets offsets = BM_uv_map_offsets_get(bm);
const uint other_axis = (uint(axis) + 1) % 2;
Array<UVAlignIslandBounds> island_bounds_all(element_map->total_islands);
for (int i = 0; i < element_map->total_islands; i++) {
UvElement *element = element_map->storage + element_map->island_indices[i];
UVAlignIslandBounds &island_bounds = island_bounds_all[i];
INIT_MINMAX2(island_bounds.bounds.min, island_bounds.bounds.max);
for (int j = 0; j < element_map->island_total_uvs[i]; j++) {
float *luv = BM_ELEM_CD_GET_FLOAT_P(element[j].l, offsets.uv);
minmax_v2v2_v2(island_bounds.bounds.min, island_bounds.bounds.max, luv);
}
island_bounds.index = i;
}
std::stable_sort(island_bounds_all.begin(),
island_bounds_all.end(),
[&order, &axis](const UVAlignIslandBounds &a, const UVAlignIslandBounds &b) {
if (order == UVAlignIslandOrder::Fixed) {
return a.bounds.min[int(axis)] < b.bounds.min[int(axis)];
}
const float area_a = (a.bounds.size()[0] * a.bounds.size()[1]);
const float area_b = (b.bounds.size()[0] * b.bounds.size()[1]);
return (order == UVAlignIslandOrder::LargeToSmall) ? (area_a >= area_b) :
(area_a < area_b);
});
for (const UVAlignIslandBounds &island_bounds : island_bounds_all) {
UvElement *element = element_map->storage + element_map->island_indices[island_bounds.index];
for (int j = 0; j < element_map->island_total_uvs[island_bounds.index]; j++) {
float *luv = BM_ELEM_CD_GET_FLOAT_P(element[j].l, offsets.uv);
if (align == UVAlignIslandMode::Min) {
luv[other_axis] += position[other_axis] - island_bounds.bounds.min[other_axis];
}
else if (align == UVAlignIslandMode::Center) {
luv[other_axis] += position[other_axis] - island_bounds.bounds.center()[other_axis];
}
else if (align == UVAlignIslandMode::Max) {
luv[other_axis] += position[other_axis] - island_bounds.bounds.max[other_axis];
}
luv[int(axis)] += position[int(axis)] - island_bounds.bounds.min[int(axis)];
}
position[int(axis)] += island_bounds.bounds.size()[int(axis)] + margin;
changed = true;
}
BM_uv_element_map_free(element_map);
return changed;
}
static wmOperatorStatus uv_arrange_islands_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
const UVAlignInitialPosition initial_position = UVAlignInitialPosition(
RNA_enum_get(op->ptr, "initial_position"));
const UVAlignIslandAxis axis = UVAlignIslandAxis(RNA_enum_get(op->ptr, "axis"));
const UVAlignIslandMode align = UVAlignIslandMode(RNA_enum_get(op->ptr, "align"));
const UVAlignIslandOrder order = UVAlignIslandOrder(RNA_enum_get(op->ptr, "order"));
const float margin = RNA_float_get(op->ptr, "margin");
const uint other_axis = (uint(axis) + 1) % 2;
float2 position = {0.0f, 0.0f};
Bounds<float2> bounds = {{0.0f, 0.0f}, {1.0f, 1.0f}};
if (initial_position == UVAlignInitialPosition::BoundingBox) {
INIT_MINMAX2(bounds.min, bounds.max);
for (Object *obedit : objects) {
BMesh *bm = BKE_editmesh_from_object(obedit)->bm;
ED_uvedit_foreach_uv(scene, bm, true, true, [&](float luv[2]) {
minmax_v2v2_v2(bounds.min, bounds.max, luv);
});
}
}
else if (initial_position == UVAlignInitialPosition::ActiveUDIM) {
if (sima && sima->image && (sima->image->source == IMA_SRC_TILED)) {
const int tile_x = sima->image->active_tile_index % 10;
const int tile_y = sima->image->active_tile_index / 10;
bounds.min[0] = tile_x;
bounds.min[1] = tile_y;
bounds.max[0] = tile_x + 1.0f;
bounds.max[1] = tile_y + 1.0f;
}
}
else if (initial_position == UVAlignInitialPosition::UVTileGrid) {
/* Leave the minimum at zero. */
bounds.max[0] = sima->tile_grid_shape[0];
bounds.max[1] = sima->tile_grid_shape[1];
}
else {
if (sima) {
position = sima->cursor;
}
}
if (ELEM(initial_position,
UVAlignInitialPosition::BoundingBox,
UVAlignInitialPosition::ActiveUDIM,
UVAlignInitialPosition::UVTileGrid))
{
if (align == UVAlignIslandMode::Min) {
position[other_axis] = bounds.min[other_axis];
}
else if (align == UVAlignIslandMode::Center) {
position[other_axis] = bounds.center()[other_axis];
}
else {
position[other_axis] = bounds.max[other_axis];
}
position[int(axis)] = bounds.min[int(axis)];
}
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (em->bm->totvertsel == 0) {
continue;
}
if (uvedit_uv_islands_arrange(scene, em->bm, axis, align, order, margin, position)) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
return OPERATOR_FINISHED;
}
static void UV_OT_arrange_islands(wmOperatorType *ot)
{
static const EnumPropertyItem initial_position_items[] = {
{int(UVAlignInitialPosition::BoundingBox),
"BOUNDING_BOX",
0,
"Bounding Box",
"Initial alignment based on the islands bounding box"},
{int(UVAlignInitialPosition::UVTileGrid),
"UV_GRID",
0,
"UV Grid",
"Initial alignment based on UV Tile Grid"},
{int(UVAlignInitialPosition::ActiveUDIM),
"ACTIVE_UDIM",
0,
"Active UDIM",
"Initial alignment based on Active UDIM"},
{int(UVAlignInitialPosition::Cursor),
"CURSOR",
0,
"2D Cursor",
"Initial alignment based on 2D cursor"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem axis_items[] = {
{int(UVAlignIslandAxis::X), "X", 0, "X", "Align UV islands along the X axis"},
{int(UVAlignIslandAxis::Y), "Y", 0, "Y", "Align UV islands along the Y axis"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem align_items[] = {
{int(UVAlignIslandMode::Min), "MIN", 0, "Min", "Align the islands to the min of the island"},
{int(UVAlignIslandMode::Max),
"MAX",
0,
"Max",
"Align the islands to the left side of the island"},
{int(UVAlignIslandMode::Center),
"CENTER",
0,
"Center",
"Align the islands to the center of the largest island"},
{int(UVAlignIslandMode::None), "NONE", 0, "None", "Preserve island alignment"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem sort_items[] = {
{int(UVAlignIslandOrder::LargeToSmall),
"LARGE_TO_SMALL",
0,
"Largest to Smallest",
"Sort islands from largest to smallest"},
{int(UVAlignIslandOrder::SmallToLarge),
"SMALL_TO_LARGE",
0,
"Smallest to Largest",
"Sort islands from smallest to largest"},
{int(UVAlignIslandOrder::Fixed), "Fixed", 0, "Fixed", "Preserve island order"},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Arrange/Align Islands";
ot->description = "Arrange selected UV islands on a line";
ot->idname = "UV_OT_arrange_islands";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_arrange_islands_exec;
ot->poll = ED_operator_uvedit;
/* properties */
RNA_def_enum(ot->srna,
"initial_position",
initial_position_items,
int(UVAlignInitialPosition::BoundingBox),
"Initial Position",
"Initial position to arrange islands from");
RNA_def_enum(ot->srna,
"axis",
axis_items,
int(UVAlignIslandAxis::Y),
"Axis",
"Axis to arrange UV islands on");
RNA_def_enum(ot->srna,
"align",
align_items,
int(UVAlignIslandMode::Min),
"Align",
"Location to align islands on");
RNA_def_enum(ot->srna,
"order",
sort_items,
int(UVAlignIslandOrder::LargeToSmall),
"Order",
"Order of islands");
RNA_def_float(
ot->srna, "margin", 0.05f, 0.0f, 1.0f, "Margin", "Space between islands", 0.0f, 1.0f);
}
static void uv_weld(bContext *C)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
float cent[2];
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
ED_uvedit_center_multi(scene, objects, cent, 0);
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
if (em->bm->totvertsel == 0) {
continue;
}
changed |= uvedit_uv_align_weld(scene, em->bm, UV_WELD, cent);
if (changed) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
}
static void uv_align(bContext *C, eUVWeldAlign tool, UVAlignPositionMode position_mode)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
float pos[2], min[2], max[2];
const bool align_auto = (tool == UV_ALIGN_AUTO);
INIT_MINMAX2(min, max);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
if (tool == UV_ALIGN_AUTO) {
ED_uvedit_foreach_uv_multi(
scene, objects, true, true, [&](float luv[2]) { minmax_v2v2_v2(min, max, luv); });
tool = (max[0] - min[0] >= max[1] - min[1]) ? UV_ALIGN_Y : UV_ALIGN_X;
}
if (!align_auto && ELEM(tool, UV_ALIGN_X, UV_ALIGN_Y) &&
ELEM(position_mode, UVAlignPositionMode::Min, UVAlignPositionMode::Max))
{
ED_uvedit_minmax_multi(scene, objects, min, max);
if (position_mode == UVAlignPositionMode::Min) {
pos[0] = min[0];
pos[1] = min[1];
}
else {
pos[0] = max[0];
pos[1] = max[1];
}
}
else {
ED_uvedit_center_multi(scene, objects, pos, V3D_AROUND_CENTER_MEDIAN);
}
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
if (em->bm->totvertsel == 0) {
continue;
}
if (ELEM(tool, UV_ALIGN_AUTO, UV_ALIGN_X, UV_ALIGN_Y)) {
changed |= uvedit_uv_align_weld(scene, em->bm, tool, pos);
}
if (ELEM(tool, UV_STRAIGHTEN, UV_STRAIGHTEN_X, UV_STRAIGHTEN_Y)) {
changed |= uvedit_uv_straighten(scene, em->bm, tool);
}
if (changed) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
}
static wmOperatorStatus uv_align_exec(bContext *C, wmOperator *op)
{
uv_align(C,
eUVWeldAlign(RNA_enum_get(op->ptr, "axis")),
UVAlignPositionMode(RNA_enum_get(op->ptr, "position_mode")));
return OPERATOR_FINISHED;
}
static bool uv_align_poll_property(const bContext * /*C*/, wmOperator *op, const PropertyRNA *prop)
{
const char *prop_id = RNA_property_identifier(prop);
if (STREQ(prop_id, "position_mode")) {
int axis = RNA_enum_get(op->ptr, "axis");
if (!ELEM(axis, UV_ALIGN_X, UV_ALIGN_Y)) {
return false;
}
}
return true;
}
static void UV_OT_align(wmOperatorType *ot)
{
static const EnumPropertyItem axis_items[] = {
{UV_STRAIGHTEN,
"ALIGN_S",
0,
"Straighten",
"Align UV vertices along the line defined by the endpoints"},
{UV_STRAIGHTEN_X,
"ALIGN_T",
0,
"Straighten X",
"Align UV vertices, moving them horizontally to the line defined by the endpoints"},
{UV_STRAIGHTEN_Y,
"ALIGN_U",
0,
"Straighten Y",
"Align UV vertices, moving them vertically to the line defined by the endpoints"},
{UV_ALIGN_AUTO,
"ALIGN_AUTO",
0,
"Align Auto",
"Automatically choose the direction on which there is most alignment already"},
{UV_ALIGN_X, "ALIGN_X", 0, "Align Vertically", "Align UV vertices on a vertical line"},
{UV_ALIGN_Y, "ALIGN_Y", 0, "Align Horizontally", "Align UV vertices on a horizontal line"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem position_mode_items[] = {
{int(UVAlignPositionMode::Mean), "MEAN", 0, "Mean", "Align UVs along the mean position"},
{int(UVAlignPositionMode::Min), "MIN", 0, "Minimum", "Align UVs along the minimum position"},
{int(UVAlignPositionMode::Max), "MAX", 0, "Maximum", "Align UVs along the maximum position"},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Align";
ot->description = "Aligns selected UV vertices on a line";
ot->idname = "UV_OT_align";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_align_exec;
ot->poll = ED_operator_uvedit;
ot->poll_property = uv_align_poll_property;
/* properties */
RNA_def_enum(
ot->srna, "axis", axis_items, UV_ALIGN_AUTO, "Axis", "Axis to align UV locations on");
RNA_def_enum(ot->srna,
"position_mode",
position_mode_items,
int(UVAlignPositionMode::Mean),
"Position Mode",
"Method of calculating the alignment position");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Remove Doubles Operator
* \{ */
static wmOperatorStatus uv_remove_doubles_to_selected(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
const float threshold = RNA_float_get(op->ptr, "threshold");
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
bool *changed = MEM_calloc_arrayN<bool>(objects.size(), __func__);
/* Maximum index of an objects[i]'s UVs in UV_arr.
* It helps find which UV in *uv_map_arr belongs to which object. */
uint *ob_uv_map_max_idx = MEM_calloc_arrayN<uint>(objects.size(), __func__);
/* Calculate max possible number of kdtree nodes. */
int uv_maxlen = 0;
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (em->bm->totvertsel == 0) {
continue;
}
uv_maxlen += em->bm->totloop;
}
KDTree_2d *tree = BLI_kdtree_2d_new(uv_maxlen);
blender::Vector<int> duplicates;
blender::Vector<float *> uv_map_arr;
int uv_map_count = 0; /* Also used for *duplicates count. */
for (const int ob_index : objects.index_range()) {
Object *obedit = objects[ob_index];
BMEditMesh *em = BKE_editmesh_from_object(obedit);
ED_uvedit_foreach_uv(scene, em->bm, true, true, [&](float luv[2]) {
BLI_kdtree_2d_insert(tree, uv_map_count, luv);
duplicates.append(-1);
uv_map_arr.append(luv);
uv_map_count++;
});
ob_uv_map_max_idx[ob_index] = uv_map_count - 1;
}
BLI_kdtree_2d_balance(tree);
int found_duplicates = BLI_kdtree_2d_calc_duplicates_fast(
tree, threshold, false, duplicates.data());
if (found_duplicates > 0) {
/* Calculate average uv for duplicates. */
int *uv_duplicate_count = MEM_calloc_arrayN<int>(uv_map_count, __func__);
for (int i = 0; i < uv_map_count; i++) {
if (duplicates[i] == -1) { /* If doesn't reference another */
uv_duplicate_count[i]++; /* self */
continue;
}
if (duplicates[i] != i) {
/* If not self then accumulate uv for averaging.
* Self uv is already present in accumulator */
add_v2_v2(uv_map_arr[duplicates[i]], uv_map_arr[i]);
}
uv_duplicate_count[duplicates[i]]++;
}
for (int i = 0; i < uv_map_count; i++) {
if (uv_duplicate_count[i] < 2) {
continue;
}
mul_v2_fl(uv_map_arr[i], 1.0f / float(uv_duplicate_count[i]));
}
MEM_freeN(uv_duplicate_count);
/* Update duplicated uvs. */
uint ob_index = 0;
for (int i = 0; i < uv_map_count; i++) {
/* Make sure we know which object owns the uv_map at this index.
* Remember that in some cases the object will have no loop uv,
* thus we need the while loop, and not simply an if check. */
while (ob_uv_map_max_idx[ob_index] < i) {
ob_index++;
}
if (duplicates[i] == -1) {
continue;
}
copy_v2_v2(uv_map_arr[i], uv_map_arr[duplicates[i]]);
changed[ob_index] = true;
}
for (ob_index = 0; ob_index < objects.size(); ob_index++) {
if (changed[ob_index]) {
Object *obedit = objects[ob_index];
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
}
BLI_kdtree_2d_free(tree);
MEM_freeN(changed);
MEM_freeN(ob_uv_map_max_idx);
return OPERATOR_FINISHED;
}
static wmOperatorStatus uv_remove_doubles_to_unselected(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
const float threshold = RNA_float_get(op->ptr, "threshold");
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
/* Calculate max possible number of kdtree nodes. */
int uv_maxlen = 0;
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
uv_maxlen += em->bm->totloop;
}
KDTree_2d *tree = BLI_kdtree_2d_new(uv_maxlen);
blender::Vector<float *> uv_map_arr;
int uv_map_count = 0;
/* Add visible non-selected uvs to tree */
ED_uvedit_foreach_uv_multi(scene, objects, true, false, [&](float luv[2]) {
BLI_kdtree_2d_insert(tree, uv_map_count, luv);
uv_map_arr.append(luv);
uv_map_count++;
});
BLI_kdtree_2d_balance(tree);
/* For each selected uv, find duplicate non selected uv. */
for (Object *obedit : objects) {
bool changed = false;
BMEditMesh *em = BKE_editmesh_from_object(obedit);
ED_uvedit_foreach_uv(scene, em->bm, true, true, [&](float luv[2]) {
KDTreeNearest_2d nearest;
const int i = BLI_kdtree_2d_find_nearest(tree, luv, &nearest);
if (i != -1 && nearest.dist < threshold) {
copy_v2_v2(luv, uv_map_arr[i]);
changed = true;
}
});
if (changed) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
BLI_kdtree_2d_free(tree);
return OPERATOR_FINISHED;
}
static wmOperatorStatus uv_remove_doubles_to_selected_shared_vertex(bContext *C, wmOperator *op)
{
/* NOTE: The calculation for the center-point of loops belonging to a vertex will be skewed
* if one UV coordinate holds more loops than the others. */
Scene *scene = CTX_data_scene(C);
SpaceImage *sima = CTX_wm_space_image(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
/* Only use the squared distance, to avoid a square-root. */
const float threshold_sq = math::square(RNA_float_get(op->ptr, "threshold"));
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
const BMUVOffsets offsets = BM_uv_map_offsets_get(em->bm);
BMVert *v;
BMLoop *l;
BMIter viter, liter;
/* The `changed` variable keeps track if any loops from the current object are merged. */
blender::Vector<float *> uvs;
uvs.reserve(32);
bool changed = false;
BM_ITER_MESH (v, &viter, em->bm, BM_VERTS_OF_MESH) {
BLI_assert(uvs.size() == 0);
BM_ITER_ELEM (l, &liter, v, BM_LOOPS_OF_VERT) {
if (uvedit_uv_select_test(scene, em->bm, l, offsets)) {
uvs.append(BM_ELEM_CD_GET_FLOAT_P(l, offsets.uv));
}
}
if (uvs.size() <= 1) {
uvs.clear();
continue;
}
while (uvs.size() > 1) {
const int uvs_num = uvs.size();
float2 uv_average = {0.0f, 0.0f};
for (const float *luv : uvs) {
uv_average += float2(luv);
}
uv_average /= uvs_num;
/* Find the loop closest to the uv_average. This loop will be the base that all
* other loop's distances are calculated from. */
float dist_best_sq = math::distance_squared(uv_average, float2(uvs[0]));
float *uv_ref = uvs[0];
int uv_ref_index = 0;
for (int i = 1; i < uvs_num; i++) {
const float dist_test_sq = math::distance_squared(uv_average, float2(uvs[i]));
if (dist_test_sq < dist_best_sq) {
dist_best_sq = dist_test_sq;
uv_ref = uvs[i];
uv_ref_index = i;
}
}
const int uvs_end = uvs_num - 1;
std::swap(uvs[uv_ref_index], uvs[uvs_end]);
/* Move all the UVs within threshold to the end of the array. Sum of all UV coordinates
* within threshold is initialized with `uv_ref` coordinate data since while loop
* ends once it hits `uv_ref` UV. */
float2 uv_merged_average = {uv_ref[0], uv_ref[1]};
int i = 0;
int uvs_num_merged = 1;
while (uvs[i] != uv_ref && i < uvs_num - uvs_num_merged) {
const float dist_test_sq = len_squared_v2v2(uv_ref, uvs[i]);
if (dist_test_sq < threshold_sq) {
uv_merged_average += float2(uvs[i]);
std::swap(uvs[i], uvs[uvs_end - uvs_num_merged]);
uvs_num_merged++;
if (dist_test_sq != 0.0f) {
changed = true;
}
}
else {
i++;
}
}
/* Recalculate `uv_average` so it only considers UV's that are being included in merge
* operation. Then Shift all loops to that position. */
if (uvs_num_merged > 1) {
uv_merged_average /= uvs_num_merged;
for (int j = uvs_num - uvs_num_merged; j < uvs_num; j++) {
copy_v2_v2(uvs[j], uv_merged_average);
}
}
uvs.resize(uvs_num - uvs_num_merged);
}
uvs.clear();
}
if (changed) {
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
return OPERATOR_FINISHED;
}
static wmOperatorStatus uv_remove_doubles_exec(bContext *C, wmOperator *op)
{
if (RNA_boolean_get(op->ptr, "use_unselected")) {
return uv_remove_doubles_to_unselected(C, op);
}
if (RNA_boolean_get(op->ptr, "use_shared_vertex")) {
return uv_remove_doubles_to_selected_shared_vertex(C, op);
}
return uv_remove_doubles_to_selected(C, op);
}
static void UV_OT_remove_doubles(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Merge UVs by Distance";
ot->description =
"Selected UV vertices that are within a radius of each other are welded together";
ot->idname = "UV_OT_remove_doubles";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_remove_doubles_exec;
ot->poll = ED_operator_uvedit;
RNA_def_float(ot->srna,
"threshold",
0.02f,
0.0f,
10.0f,
"Merge Distance",
"Maximum distance between welded vertices",
0.0f,
1.0f);
RNA_def_boolean(ot->srna,
"use_unselected",
false,
"Unselected",
"Merge selected to other unselected vertices");
RNA_def_boolean(
ot->srna, "use_shared_vertex", false, "Shared Vertex", "Weld UVs based on shared vertices");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Weld Near Operator
* \{ */
static wmOperatorStatus uv_weld_exec(bContext *C, wmOperator * /*op*/)
{
uv_weld(C);
return OPERATOR_FINISHED;
}
static void UV_OT_weld(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Weld";
ot->description = "Weld selected UV vertices together";
ot->idname = "UV_OT_weld";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_weld_exec;
ot->poll = ED_operator_uvedit;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Snap Cursor Operator
* \{ */
static void uv_snap_to_pixel(float uvco[2], float w, float h)
{
uvco[0] = roundf(uvco[0] * w) / w;
uvco[1] = roundf(uvco[1] * h) / h;
}
static void uv_snap_cursor_to_pixels(SpaceImage *sima)
{
int width = 0, height = 0;
ED_space_image_get_size(sima, &width, &height);
uv_snap_to_pixel(sima->cursor, width, height);
}
static bool uv_snap_cursor_to_selection(Scene *scene,
Span<Object *> objects_edit,
SpaceImage *sima)
{
return ED_uvedit_center_multi(scene, objects_edit, sima->cursor, sima->around);
}
static void uv_snap_cursor_to_origin(float uvco[2])
{
uvco[0] = 0;
uvco[1] = 0;
}
static wmOperatorStatus uv_snap_cursor_exec(bContext *C, wmOperator *op)
{
SpaceImage *sima = CTX_wm_space_image(C);
bool changed = false;
switch (RNA_enum_get(op->ptr, "target")) {
case 0:
uv_snap_cursor_to_pixels(sima);
changed = true;
break;
case 1: {
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Object *> objects =
BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
changed = uv_snap_cursor_to_selection(scene, objects, sima);
break;
}
case 2:
uv_snap_cursor_to_origin(sima->cursor);
changed = true;
break;
}
if (!changed) {
return OPERATOR_CANCELLED;
}
WM_event_add_notifier(C, NC_SPACE | ND_SPACE_IMAGE, sima);
return OPERATOR_FINISHED;
}
static void UV_OT_snap_cursor(wmOperatorType *ot)
{
static const EnumPropertyItem target_items[] = {
{0, "PIXELS", 0, "Pixels", ""},
{1, "SELECTED", 0, "Selected", ""},
{2, "ORIGIN", 0, "Origin", ""},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Snap Cursor";
ot->description = "Snap cursor to target type";
ot->idname = "UV_OT_snap_cursor";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_snap_cursor_exec;
ot->poll = ED_operator_uvedit_space_image; /* requires space image */
/* properties */
RNA_def_enum(
ot->srna, "target", target_items, 0, "Target", "Target to snap the selected UVs to");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Snap Selection Operator
* \{ */
static bool uv_snap_uvs_to_cursor(Scene *scene, Object *obedit, const float cursor[2])
{
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
ED_uvedit_foreach_uv(scene, em->bm, true, true, [&](float luv[2]) {
copy_v2_v2(luv, cursor);
changed = true;
});
return changed;
}
static bool uv_snap_uvs_offset(Scene *scene, Object *obedit, const float offset[2])
{
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
ED_uvedit_foreach_uv(scene, em->bm, true, true, [&](float luv[2]) {
add_v2_v2(luv, offset);
changed = true;
});
return changed;
}
static bool uv_snap_uvs_to_adjacent_unselected(Scene *scene, Object *obedit)
{
BMEditMesh *em = BKE_editmesh_from_object(obedit);
BMesh *bm = em->bm;
BMFace *f;
BMLoop *l, *lsub;
BMIter iter, liter, lsubiter;
float *luv;
bool changed = false;
const BMUVOffsets offsets = BM_uv_map_offsets_get(em->bm);
/* Index every vert that has a selected UV using it, but only once so as to
* get unique indices and to count how much to `malloc`. */
BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
if (uvedit_face_visible_test(scene, f)) {
BM_elem_flag_enable(f, BM_ELEM_TAG);
BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
BM_elem_flag_set(l, BM_ELEM_TAG, uvedit_uv_select_test(scene, bm, l, offsets));
}
}
else {
BM_elem_flag_disable(f, BM_ELEM_TAG);
}
}
BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
if (BM_elem_flag_test(f, BM_ELEM_TAG)) { /* Face: visible. */
BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
if (BM_elem_flag_test(l, BM_ELEM_TAG)) { /* Loop: selected. */
float uv[2] = {0.0f, 0.0f};
int uv_tot = 0;
BM_ITER_ELEM (lsub, &lsubiter, l->v, BM_LOOPS_OF_VERT) {
if (BM_elem_flag_test(lsub->f, BM_ELEM_TAG) && /* Face: visible. */
!BM_elem_flag_test(lsub, BM_ELEM_TAG)) /* Loop: unselected. */
{
luv = BM_ELEM_CD_GET_FLOAT_P(lsub, offsets.uv);
add_v2_v2(uv, luv);
uv_tot++;
}
}
if (uv_tot) {
luv = BM_ELEM_CD_GET_FLOAT_P(l, offsets.uv);
mul_v2_v2fl(luv, uv, 1.0f / float(uv_tot));
changed = true;
}
}
}
}
}
return changed;
}
static bool uv_snap_uvs_to_pixels(SpaceImage *sima, Scene *scene, Object *obedit)
{
BMEditMesh *em = BKE_editmesh_from_object(obedit);
int width = 0, height = 0;
float w, h;
bool changed = false;
ED_space_image_get_size(sima, &width, &height);
w = float(width);
h = float(height);
ED_uvedit_foreach_uv(scene, em->bm, true, true, [&](float luv[2]) {
uv_snap_to_pixel(luv, w, h);
changed = true;
});
return changed;
}
static wmOperatorStatus uv_snap_selection_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
const int target = RNA_enum_get(op->ptr, "target");
float offset[2] = {0};
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
if (target == 2) {
float center[2];
if (!ED_uvedit_center_multi(scene, objects, center, sima->around)) {
return OPERATOR_CANCELLED;
}
sub_v2_v2v2(offset, sima->cursor, center);
}
bool changed_multi = false;
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (em->bm->totvertsel == 0) {
continue;
}
bool changed = false;
switch (target) {
case 0:
changed = uv_snap_uvs_to_pixels(sima, scene, obedit);
break;
case 1:
changed = uv_snap_uvs_to_cursor(scene, obedit, sima->cursor);
break;
case 2:
changed = uv_snap_uvs_offset(scene, obedit, offset);
break;
case 3:
changed = uv_snap_uvs_to_adjacent_unselected(scene, obedit);
break;
}
if (changed) {
changed_multi = true;
uvedit_live_unwrap_update(sima, scene, obedit);
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
return changed_multi ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
static void UV_OT_snap_selected(wmOperatorType *ot)
{
static const EnumPropertyItem target_items[] = {
{0, "PIXELS", 0, "Pixels", ""},
{1, "CURSOR", 0, "Cursor", ""},
{2, "CURSOR_OFFSET", 0, "Cursor (Offset)", ""},
{3, "ADJACENT_UNSELECTED", 0, "Adjacent Unselected", ""},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Snap Selection";
ot->description = "Snap selected UV vertices to target type";
ot->idname = "UV_OT_snap_selected";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_snap_selection_exec;
ot->poll = ED_operator_uvedit_space_image;
/* properties */
RNA_def_enum(
ot->srna, "target", target_items, 0, "Target", "Target to snap the selected UVs to");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Pin UVs Operator
* \{ */
static wmOperatorStatus uv_pin_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
BMFace *efa;
BMLoop *l;
BMIter iter, liter;
const bool clear = RNA_boolean_get(op->ptr, "clear");
const bool invert = RNA_boolean_get(op->ptr, "invert");
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
bool changed = false;
const char *active_uv_name = CustomData_get_active_layer_name(&em->bm->ldata, CD_PROP_FLOAT2);
if (em->bm->totvertsel == 0) {
continue;
}
if (clear && !BM_uv_map_attr_pin_exists(em->bm, active_uv_name)) {
continue;
}
BM_uv_map_attr_pin_ensure_named(em->bm, active_uv_name);
const BMUVOffsets offsets = BM_uv_map_offsets_get(em->bm);
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
if (!uvedit_face_visible_test(scene, efa)) {
continue;
}
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (uvedit_uv_select_test(scene, em->bm, l, offsets)) {
changed = true;
if (invert) {
BM_ELEM_CD_SET_BOOL(l, offsets.pin, !BM_ELEM_CD_GET_BOOL(l, offsets.pin));
}
else {
BM_ELEM_CD_SET_BOOL(l, offsets.pin, !clear);
}
}
}
}
if (changed) {
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
DEG_id_tag_update(static_cast<ID *>(obedit->data), ID_RECALC_SYNC_TO_EVAL);
}
}
return OPERATOR_FINISHED;
}
static void UV_OT_pin(wmOperatorType *ot)
{
PropertyRNA *prop;
/* identifiers */
ot->name = "Pin";
ot->description =
"Set/clear selected UV vertices as anchored between multiple unwrap operations";
ot->idname = "UV_OT_pin";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_pin_exec;
ot->poll = ED_operator_uvedit;
/* properties */
prop = RNA_def_boolean(
ot->srna, "clear", false, "Clear", "Clear pinning for the selection instead of setting it");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(ot->srna,
"invert",
false,
"Invert",
"Invert pinning for the selection instead of setting it");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Hide Operator
* \{ */
/* check if we are selected or unselected based on 'bool_test' arg,
* needed for select swap support */
#define UV_VERT_SEL_TEST(ts, bm, l, bool_test) \
(uvedit_vert_select_get_no_sync(ts, bm, l) == bool_test)
#define UV_EDGE_SEL_TEST(ts, bm, l, bool_test) \
(uvedit_edge_select_get_no_sync(ts, bm, l) == bool_test)
/* is every UV vert selected or unselected depending on bool_test */
static bool bm_face_is_all_uv_sel(const ToolSettings *ts,
const BMesh *bm,
BMFace *f,
bool select_test)
{
BMLoop *l_iter;
BMLoop *l_first;
l_iter = l_first = BM_FACE_FIRST_LOOP(f);
do {
if (!UV_EDGE_SEL_TEST(ts, bm, l_iter, select_test)) {
return false;
}
} while ((l_iter = l_iter->next) != l_first);
return true;
}
static bool uv_mesh_hide_sync_select(const ToolSettings *ts, Object *ob, BMEditMesh *em, bool swap)
{
const bool select_to_hide = !swap;
BMesh *bm = em->bm;
bool changed = false;
if (bm->uv_select_sync_valid == false || ED_uvedit_sync_uvselect_ignore(ts)) {
/* Simple case, no need to synchronize UV's, forward to mesh hide. */
changed = EDBM_mesh_hide(em, swap);
}
else {
/* For vertices & edges hiding faces immediately causes a feedback loop,
* where hiding doesn't work predictably as values are being both read and written to.
* Perform two passes, use tagging. */
/* Vertex and edge modes use almost the same logic. */
if (em->selectmode & (SCE_SELECT_VERTEX | SCE_SELECT_EDGE)) {
BMIter iter;
BM_mesh_elem_hflag_disable_all(bm, BM_FACE, BM_ELEM_TAG, false);
if (em->selectmode & SCE_SELECT_VERTEX) {
BMFace *f;
BM_ITER_MESH (f, &iter, em->bm, BM_FACES_OF_MESH) {
if (BM_elem_flag_test(f, BM_ELEM_HIDDEN)) {
continue;
}
BMLoop *l_iter, *l_first;
l_iter = l_first = BM_FACE_FIRST_LOOP(f);
do {
if ((BM_elem_flag_test_bool(l_iter->v, BM_ELEM_SELECT) == select_to_hide) &&
(BM_elem_flag_test_bool(l_iter, BM_ELEM_SELECT_UV) == select_to_hide))
{
BM_elem_flag_enable(l_iter->f, BM_ELEM_TAG);
changed = true;
break;
}
} while ((l_iter = l_iter->next) != l_first);
}
}
else {
BLI_assert(em->selectmode & SCE_SELECT_EDGE);
BMFace *f;
BM_ITER_MESH (f, &iter, em->bm, BM_FACES_OF_MESH) {
if (BM_elem_flag_test(f, BM_ELEM_HIDDEN)) {
continue;
}
BMLoop *l_iter, *l_first;
l_iter = l_first = BM_FACE_FIRST_LOOP(f);
do {
if ((BM_elem_flag_test_bool(l_iter->e, BM_ELEM_SELECT) == select_to_hide) &&
(BM_elem_flag_test_bool(l_iter, BM_ELEM_SELECT_UV_EDGE) == select_to_hide))
{
BM_elem_flag_enable(l_iter->f, BM_ELEM_TAG);
changed = true;
break;
}
} while ((l_iter = l_iter->next) != l_first);
}
}
if (changed) {
BMFace *f;
BM_ITER_MESH (f, &iter, em->bm, BM_FACES_OF_MESH) {
if (BM_elem_flag_test(f, BM_ELEM_TAG)) {
BM_elem_hide_set(bm, f, true);
}
}
if (swap) {
/* Without re-selecting, the faces vertices are de-selected when hiding adjacent faces.
*
* TODO(@ideasman42): consider a more elegant solution of ensuring
* faces at the boundaries don't get their vertices de-selected.
* This is low-priority as it's no a bottleneck. */
BM_ITER_MESH (f, &iter, em->bm, BM_FACES_OF_MESH) {
if (!BM_elem_flag_test(f, BM_ELEM_TAG)) {
BM_face_select_set(bm, f, true);
}
}
}
}
}
else {
BLI_assert(em->selectmode & SCE_SELECT_FACE);
BMIter iter;
BMFace *f;
BM_ITER_MESH (f, &iter, em->bm, BM_FACES_OF_MESH) {
if (BM_elem_flag_test(f, BM_ELEM_HIDDEN)) {
continue;
}
if (BM_elem_flag_test_bool(f, BM_ELEM_SELECT_UV) == select_to_hide) {
BM_elem_hide_set(bm, f, true);
changed = true;
}
}
}
if (changed) {
if (swap) {
EDBM_selectmode_flush(em);
}
else {
EDBM_flag_disable_all(em, BM_ELEM_SELECT);
}
/* Clearing is OK even when hiding unselected
* as the remaining geometry is entirely selected. */
EDBM_uvselect_clear(em);
}
}
if (changed) {
Mesh *mesh = static_cast<Mesh *>(ob->data);
EDBMUpdate_Params params = {0};
params.calc_looptris = true;
params.calc_normals = false;
params.is_destructive = false;
EDBM_update(mesh, &params);
}
return changed;
}
static wmOperatorStatus uv_hide_exec(bContext *C, wmOperator *op)
{
ViewLayer *view_layer = CTX_data_view_layer(C);
Scene *scene = CTX_data_scene(C);
const ToolSettings *ts = scene->toolsettings;
const bool swap = RNA_boolean_get(op->ptr, "unselected");
const bool use_face_center = (ts->uv_selectmode == UV_SELECT_FACE);
const bool use_select_linked = ED_uvedit_select_island_check(ts);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
for (Object *ob : objects) {
BMEditMesh *em = BKE_editmesh_from_object(ob);
BMFace *efa;
BMLoop *l;
BMIter iter, liter;
if (ts->uv_flag & UV_FLAG_SELECT_SYNC) {
uv_mesh_hide_sync_select(ts, ob, em, swap);
continue;
}
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
int hide = 0;
if (!uvedit_face_visible_test(scene, efa)) {
continue;
}
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (UV_VERT_SEL_TEST(ts, em->bm, l, !swap) || UV_EDGE_SEL_TEST(ts, em->bm, l, !swap)) {
hide = 1;
break;
}
}
if (hide) {
if (use_face_center) {
if (em->selectmode == SCE_SELECT_FACE) {
/* Deselect BMesh face if UV face is (de)selected depending on #swap. */
if (bm_face_is_all_uv_sel(ts, em->bm, efa, !swap)) {
BM_face_select_set(em->bm, efa, false);
}
uvedit_face_select_disable(scene, em->bm, efa);
}
else {
if (bm_face_is_all_uv_sel(ts, em->bm, efa, true) == !swap) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
/* For both cases rely on edge sel tests, since all vert sel tests are invalid in
* case of sticky selections. */
if (UV_EDGE_SEL_TEST(ts, em->bm, l, !swap) && (em->selectmode == SCE_SELECT_EDGE))
{
BM_edge_select_set(em->bm, l->e, false);
}
else if (UV_EDGE_SEL_TEST(ts, em->bm, l, !swap) &&
(em->selectmode == SCE_SELECT_VERTEX))
{
BM_vert_select_set(em->bm, l->v, false);
}
}
}
if (!swap) {
uvedit_face_select_disable(scene, em->bm, efa);
}
}
}
else if (em->selectmode == SCE_SELECT_FACE) {
/* Deselect BMesh face depending on the type of UV selectmode and the type of UV element
* being considered. */
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (UV_EDGE_SEL_TEST(ts, em->bm, l, !swap) && (ts->uv_selectmode == UV_SELECT_EDGE)) {
BM_face_select_set(em->bm, efa, false);
break;
}
if (UV_VERT_SEL_TEST(ts, em->bm, l, !swap) && (ts->uv_selectmode == UV_SELECT_VERT)) {
BM_face_select_set(em->bm, efa, false);
break;
}
if (use_select_linked) {
BM_face_select_set(em->bm, efa, false);
break;
}
}
uvedit_face_select_disable(scene, em->bm, efa);
}
else {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (UV_EDGE_SEL_TEST(ts, em->bm, l, !swap) && (ts->uv_selectmode == UV_SELECT_EDGE)) {
if (em->selectmode == SCE_SELECT_EDGE) {
BM_edge_select_set(em->bm, l->e, false);
}
else {
BM_vert_select_set(em->bm, l->v, false);
BM_vert_select_set(em->bm, l->next->v, false);
}
}
else if (UV_VERT_SEL_TEST(ts, em->bm, l, !swap) &&
(ts->uv_selectmode != UV_SELECT_EDGE))
{
if (em->selectmode == SCE_SELECT_EDGE) {
BM_edge_select_set(em->bm, l->e, false);
}
else {
BM_vert_select_set(em->bm, l->v, false);
}
}
}
if (!swap) {
uvedit_face_select_disable(scene, em->bm, efa);
}
}
}
}
/* Flush editmesh selections to ensure valid selection states. */
if (em->selectmode != SCE_SELECT_FACE) {
/* NOTE: Make sure correct flags are used. Previously this was done by passing
* (SCE_SELECT_VERTEX | SCE_SELECT_EDGE), which doesn't work now that we support proper UV
* edge selection. */
BM_mesh_select_mode_flush(em->bm);
}
BM_select_history_validate(em->bm);
DEG_id_tag_update(static_cast<ID *>(ob->data), ID_RECALC_SELECT);
WM_event_add_notifier(C, NC_GEOM | ND_SELECT, ob->data);
}
return OPERATOR_FINISHED;
}
#undef UV_VERT_SEL_TEST
#undef UV_EDGE_SEL_TEST
static void UV_OT_hide(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Hide Selected";
ot->description = "Hide (un)selected UV vertices";
ot->idname = "UV_OT_hide";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_hide_exec;
ot->poll = ED_operator_uvedit;
/* props */
RNA_def_boolean(
ot->srna, "unselected", false, "Unselected", "Hide unselected rather than selected");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Reveal Operator
* \{ */
static wmOperatorStatus uv_reveal_exec(bContext *C, wmOperator *op)
{
ViewLayer *view_layer = CTX_data_view_layer(C);
Scene *scene = CTX_data_scene(C);
const ToolSettings *ts = scene->toolsettings;
const bool use_face_center = (ts->uv_selectmode == UV_SELECT_FACE);
const bool select = RNA_boolean_get(op->ptr, "select");
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
for (Object *ob : objects) {
BMEditMesh *em = BKE_editmesh_from_object(ob);
BMFace *efa;
BMLoop *l;
BMIter iter, liter;
/* NOTE: Selecting faces is delayed so that it doesn't select verts/edges and confuse certain
* UV selection checks.
* This creates a temporary state which breaks certain UV selection functions that do face
* visibility checks internally. Current implementation handles each case separately. */
/* call the mesh function if we are in mesh sync sel */
if (ts->uv_flag & UV_FLAG_SELECT_SYNC) {
if (EDBM_mesh_reveal(em, select)) {
Mesh *mesh = static_cast<Mesh *>(ob->data);
EDBMUpdate_Params params = {0};
params.calc_looptris = true;
params.calc_normals = false;
params.is_destructive = false;
EDBM_update(mesh, &params);
}
continue;
}
/* NOTE(@sidd017): Supporting selections in all cases is quite difficult considering there are
* at least 12 cases to look into (3 mesh select-modes + 4 uv select-modes + sticky modes).
* For now we select all UV faces as sticky disabled to ensure proper UV selection states (vert
* + edge flags) */
if (use_face_center) {
if (em->selectmode == SCE_SELECT_FACE) {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
uvedit_vert_select_set_no_sync(ts, em->bm, l, select);
uvedit_edge_select_set_no_sync(ts, em->bm, l, select);
}
uvedit_face_select_set_no_sync(ts, em->bm, efa, select);
// BM_face_select_set(em->bm, efa, true);
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
else {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
int totsel = 0;
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (em->selectmode == SCE_SELECT_VERTEX) {
totsel += BM_elem_flag_test(l->v, BM_ELEM_SELECT);
}
else if (em->selectmode == SCE_SELECT_EDGE) {
totsel += BM_elem_flag_test(l->e, BM_ELEM_SELECT);
}
}
if (!totsel) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
uvedit_vert_select_set_no_sync(ts, em->bm, l, select);
uvedit_edge_select_set_no_sync(ts, em->bm, l, select);
}
}
uvedit_face_select_set_no_sync(ts, em->bm, efa, select);
// BM_face_select_set(em->bm, efa, true);
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
}
else if (em->selectmode == SCE_SELECT_FACE) {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
uvedit_vert_select_set_no_sync(ts, em->bm, l, select);
uvedit_edge_select_set_no_sync(ts, em->bm, l, select);
}
uvedit_face_select_set_no_sync(ts, em->bm, efa, select);
// BM_face_select_set(em->bm, efa, true);
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
else {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
uvedit_vert_select_set_no_sync(ts, em->bm, l, select);
uvedit_edge_select_set_no_sync(ts, em->bm, l, select);
}
uvedit_face_select_set_no_sync(ts, em->bm, efa, select);
// BM_face_select_set(em->bm, efa, true);
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
/* re-select tagged faces */
BM_mesh_elem_hflag_enable_test(em->bm, BM_FACE, BM_ELEM_SELECT, true, false, BM_ELEM_TAG);
DEG_id_tag_update(static_cast<ID *>(ob->data), ID_RECALC_SELECT);
WM_event_add_notifier(C, NC_GEOM | ND_SELECT, ob->data);
}
return OPERATOR_FINISHED;
}
static void UV_OT_reveal(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Reveal Hidden";
ot->description = "Reveal all hidden UV vertices";
ot->idname = "UV_OT_reveal";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_reveal_exec;
ot->poll = ED_operator_uvedit;
RNA_def_boolean(ot->srna, "select", true, "Select", "");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Set 2D Cursor Operator
* \{ */
static wmOperatorStatus uv_set_2d_cursor_exec(bContext *C, wmOperator *op)
{
SpaceImage *sima = CTX_wm_space_image(C);
if (!sima) {
return OPERATOR_CANCELLED;
}
RNA_float_get_array(op->ptr, "location", sima->cursor);
{
wmMsgBus *mbus = CTX_wm_message_bus(C);
bScreen *screen = CTX_wm_screen(C);
WM_msg_publish_rna_prop(mbus, &screen->id, sima, SpaceImageEditor, cursor_location);
}
WM_event_add_notifier(C, NC_SPACE | ND_SPACE_IMAGE, nullptr);
/* Use pass-through to allow click-drag to transform the cursor. */
return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;
}
static wmOperatorStatus uv_set_2d_cursor_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
ARegion *region = CTX_wm_region(C);
float location[2];
if (region->regiontype == RGN_TYPE_WINDOW) {
SpaceImage *sima = CTX_wm_space_image(C);
if (sima && ED_space_image_show_cache_and_mval_over(sima, region, event->mval)) {
return OPERATOR_PASS_THROUGH;
}
}
UI_view2d_region_to_view(
&region->v2d, event->mval[0], event->mval[1], &location[0], &location[1]);
RNA_float_set_array(op->ptr, "location", location);
return uv_set_2d_cursor_exec(C, op);
}
static void UV_OT_cursor_set(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Set 2D Cursor";
ot->description = "Set 2D cursor location";
ot->idname = "UV_OT_cursor_set";
/* API callbacks. */
ot->exec = uv_set_2d_cursor_exec;
ot->invoke = uv_set_2d_cursor_invoke;
ot->poll = ED_space_image_cursor_poll;
/* properties */
RNA_def_float_vector(ot->srna,
"location",
2,
nullptr,
-FLT_MAX,
FLT_MAX,
"Location",
"Cursor location in normalized (0.0 to 1.0) coordinates",
-10.0f,
10.0f);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Seam from UV Islands Operator
* \{ */
static wmOperatorStatus uv_seams_from_islands_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
const bool mark_seams = RNA_boolean_get(op->ptr, "mark_seams");
const bool mark_sharp = RNA_boolean_get(op->ptr, "mark_sharp");
bool changed_multi = false;
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
for (Object *ob : objects) {
Mesh *mesh = (Mesh *)ob->data;
BMEditMesh *em = mesh->runtime->edit_mesh.get();
BMesh *bm = em->bm;
BMIter iter;
if (!EDBM_uv_check(em)) {
continue;
}
const BMUVOffsets offsets = BM_uv_map_offsets_get(em->bm);
bool changed = false;
BMFace *f;
BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
if (!uvedit_face_visible_test(scene, f)) {
continue;
}
BMLoop *l_iter;
BMLoop *l_first;
l_iter = l_first = BM_FACE_FIRST_LOOP(f);
do {
if (l_iter == l_iter->radial_next) {
continue;
}
if (!uvedit_edge_select_test(scene, em->bm, l_iter, offsets)) {
continue;
}
bool mark = false;
BMLoop *l_other = l_iter->radial_next;
do {
if (!BM_loop_uv_share_edge_check(l_iter, l_other, offsets.uv)) {
mark = true;
break;
}
} while ((l_other = l_other->radial_next) != l_iter);
if (mark) {
if (mark_seams) {
BM_elem_flag_enable(l_iter->e, BM_ELEM_SEAM);
}
if (mark_sharp) {
BM_elem_flag_disable(l_iter->e, BM_ELEM_SMOOTH);
}
changed = true;
}
} while ((l_iter = l_iter->next) != l_first);
}
if (changed) {
changed_multi = true;
DEG_id_tag_update(&mesh->id, 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);
}
}
return changed_multi ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
static void UV_OT_seams_from_islands(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Seams from Islands";
ot->description = "Set mesh seams according to island setup in the UV editor";
ot->idname = "UV_OT_seams_from_islands";
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_seams_from_islands_exec;
ot->poll = ED_operator_uvedit;
RNA_def_boolean(ot->srna, "mark_seams", true, "Mark Seams", "Mark boundary edges as seams");
RNA_def_boolean(ot->srna, "mark_sharp", false, "Mark Sharp", "Mark boundary edges as sharp");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Mark Seam Operator
* \{ */
static wmOperatorStatus uv_mark_seam_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
const ToolSettings *ts = scene->toolsettings;
BMFace *efa;
BMLoop *loop;
BMIter iter, liter;
const bool flag_set = !RNA_boolean_get(op->ptr, "clear");
const bool synced_selection = (ts->uv_flag & UV_FLAG_SELECT_SYNC) != 0;
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
bool changed = false;
for (Object *ob : objects) {
Mesh *mesh = (Mesh *)ob->data;
BMEditMesh *em = mesh->runtime->edit_mesh.get();
BMesh *bm = em->bm;
if (synced_selection && (bm->totedgesel == 0)) {
continue;
}
const BMUVOffsets offsets = BM_uv_map_offsets_get(em->bm);
BM_ITER_MESH (efa, &iter, bm, BM_FACES_OF_MESH) {
if (uvedit_face_visible_test(scene, efa)) {
BM_ITER_ELEM (loop, &liter, efa, BM_LOOPS_OF_FACE) {
if (uvedit_edge_select_test(scene, bm, loop, offsets)) {
BM_elem_flag_set(loop->e, BM_ELEM_SEAM, flag_set);
changed = true;
}
}
}
}
if (changed) {
DEG_id_tag_update(&mesh->id, 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);
}
}
if (changed) {
ED_uvedit_live_unwrap(scene, objects);
}
return OPERATOR_FINISHED;
}
static wmOperatorStatus uv_mark_seam_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
uiPopupMenu *pup;
uiLayout *layout;
if (RNA_struct_property_is_set(op->ptr, "clear")) {
return uv_mark_seam_exec(C, op);
}
pup = UI_popup_menu_begin(C, IFACE_("Edges"), ICON_NONE);
layout = UI_popup_menu_layout(pup);
layout->operator_context_set(blender::wm::OpCallContext::ExecDefault);
PointerRNA op_ptr = layout->op(
op->type->idname, CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Mark Seam"), ICON_NONE);
RNA_boolean_set(&op_ptr, "clear", false);
op_ptr = layout->op(
op->type->idname, CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Clear Seam"), ICON_NONE);
RNA_boolean_set(&op_ptr, "clear", true);
UI_popup_menu_end(C, pup);
return OPERATOR_INTERFACE;
}
static void UV_OT_mark_seam(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Mark Seam";
ot->description = "Mark selected UV edges as seams";
ot->idname = "UV_OT_mark_seam";
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* API callbacks. */
ot->exec = uv_mark_seam_exec;
ot->invoke = uv_mark_seam_invoke;
ot->poll = ED_operator_uvedit;
RNA_def_boolean(ot->srna, "clear", false, "Clear Seams", "Clear instead of marking seams");
}
static bool uv_copy_mirrored_faces(
const Scene *scene, BMesh *bm, int direction, int precision, int *r_double_warn)
{
*r_double_warn = 0;
const float precision_scale = powf(10.0f, precision);
/* TODO: replace mirror look-ups with #EditMeshSymmetryHelper. */
Map<float3, BMVert *> mirror_gt, mirror_lt;
Map<BMVert *, BMVert *> vmap;
const BMUVOffsets offsets = BM_uv_map_offsets_get(bm);
BLI_assert(offsets.uv != -1);
UNUSED_VARS_NDEBUG(offsets);
BMVert *v;
BMIter iter;
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
float3 pos = math::round(float3(v->co) * precision_scale);
if (pos.x >= 0.0f) {
if (!mirror_gt.add_overwrite(pos, v)) {
(*r_double_warn)++;
}
}
if (pos.x <= 0.0f) {
if (!mirror_lt.add_overwrite(pos, v)) {
(*r_double_warn)++;
}
}
}
for (const auto &[pos, v] : mirror_gt.items()) {
float3 mirror_pos = pos;
mirror_pos[0] = -mirror_pos[0];
BMVert *v_mirror = mirror_lt.lookup_default(mirror_pos, nullptr);
if (v_mirror) {
vmap.add(v, v_mirror);
}
}
for (const auto &[pos, v] : mirror_lt.items()) {
float3 mirror_pos = pos;
mirror_pos[0] = -mirror_pos[0];
BMVert *v_mirror = mirror_gt.lookup_default(mirror_pos, nullptr);
if (v_mirror) {
vmap.add(v, v_mirror);
}
}
Map<Array<BMVert *>, BMFace *> sorted_verts_to_face;
/* Maps faces to their corresponding mirrored face. */
Map<BMFace *, BMFace *> face_map;
BMFace *f;
BMIter iter_face;
BM_ITER_MESH (f, &iter_face, bm, BM_FACES_OF_MESH) {
Array<BMVert *> sorted_verts(f->len);
bool valid = true;
int loop_index = 0;
BMLoop *l;
BMIter liter;
BM_ITER_ELEM_INDEX (l, &liter, f, BM_LOOPS_OF_FACE, loop_index) {
if (!vmap.contains(l->v)) {
valid = false;
break;
}
sorted_verts[loop_index] = l->v;
}
if (valid) {
std::sort(sorted_verts.begin(), sorted_verts.end());
sorted_verts_to_face.add(std::move(sorted_verts), f);
}
}
for (const auto &[sorted_verts, f_dst] : sorted_verts_to_face.items()) {
Array<BMVert *> mirror_verts(sorted_verts.size());
for (int index = 0; index < sorted_verts.size(); index++) {
mirror_verts[index] = vmap.lookup_default(sorted_verts[index], nullptr);
}
std::sort(mirror_verts.begin(), mirror_verts.end());
BMFace *f_src = sorted_verts_to_face.lookup_default(mirror_verts, nullptr);
if (f_src) {
if (f_src != f_dst) {
face_map.add(f_dst, f_src);
}
}
}
const int cd_loop_uv_offset = CustomData_get_offset(&bm->ldata, CD_PROP_FLOAT2);
bool changed = false;
for (const auto &[f_dst, f_src] : face_map.items()) {
/* Skip unless both faces have all their UVs selected. */
if (!uvedit_face_select_test(scene, bm, f_dst) || !uvedit_face_select_test(scene, bm, f_src)) {
continue;
}
{
float f_dst_center[3];
BM_face_calc_center_median(f_dst, f_dst_center);
if (direction ? (f_dst_center[0] > 0.0f) : (f_dst_center[0] < 0.0f)) {
continue;
}
}
BMIter liter;
BMLoop *l_dst;
BM_ITER_ELEM (l_dst, &liter, f_dst, BM_LOOPS_OF_FACE) {
BMVert *v_src = vmap.lookup_default(l_dst->v, nullptr);
if (!v_src) {
continue;
}
BMLoop *l_src = BM_face_vert_share_loop(f_src, v_src);
if (!l_src) {
continue;
}
const float *uv_src = BM_ELEM_CD_GET_FLOAT_P(l_src, cd_loop_uv_offset);
float *uv_dst = BM_ELEM_CD_GET_FLOAT_P(l_dst, cd_loop_uv_offset);
uv_dst[0] = -(uv_src[0] - 0.5f) + 0.5f;
uv_dst[1] = uv_src[1];
changed = true;
}
}
return changed;
}
static wmOperatorStatus uv_copy_mirrored_faces_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
scene, view_layer, nullptr);
const int direction = RNA_enum_get(op->ptr, "direction");
const int precision = RNA_int_get(op->ptr, "precision");
int total_duplicates = 0;
int meshes_with_duplicates = 0;
for (Object *obedit : objects) {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
int double_warn = 0;
bool changed = uv_copy_mirrored_faces(scene, em->bm, direction, precision, &double_warn);
if (double_warn) {
total_duplicates += double_warn;
meshes_with_duplicates++;
}
if (changed) {
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
}
}
if (total_duplicates) {
BKE_reportf(op->reports,
RPT_WARNING,
"%d duplicates found in %d mesh(es), mirror may be incomplete",
total_duplicates,
meshes_with_duplicates);
}
return OPERATOR_FINISHED;
}
void UV_OT_copy_mirrored_faces(wmOperatorType *ot)
{
static const EnumPropertyItem direction_items[] = {
{0, "POSITIVE", 0, "Positive", ""},
{1, "NEGATIVE", 0, "Negative", ""},
{0, nullptr, 0, nullptr, nullptr},
};
ot->name = "Copy Mirrored UV Coords";
ot->description = "Copy mirror UV coordinates on the X axis based on a mirrored mesh";
ot->idname = "UV_OT_copy_mirrored_faces";
ot->exec = uv_copy_mirrored_faces_exec;
ot->poll = ED_operator_editmesh;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
RNA_def_enum(ot->srna, "direction", direction_items, 0, "Axis Direction", "");
RNA_def_int(ot->srna,
"precision",
3,
1,
16,
"Precision",
"Tolerance for finding vertex duplicates",
1,
16);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operator Registration & Keymap
* \{ */
void ED_operatortypes_uvedit()
{
/* `uvedit_select.cc` */
WM_operatortype_append(UV_OT_select_all);
WM_operatortype_append(UV_OT_select);
WM_operatortype_append(UV_OT_select_loop);
WM_operatortype_append(UV_OT_select_edge_ring);
WM_operatortype_append(UV_OT_select_linked);
WM_operatortype_append(UV_OT_select_linked_pick);
WM_operatortype_append(UV_OT_select_split);
WM_operatortype_append(UV_OT_select_pinned);
WM_operatortype_append(UV_OT_select_box);
WM_operatortype_append(UV_OT_select_lasso);
WM_operatortype_append(UV_OT_select_similar);
WM_operatortype_append(UV_OT_select_circle);
WM_operatortype_append(UV_OT_select_more);
WM_operatortype_append(UV_OT_select_less);
WM_operatortype_append(UV_OT_select_overlap);
WM_operatortype_append(UV_OT_select_mode);
WM_operatortype_append(UV_OT_custom_region_set);
WM_operatortype_append(UV_OT_snap_cursor);
WM_operatortype_append(UV_OT_snap_selected);
WM_operatortype_append(UV_OT_align);
WM_operatortype_append(UV_OT_arrange_islands);
WM_operatortype_append(UV_OT_rip);
WM_operatortype_append(UV_OT_stitch);
WM_operatortype_append(UV_OT_shortest_path_pick);
WM_operatortype_append(UV_OT_shortest_path_select);
WM_operatortype_append(UV_OT_seams_from_islands);
WM_operatortype_append(UV_OT_mark_seam);
WM_operatortype_append(UV_OT_weld);
WM_operatortype_append(UV_OT_remove_doubles);
WM_operatortype_append(UV_OT_pin);
WM_operatortype_append(UV_OT_average_islands_scale);
WM_operatortype_append(UV_OT_cube_project);
WM_operatortype_append(UV_OT_cylinder_project);
WM_operatortype_append(UV_OT_project_from_view);
WM_operatortype_append(UV_OT_minimize_stretch);
WM_operatortype_append(UV_OT_pack_islands);
WM_operatortype_append(UV_OT_reset);
WM_operatortype_append(UV_OT_sphere_project);
WM_operatortype_append(UV_OT_unwrap);
WM_operatortype_append(UV_OT_smart_project);
WM_operatortype_append(UV_OT_reveal);
WM_operatortype_append(UV_OT_hide);
WM_operatortype_append(UV_OT_copy);
WM_operatortype_append(UV_OT_paste);
WM_operatortype_append(UV_OT_cursor_set);
WM_operatortype_append(UV_OT_copy_mirrored_faces);
WM_operatortype_append(UV_OT_move_on_axis);
}
void ED_operatormacros_uvedit()
{
wmOperatorType *ot;
wmOperatorTypeMacro *otmacro;
ot = WM_operatortype_append_macro("UV_OT_rip_move",
"UV Rip Move",
"Unstitch UVs and move the result",
OPTYPE_UNDO | OPTYPE_REGISTER);
WM_operatortype_macro_define(ot, "UV_OT_rip");
otmacro = WM_operatortype_macro_define(ot, "TRANSFORM_OT_translate");
RNA_boolean_set(otmacro->ptr, "use_proportional_edit", false);
RNA_boolean_set(otmacro->ptr, "mirror", false);
}
void ED_keymap_uvedit(wmKeyConfig *keyconf)
{
wmKeyMap *keymap;
keymap = WM_keymap_ensure(keyconf, "UV Editor", SPACE_EMPTY, RGN_TYPE_WINDOW);
keymap->poll = ED_operator_uvedit;
}
/** \} */