Files
test/source/blender/editors/space_view3d/view3d_navigate.cc
Bastien Montagne 48e26c3afe MEM_guardedalloc: Refactor to add more type-safety.
The main goal of these changes are to improve static (i.e. build-time)
checks on whether a given data can be allocated and freed with `malloc`
and `free` (C-style), or requires proper C++-style construction and
destruction (`new` and `delete`).

* Add new `MEM_malloc_arrayN_aligned` API.
* Make `MEM_freeN` a template function in C++, which does static assert on
  type triviality.
* Add `MEM_SAFE_DELETE`, similar to `MEM_SAFE_FREE` but calling
  `MEM_delete`.

The changes to `MEM_freeN` was painful and useful, as it allowed to fix a bunch
of invalid calls in existing codebase already.

It also highlighted a fair amount of places where it is called to free incomplete
type pointers, which is likely a sign of badly designed code (there should
rather be an API to destroy and free these data then, if the data type is not fully
publicly exposed). For now, these are 'worked around' by explicitly casting the
freed pointers to `void *` in these cases - which also makes them easy to search for.
Some of these will be addressed separately (see blender/blender!134765).

Finally, MSVC seems to consider structs defining new/delete operators (e.g. by
using the `MEM_CXX_CLASS_ALLOC_FUNCS` macro) as non-trivial. This does not
seem to follow the definition of type triviality, so for now static type checking in
`MEM_freeN` has been disabled for Windows. We'll likely have to do the same
with type-safe `MEM_[cm]allocN` API being worked on in blender/blender!134771

Based on ideas from Brecht in blender/blender!134452

Pull Request: https://projects.blender.org/blender/blender/pulls/134463
2025-02-20 10:37:10 +01:00

