GPv3: Snap operators for edit mode

Implements snapping for grid, selected-to-cursor, and cursor-to-selected,
as well as the regular menu and pie menu for these operators.

Pull Request: https://projects.blender.org/blender/blender/pulls/121582
This commit is contained in:
Lukas Tönne
2024-05-12 10:24:07 +02:00
parent e63fef6162
commit db1443249c
4 changed files with 330 additions and 0 deletions

View File

@@ -4620,6 +4620,8 @@ def km_grease_pencil_edit_mode(params):
("grease_pencil.paste", {"type": 'V', "value": 'PRESS', "ctrl": True}, None),
("grease_pencil.paste", {"type": 'V', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("paste_back", True)]}),
# Snap
op_menu_pie("GREASE_PENCIL_MT_snap_pie", {"type": 'S', "value": 'PRESS', "shift": True}),
# Separate
("grease_pencil.separate", {"type": 'P', "value": 'PRESS'}, None),
# Delete all active frames

View File

@@ -964,6 +964,48 @@ class GreasePencilFlipTintColors(Operator):
return {'FINISHED'}
class GREASE_PENCIL_MT_snap(Menu):
bl_label = "Snap"
def draw(self, _context):
layout = self.layout
layout.operator("grease_pencil.snap_to_grid", text="Selection to Grid")
layout.operator("grease_pencil.snap_to_cursor", text="Selection to Cursor").use_offset = False
layout.operator("grease_pencil.snap_to_cursor", text="Selection to Cursor (Keep Offset)").use_offset = True
layout.separator()
layout.operator("grease_pencil.snap_cursor_to_selected", text="Cursor to Selected")
layout.operator("view3d.snap_cursor_to_center", text="Cursor to World Origin")
layout.operator("view3d.snap_cursor_to_grid", text="Cursor to Grid")
class GREASE_PENCIL_MT_snap_pie(Menu):
bl_label = "Snap"
def draw(self, _context):
layout = self.layout
pie = layout.menu_pie()
pie.operator("view3d.snap_cursor_to_grid", text="Cursor to Grid", icon='CURSOR')
pie.operator("grease_pencil.snap_to_grid", text="Selection to Grid", icon='RESTRICT_SELECT_OFF')
pie.operator("grease_pencil.snap_cursor_to_selected", text="Cursor to Selected", icon='CURSOR')
pie.operator(
"grease_pencil.snap_to_cursor",
text="Selection to Cursor",
icon='RESTRICT_SELECT_OFF',
).use_offset = False
pie.operator(
"grease_pencil.snap_to_cursor",
text="Selection to Cursor (Keep Offset)",
icon='RESTRICT_SELECT_OFF',
).use_offset = True
pie.separator()
pie.operator("view3d.snap_cursor_to_center", text="Cursor to World Origin", icon='CURSOR')
pie.separator()
classes = (
GPENCIL_MT_snap,
GPENCIL_MT_snap_pie,
@@ -982,6 +1024,9 @@ classes = (
GREASE_PENCIL_MT_move_to_layer,
GREASE_PENCIL_MT_layer_active,
GREASE_PENCIL_MT_snap,
GREASE_PENCIL_MT_snap_pie,
GreasePencilFlipTintColors,
)

View File

@@ -6023,6 +6023,7 @@ class VIEW3D_MT_edit_greasepencil(Menu):
layout = self.layout
layout.menu("VIEW3D_MT_transform")
layout.menu("VIEW3D_MT_mirror")
layout.menu("GREASE_PENCIL_MT_snap")
layout.separator()

View File

@@ -7,15 +7,23 @@
*/
#include "BLI_array_utils.hh"
#include "BLI_assert.h"
#include "BLI_index_mask.hh"
#include "BLI_index_range.hh"
#include "BLI_math_base.hh"
#include "BLI_math_geom.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_offset_indices.hh"
#include "BLI_span.hh"
#include "BLI_utildefines.h"
#include "BLT_translation.hh"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"
#include "BKE_attribute.hh"
#include "BKE_context.hh"
@@ -27,6 +35,8 @@
#include "BKE_material.h"
#include "BKE_report.hh"
#include "DNA_view3d_types.h"
#include "DNA_windowmanager_types.h"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
@@ -36,6 +46,7 @@
#include "ED_curves.hh"
#include "ED_grease_pencil.hh"
#include "ED_object.hh"
#include "ED_view3d.hh"
#include "GEO_join_geometries.hh"
#include "GEO_reorder.hh"
@@ -43,6 +54,7 @@
#include "GEO_subdivide_curves.hh"
#include "UI_resources.hh"
#include <limits>
namespace blender::ed::greasepencil {
@@ -2633,6 +2645,273 @@ static void GREASE_PENCIL_OT_extrude(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Snapping Selection to Grid Operator
* \{ */
/* Poll callback for snap operators */
/* NOTE: For now, we only allow these in the 3D view, as other editors do not
* define a cursor or grid-step which can be used.
*/
static bool grease_pencil_snap_poll(bContext *C)
{
if (!editable_grease_pencil_poll(C)) {
return false;
}
ScrArea *area = CTX_wm_area(C);
return (area != nullptr) && (area->spacetype == SPACE_VIEW3D);
}
static int grease_pencil_snap_to_grid_exec(bContext *C, wmOperator * /*op*/)
{
using bke::greasepencil::Layer;
const Scene &scene = *CTX_data_scene(C);
Object &object = *CTX_data_active_object(C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
const View3D &v3d = *CTX_wm_view3d(C);
const ARegion &region = *CTX_wm_region(C);
const float grid_size = ED_view3d_grid_view_scale(&scene, &v3d, &region, nullptr);
const Vector<MutableDrawingInfo> drawings = retrieve_editable_drawings(scene, grease_pencil);
for (const MutableDrawingInfo &drawing_info : drawings) {
bke::CurvesGeometry &curves = drawing_info.drawing.strokes_for_write();
if (curves.curves_num() == 0) {
continue;
}
if (!ed::curves::has_anything_selected(curves)) {
continue;
}
IndexMaskMemory memory;
const IndexMask selected_points = ed::curves::retrieve_selected_points(curves, memory);
const Layer &layer = *grease_pencil.layer(drawing_info.layer_index);
const float4x4 layer_to_world = layer.to_world_space(object);
const float4x4 world_to_layer = math::invert(layer_to_world);
MutableSpan<float3> positions = curves.positions_for_write();
selected_points.foreach_index(GrainSize(4096), [&](const int point_i) {
const float3 pos_world = math::transform_point(layer_to_world, positions[point_i]);
const float3 pos_snapped = grid_size * math::floor(pos_world / grid_size + 0.5f);
positions[point_i] = math::transform_point(world_to_layer, pos_snapped);
});
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
DEG_id_tag_update(&object.id, ID_RECALC_SYNC_TO_EVAL);
WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr);
}
return OPERATOR_FINISHED;
}
static void GREASE_PENCIL_OT_snap_to_grid(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Snap Selection to Grid";
ot->idname = "GREASE_PENCIL_OT_snap_to_grid";
ot->description = "Snap selected points to the nearest grid points";
/* callbacks */
ot->exec = grease_pencil_snap_to_grid_exec;
ot->poll = grease_pencil_snap_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Snapping Selection to Cursor Operator
* \{ */
static int grease_pencil_snap_to_cursor_exec(bContext *C, wmOperator *op)
{
using bke::greasepencil::Layer;
const Scene &scene = *CTX_data_scene(C);
Object &object = *CTX_data_active_object(C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
const bool use_offset = RNA_boolean_get(op->ptr, "use_offset");
const float3 cursor_world = scene.cursor.location;
const Vector<MutableDrawingInfo> drawings = retrieve_editable_drawings(scene, grease_pencil);
for (const MutableDrawingInfo &drawing_info : drawings) {
bke::CurvesGeometry &curves = drawing_info.drawing.strokes_for_write();
if (curves.curves_num() == 0) {
continue;
}
if (!ed::curves::has_anything_selected(curves)) {
continue;
}
IndexMaskMemory selected_points_memory;
const IndexMask selected_points = ed::curves::retrieve_selected_points(curves,
selected_points_memory);
const Layer &layer = *grease_pencil.layer(drawing_info.layer_index);
const float4x4 layer_to_world = layer.to_world_space(object);
const float4x4 world_to_layer = math::invert(layer_to_world);
const float3 cursor_layer = math::transform_point(world_to_layer, cursor_world);
MutableSpan<float3> positions = curves.positions_for_write();
if (use_offset) {
const OffsetIndices points_by_curve = curves.points_by_curve();
IndexMaskMemory selected_curves_memory;
const IndexMask selected_curves = ed::curves::retrieve_selected_curves(
curves, selected_curves_memory);
selected_curves.foreach_index(GrainSize(512), [&](const int curve_i) {
const IndexRange points = points_by_curve[curve_i];
/* Offset from first point of the curve. */
const float3 offset = cursor_layer - positions[points.first()];
selected_points.slice_content(points).foreach_index(
GrainSize(4096), [&](const int point_i) { positions[point_i] += offset; });
});
}
else {
/* Set all selected positions to the cursor location. */
index_mask::masked_fill(positions, cursor_layer, selected_points);
}
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
DEG_id_tag_update(&object.id, ID_RECALC_SYNC_TO_EVAL);
WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr);
}
return OPERATOR_FINISHED;
}
static void GREASE_PENCIL_OT_snap_to_cursor(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Snap Selection to Cursor";
ot->idname = "GREASE_PENCIL_OT_snap_to_cursor";
ot->description = "Snap selected points/strokes to the cursor";
/* callbacks */
ot->exec = grease_pencil_snap_to_cursor_exec;
ot->poll = grease_pencil_snap_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* props */
ot->prop = RNA_def_boolean(ot->srna,
"use_offset",
true,
"With Offset",
"Offset the entire stroke instead of selected points only");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Snapping Cursor to Selection Operator
* \{ */
static bool grease_pencil_snap_compute_centroid(const Scene &scene,
const Object &object,
const GreasePencil &grease_pencil,
float3 &r_centroid,
float3 &r_min,
float3 &r_max)
{
using bke::greasepencil::Layer;
int num_selected = 0;
r_centroid = float3(0.0f);
r_min = float3(std::numeric_limits<float>::max());
r_max = float3(std::numeric_limits<float>::lowest());
const Vector<DrawingInfo> drawings = retrieve_visible_drawings(scene, grease_pencil, false);
for (const DrawingInfo &drawing_info : drawings) {
const bke::CurvesGeometry &curves = drawing_info.drawing.strokes();
if (curves.curves_num() == 0) {
continue;
}
if (!ed::curves::has_anything_selected(curves)) {
continue;
}
IndexMaskMemory selected_points_memory;
const IndexMask selected_points = ed::curves::retrieve_selected_points(curves,
selected_points_memory);
const Layer &layer = *grease_pencil.layer(drawing_info.layer_index);
const float4x4 layer_to_world = layer.to_world_space(object);
Span<float3> positions = curves.positions();
selected_points.foreach_index(GrainSize(4096), [&](const int point_i) {
const float3 pos_world = math::transform_point(layer_to_world, positions[point_i]);
r_centroid += pos_world;
math::min_max(pos_world, r_min, r_max);
});
num_selected += selected_points.size();
}
if (num_selected == 0) {
r_min = r_max = float3(0.0f);
return false;
}
r_centroid /= num_selected;
return true;
}
static int grease_pencil_snap_cursor_to_sel_exec(bContext *C, wmOperator *op)
{
Scene &scene = *CTX_data_scene(C);
const Object &object = *CTX_data_active_object(C);
const GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
float3 &cursor = reinterpret_cast<float3 &>(scene.cursor.location);
float3 centroid, points_min, points_max;
if (!grease_pencil_snap_compute_centroid(
scene, object, grease_pencil, centroid, points_min, points_max))
{
return OPERATOR_FINISHED;
}
switch (scene.toolsettings->transform_pivot_point) {
case V3D_AROUND_CENTER_BOUNDS:
cursor = math::midpoint(points_min, points_max);
break;
case V3D_AROUND_CENTER_MEDIAN:
case V3D_AROUND_CURSOR:
case V3D_AROUND_LOCAL_ORIGINS:
case V3D_AROUND_ACTIVE:
cursor = centroid;
break;
default:
BLI_assert_unreachable();
}
DEG_id_tag_update(&scene.id, ID_RECALC_SYNC_TO_EVAL);
WM_event_add_notifier(C, NC_SPACE | ND_SPACE_VIEW3D, nullptr);
return OPERATOR_FINISHED;
}
static void GREASE_PENCIL_OT_snap_cursor_to_selected(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Snap Cursor to Selected Points";
ot->idname = "GREASE_PENCIL_OT_snap_cursor_to_selected";
ot->description = "Snap cursor to center of selected points";
/* callbacks */
ot->exec = grease_pencil_snap_cursor_to_sel_exec;
ot->poll = grease_pencil_snap_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
/** \} */
} // namespace blender::ed::greasepencil
void ED_operatortypes_grease_pencil_edit()
@@ -2661,4 +2940,7 @@ void ED_operatortypes_grease_pencil_edit()
WM_operatortype_append(GREASE_PENCIL_OT_paste);
WM_operatortype_append(GREASE_PENCIL_OT_stroke_cutter);
WM_operatortype_append(GREASE_PENCIL_OT_extrude);
WM_operatortype_append(GREASE_PENCIL_OT_snap_to_grid);
WM_operatortype_append(GREASE_PENCIL_OT_snap_to_cursor);
WM_operatortype_append(GREASE_PENCIL_OT_snap_cursor_to_selected);
}