From cfad6f4f25822c96a2ce57574b572336c65731ea Mon Sep 17 00:00:00 2001 From: Eitan Traurig Date: Fri, 3 Oct 2025 15:50:14 +1000 Subject: [PATCH] UV: Move on Axis selected UVs operator The operator moves selected UVs, using the num-pad for directional keys: Modifiers control the units: - UDIM / UV unit (NUMPAD KEY) - Dynamic grid unit (CTRL + NUMPAD KEY) - Pixel unit (SHIFT + NUMPAD KEY) Implements design task #78405. Ref !139608 --- .../keyconfig/keymap_data/blender_default.py | 18 ++++ scripts/startup/bl_ui/space_image.py | 1 + source/blender/editors/uvedit/uvedit_ops.cc | 96 +++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index fa50c713376..a7fcfc95331 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -1418,6 +1418,24 @@ def km_uv_editor(params): op_menu("IMAGE_MT_uvs_merge", {"type": 'M', "value": 'PRESS'}), op_menu("IMAGE_MT_uvs_split", {"type": 'M', "value": 'PRESS', "alt": True}), op_menu("IMAGE_MT_uvs_align", {"type": 'W', "value": 'PRESS', "shift": True}), + *[ + ( + "uv.move_on_axis", + {"type": key, "value": 'PRESS', **mod_dict}, + {"properties": [("axis", axis), ("type", move_type), ("distance", distance)]} + ) + for mod_dict, move_type in ( + ({"ctrl": True}, 'DYNAMIC'), + ({"shift": True}, 'PIXEL'), + ({}, 'UDIM'), + ) + for key, axis, distance in ( + ('NUMPAD_8', 'Y', 1), + ('NUMPAD_2', 'Y', -1), + ('NUMPAD_6', 'X', 1), + ('NUMPAD_4', 'X', -1), + ) + ], ("uv.stitch", {"type": 'V', "value": 'PRESS', "alt": True}, None), ("uv.rip_move", {"type": 'V', "value": 'PRESS'}, None), ("uv.pin", {"type": 'P', "value": 'PRESS'}, diff --git a/scripts/startup/bl_ui/space_image.py b/scripts/startup/bl_ui/space_image.py index 5b7564cc081..cdeb5e46a1a 100644 --- a/scripts/startup/bl_ui/space_image.py +++ b/scripts/startup/bl_ui/space_image.py @@ -484,6 +484,7 @@ class IMAGE_MT_uvs(Menu): layout.operator_context = 'EXEC_REGION_WIN' layout.menu("IMAGE_MT_uvs_align") layout.operator("uv.align_rotation") + layout.operator_menu_enum("uv.move_on_axis", "type", text="Move on Axis") layout.separator() diff --git a/source/blender/editors/uvedit/uvedit_ops.cc b/source/blender/editors/uvedit/uvedit_ops.cc index ef4c8f5a8ec..a741e607b6c 100644 --- a/source/blender/editors/uvedit/uvedit_ops.cc +++ b/source/blender/editors/uvedit/uvedit_ops.cc @@ -344,6 +344,101 @@ bool ED_uvedit_center_from_pivot_ex(const SpaceImage *sima, 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 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->tile_grid_shape[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(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); +} /** \} */ /* -------------------------------------------------------------------- */ @@ -2584,6 +2679,7 @@ void ED_operatortypes_uvedit() 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()