1169 lines
37 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup spview3d
*/
#include "DNA_curve_types.h"
#include "BLI_listbase.h"
#include "BLI_math_geom.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.hh"
#include "BLI_rect.h"
#include "BKE_context.hh"
#include "BKE_layer.hh"
#include "BKE_object.hh"
#include "BKE_paint.hh"
#include "BKE_vfont.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_screen.hh"
#include "ED_transform.hh"
#include "WM_api.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "view3d_intern.hh"
#include "view3d_navigate.hh" /* own include */
/* Prototypes. */
static const ViewOpsType *view3d_navigation_type_from_idname(const char *idname);
static eViewOpsFlag viewops_flag_from_prefs()
{
const bool use_select = (U.uiflag & USER_ORBIT_SELECTION) != 0;
const bool use_depth = (U.uiflag & USER_DEPTH_NAVIGATE) != 0;
const bool use_zoom_to_mouse = (U.uiflag & USER_ZOOM_TO_MOUSEPOS) != 0;
/**
* If the mode requires it, always set the #VIEWOPS_FLAG_PERSP_ENSURE.
* The function `ED_view3d_persp_ensure` already handles the checking of the preferences.
* And even with the option disabled, in some modes, it is still necessary to exit the camera
* view.
*
* \code{.c}
* const bool use_auto_persp = (U.uiflag & USER_AUTOPERSP) != 0;
* if (use_auto_persp) {
* flag |= VIEWOPS_FLAG_PERSP_ENSURE;
* }
* \endcode
*/
enum eViewOpsFlag flag = VIEWOPS_FLAG_INIT_ZFAC | VIEWOPS_FLAG_PERSP_ENSURE;
if (use_select) {
flag |= VIEWOPS_FLAG_ORBIT_SELECT;
}
if (use_depth) {
flag |= VIEWOPS_FLAG_DEPTH_NAVIGATE;
}
if (use_zoom_to_mouse) {
flag |= VIEWOPS_FLAG_ZOOM_TO_MOUSE;
}
return flag;
}
/* -------------------------------------------------------------------- */
/** \name ViewOpsData definition
* \{ */
void ViewOpsData::init_context(bContext *C)
{
/* Store data. */
this->depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
this->scene = CTX_data_scene(C);
this->area = CTX_wm_area(C);
this->region = CTX_wm_region(C);
this->v3d = static_cast<View3D *>(this->area->spacedata.first);
this->rv3d = static_cast<RegionView3D *>(this->region->regiondata);
}
void ViewOpsData::state_backup()
{
copy_v3_v3(this->init.ofs, rv3d->ofs);
copy_v2_v2(this->init.ofs_lock, rv3d->ofs_lock);
this->init.camdx = rv3d->camdx;
this->init.camdy = rv3d->camdy;
this->init.camzoom = rv3d->camzoom;
this->init.dist = rv3d->dist;
copy_qt_qt(this->init.quat, rv3d->viewquat);
this->init.persp = rv3d->persp;
this->init.view = rv3d->view;
this->init.view_axis_roll = rv3d->view_axis_roll;
}
void ViewOpsData::state_restore()
{
/* DOLLY, MOVE, ROTATE and ZOOM. */
{
/* For Move this only changes when offset is not locked. */
/* For Rotate this only changes when rotating around objects or last-brush. */
/* For Zoom this only changes when zooming to mouse position. */
/* Note this does not remove auto-keys on locked cameras. */
copy_v3_v3(this->rv3d->ofs, this->init.ofs);
}
/* MOVE and ZOOM. */
{
/* For Move this only changes when offset is not locked. */
/* For Zoom this only changes when zooming to mouse position in camera view. */
this->rv3d->camdx = this->init.camdx;
this->rv3d->camdy = this->init.camdy;
}
/* MOVE. */
{
if ((this->rv3d->persp == RV3D_CAMOB) && !ED_view3d_camera_lock_check(this->v3d, this->rv3d)) {
// this->rv3d->camdx = this->init.camdx;
// this->rv3d->camdy = this->init.camdy;
}
else if (ED_view3d_offset_lock_check(this->v3d, this->rv3d)) {
copy_v2_v2(this->rv3d->ofs_lock, this->init.ofs_lock);
}
else {
// copy_v3_v3(vod->rv3d->ofs, vod->init.ofs);
if (RV3D_LOCK_FLAGS(this->rv3d) & RV3D_BOXVIEW) {
view3d_boxview_sync(this->area, this->region);
}
}
}
/* ZOOM. */
{
this->rv3d->camzoom = this->init.camzoom;
}
/* ROTATE and ZOOM. */
{
/* For Rotate this only changes when orbiting from a camera view.
* In this case the `dist` is calculated based on the camera relative to the `ofs`. */
/* Note this does not remove auto-keys on locked cameras. */
this->rv3d->dist = this->init.dist;
}
/* ROLL and ROTATE. */
{
/* Note this does not remove auto-keys on locked cameras. */
copy_qt_qt(this->rv3d->viewquat, this->init.quat);
}
/* ROTATE. */
{
this->rv3d->persp = this->init.persp;
this->rv3d->view = this->init.view;
this->rv3d->view_axis_roll = this->init.view_axis_roll;
}
/* NOTE: there is no need to restore "last" values (as set by #ED_view3d_lastview_store). */
ED_view3d_camera_lock_sync(this->depsgraph, this->v3d, this->rv3d);
}
static eViewOpsFlag navigate_pivot_get(bContext *C,
Depsgraph *depsgraph,
ARegion *region,
View3D *v3d,
const wmEvent *event,
eViewOpsFlag viewops_flag,
const float dyn_ofs_override[3],
float r_pivot[3])
{
if ((viewops_flag & VIEWOPS_FLAG_ORBIT_SELECT) && view3d_orbit_calc_center(C, r_pivot)) {
return VIEWOPS_FLAG_ORBIT_SELECT;
}
wmWindow *win = CTX_wm_window(C);
if (!(viewops_flag & VIEWOPS_FLAG_DEPTH_NAVIGATE)) {
ED_view3d_autodist_last_clear(win);
/* Uses the `lastofs` in #view3d_orbit_calc_center. */
BLI_assert(viewops_flag & VIEWOPS_FLAG_ORBIT_SELECT);
return VIEWOPS_FLAG_ORBIT_SELECT;
}
if (dyn_ofs_override) {
ED_view3d_win_to_3d_int(v3d, region, dyn_ofs_override, event->mval, r_pivot);
return VIEWOPS_FLAG_DEPTH_NAVIGATE;
}
const bool use_depth_last = ED_view3d_autodist_last_check(win, event);
if (use_depth_last) {
ED_view3d_autodist_last_get(win, r_pivot);
}
else {
float fallback_depth_pt[3];
negate_v3_v3(fallback_depth_pt, static_cast<RegionView3D *>(region->regiondata)->ofs);
if (!ED_view3d_has_depth_buffer_updated(depsgraph, v3d)) {
ED_view3d_depth_override(
depsgraph, region, v3d, nullptr, V3D_DEPTH_NO_GPENCIL, true, nullptr);
}
const bool is_set = ED_view3d_autodist(region, v3d, event->mval, r_pivot, fallback_depth_pt);
ED_view3d_autodist_last_set(win, event, r_pivot, is_set);
}
return VIEWOPS_FLAG_DEPTH_NAVIGATE;
}
void ViewOpsData::init_navigation(bContext *C,
const wmEvent *event,
const ViewOpsType *nav_type,
const float dyn_ofs_override[3],
const bool use_cursor_init)
{
using namespace blender;
this->nav_type = nav_type;
eViewOpsFlag viewops_flag = nav_type->flag & viewops_flag_from_prefs();
if (!use_cursor_init) {
viewops_flag &= ~(VIEWOPS_FLAG_DEPTH_NAVIGATE | VIEWOPS_FLAG_ZOOM_TO_MOUSE);
}
bool calc_rv3d_dist = true;
#ifdef WITH_INPUT_NDOF
if (ELEM(nav_type,
&ViewOpsType_ndof_orbit,
&ViewOpsType_ndof_orbit_zoom,
&ViewOpsType_ndof_pan,
&ViewOpsType_ndof_all))
{
calc_rv3d_dist = false;
/* When using "Free" NDOF navigation, ignore "Orbit Around Selected" preference.
* Logically it doesn't make sense to use the selection as a pivot when the first-person
* navigation pivots from the view-point. This also interferes with zoom-speed,
* causing zoom-speed scale based on the distance to the selection center, see: #115253. */
if ((U.ndof_flag & NDOF_MODE_ORBIT) == 0) {
viewops_flag &= ~VIEWOPS_FLAG_ORBIT_SELECT;
}
}
#endif
/* Set the view from the camera, if view locking is enabled.
* we may want to make this optional but for now its needed always. */
ED_view3d_camera_lock_init_ex(depsgraph, v3d, rv3d, calc_rv3d_dist);
this->state_backup();
if (viewops_flag & VIEWOPS_FLAG_PERSP_ENSURE) {
if (ED_view3d_persp_ensure(depsgraph, this->v3d, this->region)) {
/* If we're switching from camera view to the perspective one,
* need to tag viewport update, so camera view and borders are properly updated. */
ED_region_tag_redraw(this->region);
}
}
if (viewops_flag & (VIEWOPS_FLAG_DEPTH_NAVIGATE | VIEWOPS_FLAG_ORBIT_SELECT)) {
float pivot_new[3];
eViewOpsFlag pivot_type = navigate_pivot_get(
C, depsgraph, region, v3d, event, viewops_flag, dyn_ofs_override, pivot_new);
viewops_flag &= ~(VIEWOPS_FLAG_DEPTH_NAVIGATE | VIEWOPS_FLAG_ORBIT_SELECT);
viewops_flag |= pivot_type;
negate_v3_v3(this->dyn_ofs, pivot_new);
this->use_dyn_ofs = true;
if (pivot_type == VIEWOPS_FLAG_DEPTH_NAVIGATE) {
/* Ensure we'll always be able to zoom into the new pivot point and panning won't go bad when
* dist is zero. Therefore, set a new #RegionView3D::ofs and #RegionView3D::dist so that the
* dist value becomes the distance from the new pivot point. */
if (rv3d->is_persp) {
float my_origin[3]; /* Original #RegionView3D.ofs. */
float my_pivot[3]; /* View pivot. */
float dvec[3];
negate_v3_v3(my_origin, rv3d->ofs); /* ofs is flipped */
/* remove dist value */
float3 upvec;
upvec[0] = upvec[1] = 0;
upvec[2] = rv3d->dist;
float3x3 mat = float3x3(float4x4(rv3d->viewinv));
upvec = math::transform_point(mat, upvec);
add_v3_v3v3(my_pivot, my_origin, upvec);
/* find a new ofs value that is along the view axis
* (rather than the mouse location) */
float lambda = closest_to_line_v3(dvec, pivot_new, my_pivot, my_origin);
negate_v3_v3(rv3d->ofs, dvec);
rv3d->dist = len_v3v3(my_pivot, dvec);
if (lambda < 0.0f) {
/* The distance is actually negative. */
rv3d->dist *= -1;
}
}
else {
const float mval_region_mid[2] = {float(region->winx) / 2.0f, float(region->winy) / 2.0f};
ED_view3d_win_to_3d(v3d, region, pivot_new, mval_region_mid, rv3d->ofs);
negate_v3(rv3d->ofs);
}
}
/* Reinitialize `this->init.dist` and `this->init.ofs` as these values may have changed
* when #ED_view3d_persp_ensure was called or when the operator uses `Auto Depth`.
*
* XXX: The initial state captured by #ViewOpsData::state_backup is being modified here.
* This causes the state not to be fully restored when canceling a navigation operation. */
this->init.dist = rv3d->dist;
copy_v3_v3(this->init.ofs, rv3d->ofs);
}
if (viewops_flag & VIEWOPS_FLAG_INIT_ZFAC) {
float tvec[3];
negate_v3_v3(tvec, rv3d->ofs);
this->init.zfac = ED_view3d_calc_zfac(rv3d, tvec);
}
this->init.persp_with_auto_persp_applied = rv3d->persp;
if (event) {
this->init.event_type = event->type;
copy_v2_v2_int(this->init.event_xy, event->xy);
copy_v2_v2_int(this->prev.event_xy, event->xy);
if (use_cursor_init) {
zero_v2_int(this->init.event_xy_offset);
}
else {
/* Simulate the event starting in the middle of the region. */
this->init.event_xy_offset[0] = BLI_rcti_cent_x(&this->region->winrct) - event->xy[0];
this->init.event_xy_offset[1] = BLI_rcti_cent_y(&this->region->winrct) - event->xy[1];
}
/* For dolly */
const float mval[2] = {float(event->mval[0]), float(event->mval[1])};
ED_view3d_win_to_vector(region, mval, this->init.mousevec);
{
int2 event_xy_offset = int2(event->xy) + this->init.event_xy_offset;
/* For rotation with trackball rotation. */
calctrackballvec(&region->winrct, event_xy_offset, this->init.trackvec);
}
}
copy_qt_qt(this->curr.viewquat, rv3d->viewquat);
this->reverse = 1.0f;
if (rv3d->persmat[2][1] < 0.0f) {
this->reverse = -1.0f;
}
this->viewops_flag = viewops_flag;
/* Default. */
this->use_dyn_ofs_ortho_correction = false;
rv3d->rflag |= RV3D_NAVIGATING;
}
void ViewOpsData::end_navigation(bContext *C)
{
this->rv3d->rflag &= ~RV3D_NAVIGATING;
if (this->timer) {
WM_event_timer_remove(CTX_wm_manager(C), this->timer->win, this->timer);
}
/* Cannot use MEM_SAFE_FREE, as #Dial type is only forward-declared in `BLI_dial_2d.h` */
if (this->init.dial) {
MEM_freeN(static_cast<void *>(this->init.dial));
this->init.dial = nullptr;
}
/* Need to redraw because drawing code uses RV3D_NAVIGATING to draw
* faster while navigation operator runs. */
ED_region_tag_redraw(this->region);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Generic Operator Callback Utils
* \{ */
/* Used for navigation utility in operators. */
struct ViewOpsData_Utility : ViewOpsData {
/* To track only the navigation #wmKeyMapItem items and allow changes to them, an internal
* #wmKeyMap is created with their copy. */
ListBase keymap_items;
/* Used by #ED_view3d_navigation_do. */
bool is_modal_event = false;
ViewOpsData_Utility(bContext *C, const wmKeyMapItem *kmi_merge = nullptr)
: ViewOpsData(), keymap_items()
{
this->init_context(C);
wmKeyMap *keymap = WM_keymap_find_all(
CTX_wm_manager(C), "3D View", SPACE_VIEW3D, RGN_TYPE_WINDOW);
WM_keyconfig_update_suppress_begin();
wmKeyMap keymap_tmp = {};
LISTBASE_FOREACH (wmKeyMapItem *, kmi, &keymap->items) {
if (!STRPREFIX(kmi->idname, "VIEW3D")) {
continue;
}
if (kmi->flag & KMI_INACTIVE) {
continue;
}
if (view3d_navigation_type_from_idname(kmi->idname) == nullptr) {
continue;
}
wmKeyMapItem *kmi_cpy = WM_keymap_add_item_copy(&keymap_tmp, kmi);
if (kmi_merge) {
if (kmi_merge->shift == 1 || ELEM(kmi_merge->type, EVT_RIGHTSHIFTKEY, EVT_LEFTSHIFTKEY)) {
kmi_cpy->shift = 1;
}
if (kmi_merge->ctrl == 1 || ELEM(kmi_merge->type, EVT_LEFTCTRLKEY, EVT_RIGHTCTRLKEY)) {
kmi_cpy->ctrl = 1;
}
if (kmi_merge->alt == 1 || ELEM(kmi_merge->type, EVT_LEFTALTKEY, EVT_RIGHTALTKEY)) {
kmi_cpy->alt = 1;
}
if (kmi_merge->oskey == 1 || ELEM(kmi_merge->type, EVT_OSKEY)) {
kmi_cpy->oskey = 1;
}
if (!ISKEYMODIFIER(kmi_merge->type)) {
kmi_cpy->keymodifier = kmi_merge->type;
}
}
}
/* Weak, but only the keymap items from the #wmKeyMap struct are needed here. */
this->keymap_items = keymap_tmp.items;
WM_keyconfig_update_suppress_end();
}
~ViewOpsData_Utility()
{
/* Weak, but rebuild the struct #wmKeyMap to clear the keymap items. */
WM_keyconfig_update_suppress_begin();
wmKeyMap keymap_tmp = {};
keymap_tmp.items = this->keymap_items;
WM_keymap_clear(&keymap_tmp);
WM_keyconfig_update_suppress_end();
}
MEM_CXX_CLASS_ALLOC_FUNCS("ViewOpsData_Utility")
};
static bool view3d_navigation_poll_impl(bContext *C, const char viewlock)
{
if (!ED_operator_region_view3d_active(C)) {
return false;
}
const RegionView3D *rv3d = CTX_wm_region_view3d(C);
return !(RV3D_LOCK_FLAGS(rv3d) & viewlock);
}
static eV3D_OpEvent view3d_navigate_event(ViewOpsData *vod, const wmEvent *event)
{
if (event->type == EVT_MODAL_MAP) {
switch (event->val) {
case VIEW_MODAL_CANCEL:
return VIEW_CANCEL;
case VIEW_MODAL_CONFIRM:
return VIEW_CONFIRM;
case VIEWROT_MODAL_AXIS_SNAP_ENABLE:
vod->axis_snap = true;
return VIEW_APPLY;
case VIEWROT_MODAL_AXIS_SNAP_DISABLE:
vod->rv3d->persp = vod->init.persp_with_auto_persp_applied;
vod->axis_snap = false;
return VIEW_APPLY;
case VIEWROT_MODAL_SWITCH_ZOOM:
case VIEWROT_MODAL_SWITCH_MOVE:
case VIEWROT_MODAL_SWITCH_ROTATE: {
const ViewOpsType *nav_type_new = (event->val == VIEWROT_MODAL_SWITCH_ZOOM) ?
&ViewOpsType_zoom :
(event->val == VIEWROT_MODAL_SWITCH_MOVE) ?
&ViewOpsType_move :
&ViewOpsType_rotate;
if (nav_type_new == vod->nav_type) {
break;
}
vod->nav_type = nav_type_new;
return VIEW_APPLY;
}
}
}
else {
if (event->type == TIMER && event->customdata == vod->timer) {
/* Zoom uses timer for continuous zoom. */
return VIEW_APPLY;
}
if (event->type == MOUSEMOVE) {
return VIEW_APPLY;
}
if (event->type == vod->init.event_type && event->val == KM_RELEASE) {
return VIEW_CONFIRM;
}
if (event->type == EVT_ESCKEY && event->val == KM_PRESS) {
return VIEW_CANCEL;
}
}
return VIEW_PASS;
}
static int view3d_navigation_invoke_generic(bContext *C,
ViewOpsData *vod,
const wmEvent *event,
PointerRNA *ptr,
const ViewOpsType *nav_type,
const float dyn_ofs_override[3])
{
if (!nav_type->init_fn) {
return OPERATOR_CANCELLED;
}
bool use_cursor_init = false;
if (PropertyRNA *prop = RNA_struct_find_property(ptr, "use_cursor_init")) {
use_cursor_init = RNA_property_boolean_get(ptr, prop);
}
vod->init_navigation(C, event, nav_type, dyn_ofs_override, use_cursor_init);
ED_view3d_smooth_view_force_finish(C, vod->v3d, vod->region);
return nav_type->init_fn(C, vod, event, ptr);
}
int view3d_navigate_invoke_impl(bContext *C,
wmOperator *op,
const wmEvent *event,
const ViewOpsType *nav_type)
{
ViewOpsData *vod = new ViewOpsData();
vod->init_context(C);
int ret = view3d_navigation_invoke_generic(C, vod, event, op->ptr, nav_type, nullptr);
op->customdata = (void *)vod;
if (ret == OPERATOR_RUNNING_MODAL) {
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
}
viewops_data_free(C, vod);
op->customdata = nullptr;
return ret;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Generic Callbacks
* \{ */
bool view3d_location_poll(bContext *C)
{
return view3d_navigation_poll_impl(C, RV3D_LOCK_LOCATION);
}
bool view3d_rotation_poll(bContext *C)
{
return view3d_navigation_poll_impl(C, RV3D_LOCK_ROTATION);
}
bool view3d_zoom_or_dolly_poll(bContext *C)
{
return view3d_navigation_poll_impl(C, RV3D_LOCK_ZOOM_AND_DOLLY);
}
bool view3d_zoom_or_dolly_or_rotation_poll(bContext *C)
{
/* This combination of flags is needed for the dolly operator,
* see code-comments there for details. */
return view3d_navigation_poll_impl(C, RV3D_LOCK_ZOOM_AND_DOLLY | RV3D_LOCK_ROTATION);
}
int view3d_navigate_modal_fn(bContext *C, wmOperator *op, const wmEvent *event)
{
ViewOpsData *vod = static_cast<ViewOpsData *>(op->customdata);
const ViewOpsType *nav_type_prev = vod->nav_type;
const eV3D_OpEvent event_code = view3d_navigate_event(vod, event);
if (nav_type_prev != vod->nav_type) {
wmOperatorType *ot_new = WM_operatortype_find(vod->nav_type->idname, false);
WM_operator_type_set(op, ot_new);
vod->end_navigation(C);
return view3d_navigation_invoke_generic(C, vod, event, op->ptr, vod->nav_type, nullptr);
}
int ret = vod->nav_type->apply_fn(C, vod, event_code, event->xy);
if ((ret & OPERATOR_RUNNING_MODAL) == 0) {
if (ret & OPERATOR_FINISHED) {
ED_view3d_camera_lock_undo_push(op->type->name, vod->v3d, vod->rv3d, C);
}
viewops_data_free(C, vod);
op->customdata = nullptr;
}
return ret;
}
void view3d_navigate_cancel_fn(bContext *C, wmOperator *op)
{
viewops_data_free(C, static_cast<ViewOpsData *>(op->customdata));
op->customdata = nullptr;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Generic View Operator Properties
* \{ */
void view3d_operator_properties_common(wmOperatorType *ot, const enum eV3D_OpPropFlag flag)
{
if (flag & V3D_OP_PROP_MOUSE_CO) {
PropertyRNA *prop;
prop = RNA_def_int(ot->srna, "mx", 0, 0, INT_MAX, "Region Position X", "", 0, INT_MAX);
RNA_def_property_flag(prop, PROP_HIDDEN);
prop = RNA_def_int(ot->srna, "my", 0, 0, INT_MAX, "Region Position Y", "", 0, INT_MAX);
RNA_def_property_flag(prop, PROP_HIDDEN);
}
if (flag & V3D_OP_PROP_DELTA) {
RNA_def_int(ot->srna, "delta", 0, INT_MIN, INT_MAX, "Delta", "", INT_MIN, INT_MAX);
}
if (flag & V3D_OP_PROP_USE_ALL_REGIONS) {
PropertyRNA *prop;
prop = RNA_def_boolean(
ot->srna, "use_all_regions", false, "All Regions", "View selected for all regions");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
if (flag & V3D_OP_PROP_USE_MOUSE_INIT) {
WM_operator_properties_use_cursor_init(ot);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Generic View Operator Custom-Data
* \{ */
void calctrackballvec(const rcti *rect, const int event_xy[2], float r_dir[3])
{
const float radius = V3D_OP_TRACKBALLSIZE;
const float t = radius / float(M_SQRT2);
const float size[2] = {float(BLI_rcti_size_x(rect)), float(BLI_rcti_size_y(rect))};
/* Aspect correct so dragging in a non-square view doesn't squash the direction.
* So diagonal motion rotates the same direction the cursor is moving. */
const float size_min = min_ff(size[0], size[1]);
const float aspect[2] = {size_min / size[0], size_min / size[1]};
/* Normalize x and y. */
r_dir[0] = (event_xy[0] - BLI_rcti_cent_x(rect)) / ((size[0] * aspect[0]) / 2.0);
r_dir[1] = (event_xy[1] - BLI_rcti_cent_y(rect)) / ((size[1] * aspect[1]) / 2.0);
const float d = len_v2(r_dir);
if (d < t) {
/* Inside sphere. */
r_dir[2] = sqrtf(square_f(radius) - square_f(d));
}
else {
/* On hyperbola. */
r_dir[2] = square_f(t) / d;
}
}
void view3d_orbit_apply_dyn_ofs(float r_ofs[3],
const float ofs_old[3],
const float viewquat_old[4],
const float viewquat_new[4],
const float dyn_ofs[3])
{
float q[4];
invert_qt_qt_normalized(q, viewquat_old);
mul_qt_qtqt(q, q, viewquat_new);
invert_qt_normalized(q);
sub_v3_v3v3(r_ofs, ofs_old, dyn_ofs);
mul_qt_v3(q, r_ofs);
add_v3_v3(r_ofs, dyn_ofs);
}
static void view3d_orbit_apply_dyn_ofs_ortho_correction(float ofs[3],
const float viewquat_old[4],
const float viewquat_new[4],
const float dyn_ofs[3])
{
/* NOTE(@ideasman42): While orbiting in orthographic mode the "depth" of the offset
* (position along the views Z-axis) is only noticeable when the view contents is clipped.
* The likelihood of clipping depends on the clipping range & size of the scene.
* In practice some users might not run into this, however using dynamic-offset in
* orthographic views can cause the depth of the offset to drift while navigating the view,
* causing unexpected clipping that seems like a bug from the user perspective, see: #104385.
*
* Imagine a camera is focused on a distant object. Now imagine a closer object in front of
* the camera is used as a pivot, the camera is rotated to view it from the side (~90d rotation).
* The outcome is the camera is now focused on a distant region to the left/right.
* The new focal point is unlikely to point to anything useful (unless by accident).
* Instead of a focal point - the `rv3d->ofs` is being manipulated in this case.
*
* Resolve by moving #RegionView3D::ofs so it is depth-aligned to `dyn_ofs`,
* this is interpolated by the amount of rotation so minor rotations don't cause
* the view-clipping to suddenly jump.
*
* Perspective Views
* =================
*
* This logic could also be applied to perspective views because the issue of the `ofs`
* being a location which isn't useful exists there too, however the problem where this location
* impacts the clipping does *not* exist, as the clipping range starts from the view-point
* (`ofs` + `dist` along the view Z-axis) unlike orthographic views which center around `ofs`.
* Nevertheless there will be cases when having `ofs` and a large `dist` pointing nowhere doesn't
* give ideal behavior (zooming may jump in larger than expected steps and panning the view may
* move too much in relation to nearby objects - for e.g.). So it's worth investigating but
* should be done with extra care as changing `ofs` in perspective view also requires changing
* the `dist` which could cause unexpected results if the calculated `dist` happens to be small.
* So disable this workaround in perspective view unless there are clear benefits to enabling. */
float q_inv[4];
float view_z_init[3] = {0.0f, 0.0f, 1.0f};
invert_qt_qt_normalized(q_inv, viewquat_old);
mul_qt_v3(q_inv, view_z_init);
float view_z_curr[3] = {0.0f, 0.0f, 1.0f};
invert_qt_qt_normalized(q_inv, viewquat_new);
mul_qt_v3(q_inv, view_z_curr);
const float angle_cos = max_ff(0.0f, dot_v3v3(view_z_init, view_z_curr));
/* 1.0 or more means no rotation, there is nothing to do in that case. */
if (LIKELY(angle_cos < 1.0f)) {
const float dot_ofs_curr = dot_v3v3(view_z_curr, ofs);
const float dot_ofs_next = dot_v3v3(view_z_curr, dyn_ofs);
const float ofs_delta = dot_ofs_next - dot_ofs_curr;
if (LIKELY(ofs_delta != 0.0f)) {
/* Calculate a factor where 0.0 represents no rotation and 1.0 represents 90d or more.
* NOTE: Without applying the factor, the distances immediately changes
* (useful for testing), but not good for the users experience as minor rotations
* should not immediately adjust the depth. */
const float factor = acosf(angle_cos) / M_PI_2;
madd_v3_v3fl(ofs, view_z_curr, ofs_delta * factor);
}
}
}
void viewrotate_apply_dyn_ofs(ViewOpsData *vod, const float viewquat_new[4])
{
if (vod->use_dyn_ofs) {
RegionView3D *rv3d = vod->rv3d;
view3d_orbit_apply_dyn_ofs(
rv3d->ofs, vod->init.ofs, vod->init.quat, viewquat_new, vod->dyn_ofs);
if (vod->use_dyn_ofs_ortho_correction) {
view3d_orbit_apply_dyn_ofs_ortho_correction(
rv3d->ofs, vod->init.quat, viewquat_new, vod->dyn_ofs);
}
}
}
bool view3d_orbit_calc_center(bContext *C, float r_dyn_ofs[3])
{
using namespace blender;
static float3 lastofs = float3(0);
bool is_set = false;
const Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Scene *scene = CTX_data_scene(C);
Scene *scene_eval = DEG_get_evaluated_scene(depsgraph);
ViewLayer *view_layer_eval = DEG_get_evaluated_view_layer(depsgraph);
View3D *v3d = CTX_wm_view3d(C);
BKE_view_layer_synced_ensure(scene_eval, view_layer_eval);
Object *ob_act_eval = BKE_view_layer_active_object_get(view_layer_eval);
Object *ob_act = DEG_get_original_object(ob_act_eval);
if (ob_act && (ob_act->mode & OB_MODE_ALL_PAINT) &&
/* with weight-paint + pose-mode, fall through to using calculateTransformCenter */
((ob_act->mode & OB_MODE_WEIGHT_PAINT) && BKE_object_pose_armature_get(ob_act)) == 0)
{
BKE_paint_stroke_get_average(scene, ob_act_eval, lastofs);
is_set = true;
}
else if (ob_act && (ob_act->mode & OB_MODE_SCULPT_CURVES)) {
BKE_paint_stroke_get_average(scene, ob_act_eval, lastofs);
is_set = true;
}
else if (ob_act && (ob_act->mode & OB_MODE_EDIT) && (ob_act->type == OB_FONT)) {
Curve *cu = static_cast<Curve *>(ob_act_eval->data);
EditFont *ef = cu->editfont;
lastofs = float3(0);
for (int i = 0; i < 4; i++) {
lastofs += ef->textcurs[i];
}
lastofs *= 0.25f;
lastofs = math::transform_point(ob_act_eval->object_to_world(), lastofs);
is_set = true;
}
else if (ob_act == nullptr || ob_act->mode == OB_MODE_OBJECT) {
/* Object mode uses bounding-box centers. */
int total = 0;
float3 select_center(0);
zero_v3(select_center);
LISTBASE_FOREACH (const Base *, base_eval, BKE_view_layer_object_bases_get(view_layer_eval)) {
if (BASE_SELECTED(v3d, base_eval)) {
/* Use the bounding-box if we can. */
const Object *ob_eval = base_eval->object;
if (const std::optional<Bounds<float3>> bounds = BKE_object_boundbox_get(ob_eval)) {
const float3 center = math::midpoint(bounds->min, bounds->max);
select_center += math::transform_point(ob_eval->object_to_world(), center);
}
else {
add_v3_v3(select_center, ob_eval->object_to_world().location());
}
total++;
}
}
if (total) {
mul_v3_fl(select_center, 1.0f / float(total));
copy_v3_v3(lastofs, select_center);
is_set = true;
}
}
else {
/* If there's no selection, `lastofs` is unmodified and last value since static. */
is_set = blender::ed::transform::calc_pivot_pos(C, V3D_AROUND_CENTER_MEDIAN, lastofs);
}
copy_v3_v3(r_dyn_ofs, lastofs);
return is_set;
}
ViewOpsData *viewops_data_create(bContext *C,
const wmEvent *event,
const ViewOpsType *nav_type,
const bool use_cursor_init)
{
ViewOpsData *vod = new ViewOpsData();
vod->init_context(C);
vod->init_navigation(C, event, nav_type, nullptr, use_cursor_init);
return vod;
}
void viewops_data_free(bContext *C, ViewOpsData *vod)
{
if (!vod) {
return;
}
vod->end_navigation(C);
delete vod;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Generic View Operator Utilities
* \{ */
/**
* \param align_to_quat: When not nullptr, set the axis relative to this rotation.
*/
void axis_set_view(bContext *C,
View3D *v3d,
ARegion *region,
const float quat_[4],
char view,
char view_axis_roll,
int perspo,
const float *align_to_quat,
const int smooth_viewtx)
{
/* no nullptr check is needed, poll checks */
RegionView3D *rv3d = static_cast<RegionView3D *>(region->regiondata);
float quat[4];
const short orig_persp = rv3d->persp;
const char orig_view = rv3d->view;
const char orig_view_axis_roll = rv3d->view_axis_roll;
normalize_qt_qt(quat, quat_);
if (align_to_quat) {
mul_qt_qtqt(quat, quat, align_to_quat);
rv3d->view = view = RV3D_VIEW_USER;
rv3d->view_axis_roll = RV3D_VIEW_AXIS_ROLL_0;
}
else {
rv3d->view = view;
rv3d->view_axis_roll = view_axis_roll;
}
/* Redrawing when changes are detected is needed because the current view
* orientation may be a "User" view that matches the axis exactly.
* In this case smooth-view exits early as no view transition is needed.
* However, changing the view must redraw the region as it changes the
* viewport name & grid drawing. */
if ((rv3d->view != orig_view) || (rv3d->view_axis_roll != orig_view_axis_roll)) {
ED_region_tag_redraw(region);
}
if (RV3D_LOCK_FLAGS(rv3d) & RV3D_LOCK_ROTATION) {
return;
}
if (U.uiflag & USER_AUTOPERSP) {
rv3d->persp = RV3D_VIEW_IS_AXIS(view) ? RV3D_ORTHO : perspo;
}
else if (rv3d->persp == RV3D_CAMOB) {
rv3d->persp = perspo;
}
if (rv3d->persp != orig_persp) {
ED_region_tag_redraw(region);
}
if (rv3d->persp == RV3D_CAMOB && v3d->camera) {
/* to camera */
V3D_SmoothParams sview = {nullptr};
sview.camera_old = v3d->camera;
sview.ofs = rv3d->ofs;
sview.quat = quat;
/* No undo because this switches to/from camera. */
sview.undo_str = nullptr;
ED_view3d_smooth_view(C, v3d, region, smooth_viewtx, &sview);
}
else if (orig_persp == RV3D_CAMOB && v3d->camera) {
/* from camera */
float ofs[3], dist;
copy_v3_v3(ofs, rv3d->ofs);
dist = rv3d->dist;
/* so we animate _from_ the camera location */
Object *camera_eval = DEG_get_evaluated_object(CTX_data_ensure_evaluated_depsgraph(C),
v3d->camera);
ED_view3d_from_object(camera_eval, rv3d->ofs, nullptr, &rv3d->dist, nullptr);
V3D_SmoothParams sview = {nullptr};
sview.camera_old = camera_eval;
sview.ofs = ofs;
sview.quat = quat;
sview.dist = &dist;
/* No undo because this switches to/from camera. */
sview.undo_str = nullptr;
ED_view3d_smooth_view(C, v3d, region, smooth_viewtx, &sview);
}
else {
/* rotate around selection */
const float *dyn_ofs_pt = nullptr;
float dyn_ofs[3];
if (U.uiflag & USER_ORBIT_SELECTION) {
if (view3d_orbit_calc_center(C, dyn_ofs)) {
negate_v3(dyn_ofs);
dyn_ofs_pt = dyn_ofs;
}
}
/* no camera involved */
V3D_SmoothParams sview = {nullptr};
sview.quat = quat;
sview.dyn_ofs = dyn_ofs_pt;
/* No undo because this switches to/from camera. */
sview.undo_str = nullptr;
ED_view3d_smooth_view(C, v3d, region, smooth_viewtx, &sview);
}
}
void viewmove_apply(ViewOpsData *vod, int x, int y)
{
const float event_ofs[2] = {
float(vod->prev.event_xy[0] - x),
float(vod->prev.event_xy[1] - y),
};
if ((vod->rv3d->persp == RV3D_CAMOB) && !ED_view3d_camera_lock_check(vod->v3d, vod->rv3d)) {
ED_view3d_camera_view_pan(vod->region, event_ofs);
}
else if (ED_view3d_offset_lock_check(vod->v3d, vod->rv3d)) {
vod->rv3d->ofs_lock[0] -= (event_ofs[0] * 2.0f) / float(vod->region->winx);
vod->rv3d->ofs_lock[1] -= (event_ofs[1] * 2.0f) / float(vod->region->winy);
}
else {
float dvec[3];
ED_view3d_win_to_delta(vod->region, event_ofs, vod->init.zfac, dvec);
sub_v3_v3(vod->rv3d->ofs, dvec);
if (RV3D_LOCK_FLAGS(vod->rv3d) & RV3D_BOXVIEW) {
view3d_boxview_sync(vod->area, vod->region);
}
}
vod->prev.event_xy[0] = x;
vod->prev.event_xy[1] = y;
ED_view3d_camera_lock_sync(vod->depsgraph, vod->v3d, vod->rv3d);
ED_region_tag_redraw(vod->region);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Navigation Utilities
* \{ */
/* Detect the navigation operation, by the name of the navigation operator (obtained by
* `wmKeyMapItem::idname`) */
static const ViewOpsType *view3d_navigation_type_from_idname(const char *idname)
{
const blender::Array<const ViewOpsType *> nav_types = {
&ViewOpsType_zoom,
&ViewOpsType_rotate,
&ViewOpsType_move,
&ViewOpsType_pan,
// &ViewOpsType_orbit,
// &ViewOpsType_roll,
// &ViewOpsType_dolly,
#ifdef WITH_INPUT_NDOF
&ViewOpsType_ndof_orbit,
&ViewOpsType_ndof_orbit_zoom,
&ViewOpsType_ndof_pan,
&ViewOpsType_ndof_all,
#endif
};
const char *op_name = idname + sizeof("VIEW3D_OT_");
for (const ViewOpsType *nav_type : nav_types) {
if (STREQ(op_name, nav_type->idname + sizeof("VIEW3D_OT_"))) {
return nav_type;
}
}
return nullptr;
}
ViewOpsData *ED_view3d_navigation_init(bContext *C, const wmKeyMapItem *kmi_merge)
{
/* Unlike `viewops_data_create`, `ED_view3d_navigation_init` creates a navigation context along
* with an array of `wmKeyMapItem`s used for navigation. */
if (!CTX_wm_region_view3d(C)) {
return nullptr;
}
return new ViewOpsData_Utility(C, kmi_merge);
}
bool ED_view3d_navigation_do(bContext *C,
ViewOpsData *vod,
const wmEvent *event,
const float depth_loc_override[3])
{
if (!vod) {
return false;
}
wmEvent event_tmp;
if (event->type == EVT_MODAL_MAP) {
/* Workaround to use the original event values. */
event_tmp = *event;
event_tmp.type = event->prev_type;
event_tmp.val = event->prev_val;
event = &event_tmp;
}
int op_return = OPERATOR_CANCELLED;
ViewOpsData_Utility *vod_intern = static_cast<ViewOpsData_Utility *>(vod);
if (vod_intern->is_modal_event) {
const eV3D_OpEvent event_code = view3d_navigate_event(vod, event);
op_return = vod->nav_type->apply_fn(C, vod, event_code, event->xy);
if (op_return != OPERATOR_RUNNING_MODAL) {
vod->end_navigation(C);
vod_intern->is_modal_event = false;
}
}
else {
LISTBASE_FOREACH (wmKeyMapItem *, kmi, &vod_intern->keymap_items) {
if (!WM_event_match(event, kmi)) {
continue;
}
const ViewOpsType *nav_type = view3d_navigation_type_from_idname(kmi->idname);
if (nav_type->poll_fn && !nav_type->poll_fn(C)) {
break;
}
op_return = view3d_navigation_invoke_generic(
C, vod, event, kmi->ptr, nav_type, depth_loc_override);
if (op_return == OPERATOR_RUNNING_MODAL) {
vod_intern->is_modal_event = true;
}
else {
vod->end_navigation(C);
/* Postpone the navigation confirmation to the next call.
* This avoids constant updating of the transform operation for example. */
vod->rv3d->rflag |= RV3D_NAVIGATING;
}
break;
}
}
if (op_return != OPERATOR_CANCELLED) {
/* Although #ED_view3d_update_viewmat is already called when redrawing the 3D View, do it here
* as well, so the updated matrix values can be accessed by the operator. */
ED_view3d_update_viewmat(
vod->depsgraph, vod->scene, vod->v3d, vod->region, nullptr, nullptr, nullptr, false);
return true;
}
if (vod->rv3d->rflag & RV3D_NAVIGATING) {
/* Add a fake confirmation. */
vod->rv3d->rflag &= ~RV3D_NAVIGATING;
return true;
}
return false;
}
void ED_view3d_navigation_free(bContext *C, ViewOpsData *vod)
{
ViewOpsData_Utility *vod_intern = static_cast<ViewOpsData_Utility *>(vod);
vod_intern->end_navigation(C);
delete vod_intern;
}
/** \} */