/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** \file * \ingroup spview3d */ #include "BLI_math.h" #include "BKE_context.h" #include "WM_api.h" #include "RNA_access.h" #include "ED_screen.h" #include "view3d_intern.h" #include "view3d_navigate.h" /* own include */ /* -------------------------------------------------------------------- */ /** \name View Rotate Operator * \{ */ void viewrotate_modal_keymap(wmKeyConfig *keyconf) { static const EnumPropertyItem modal_items[] = { {VIEW_MODAL_CONFIRM, "CONFIRM", 0, "Confirm", ""}, {VIEWROT_MODAL_AXIS_SNAP_ENABLE, "AXIS_SNAP_ENABLE", 0, "Axis Snap", ""}, {VIEWROT_MODAL_AXIS_SNAP_DISABLE, "AXIS_SNAP_DISABLE", 0, "Axis Snap (Off)", ""}, {VIEWROT_MODAL_SWITCH_ZOOM, "SWITCH_TO_ZOOM", 0, "Switch to Zoom"}, {VIEWROT_MODAL_SWITCH_MOVE, "SWITCH_TO_MOVE", 0, "Switch to Move"}, {0, NULL, 0, NULL, NULL}, }; wmKeyMap *keymap = WM_modalkeymap_find(keyconf, "View3D Rotate Modal"); /* this function is called for each spacetype, only needs to add map once */ if (keymap && keymap->modal_items) { return; } keymap = WM_modalkeymap_ensure(keyconf, "View3D Rotate Modal", modal_items); /* disabled mode switching for now, can re-implement better, later on */ #if 0 WM_modalkeymap_add_item(keymap, LEFTMOUSE, KM_PRESS, KM_ANY, 0, VIEWROT_MODAL_SWITCH_ZOOM); WM_modalkeymap_add_item(keymap, LEFTCTRLKEY, KM_PRESS, KM_ANY, 0, VIEWROT_MODAL_SWITCH_ZOOM); WM_modalkeymap_add_item(keymap, LEFTSHIFTKEY, KM_PRESS, KM_ANY, 0, VIEWROT_MODAL_SWITCH_MOVE); #endif /* assign map to operators */ WM_modalkeymap_assign(keymap, "VIEW3D_OT_rotate"); } static void viewrotate_apply_snap(ViewOpsData *vod) { const float axis_limit = DEG2RADF(45 / 3); RegionView3D *rv3d = vod->rv3d; float viewquat_inv[4]; float zaxis[3] = {0, 0, 1}; float zaxis_best[3]; int x, y, z; bool found = false; invert_qt_qt_normalized(viewquat_inv, vod->curr.viewquat); mul_qt_v3(viewquat_inv, zaxis); normalize_v3(zaxis); for (x = -1; x < 2; x++) { for (y = -1; y < 2; y++) { for (z = -1; z < 2; z++) { if (x || y || z) { float zaxis_test[3] = {x, y, z}; normalize_v3(zaxis_test); if (angle_normalized_v3v3(zaxis_test, zaxis) < axis_limit) { copy_v3_v3(zaxis_best, zaxis_test); found = true; } } } } } if (found) { /* find the best roll */ float quat_roll[4], quat_final[4], quat_best[4], quat_snap[4]; float viewquat_align[4]; /* viewquat aligned to zaxis_best */ float viewquat_align_inv[4]; /* viewquat aligned to zaxis_best */ float best_angle = axis_limit; int j; /* viewquat_align is the original viewquat aligned to the snapped axis * for testing roll */ rotation_between_vecs_to_quat(viewquat_align, zaxis_best, zaxis); normalize_qt(viewquat_align); mul_qt_qtqt(viewquat_align, vod->curr.viewquat, viewquat_align); normalize_qt(viewquat_align); invert_qt_qt_normalized(viewquat_align_inv, viewquat_align); vec_to_quat(quat_snap, zaxis_best, OB_NEGZ, OB_POSY); normalize_qt(quat_snap); invert_qt_normalized(quat_snap); /* check if we can find the roll */ found = false; /* find best roll */ for (j = 0; j < 8; j++) { float angle; float xaxis1[3] = {1, 0, 0}; float xaxis2[3] = {1, 0, 0}; float quat_final_inv[4]; axis_angle_to_quat(quat_roll, zaxis_best, (float)j * DEG2RADF(45.0f)); normalize_qt(quat_roll); mul_qt_qtqt(quat_final, quat_snap, quat_roll); normalize_qt(quat_final); /* compare 2 vector angles to find the least roll */ invert_qt_qt_normalized(quat_final_inv, quat_final); mul_qt_v3(viewquat_align_inv, xaxis1); mul_qt_v3(quat_final_inv, xaxis2); angle = angle_v3v3(xaxis1, xaxis2); if (angle <= best_angle) { found = true; best_angle = angle; copy_qt_qt(quat_best, quat_final); } } if (found) { /* lock 'quat_best' to an axis view if we can */ ED_view3d_quat_to_axis_view(quat_best, 0.01f, &rv3d->view, &rv3d->view_axis_roll); if (rv3d->view != RV3D_VIEW_USER) { ED_view3d_quat_from_axis_view(rv3d->view, rv3d->view_axis_roll, quat_best); } } else { copy_qt_qt(quat_best, viewquat_align); } copy_qt_qt(rv3d->viewquat, quat_best); viewrotate_apply_dyn_ofs(vod, rv3d->viewquat); if (U.uiflag & USER_AUTOPERSP) { if (RV3D_VIEW_IS_AXIS(rv3d->view)) { if (rv3d->persp == RV3D_PERSP) { rv3d->persp = RV3D_ORTHO; } } } } else if (U.uiflag & USER_AUTOPERSP) { rv3d->persp = vod->init.persp; } } static void viewrotate_apply(ViewOpsData *vod, const int event_xy[2]) { RegionView3D *rv3d = vod->rv3d; rv3d->view = RV3D_VIEW_USER; /* need to reset every time because of view snapping */ if (U.flag & USER_TRACKBALL) { float axis[3], q1[4], dvec[3], newvec[3]; float angle; { const int event_xy_offset[2] = { event_xy[0] + vod->init.event_xy_offset[0], event_xy[1] + vod->init.event_xy_offset[1], }; calctrackballvec(&vod->region->winrct, event_xy_offset, newvec); } sub_v3_v3v3(dvec, newvec, vod->init.trackvec); angle = (len_v3(dvec) / (2.0f * V3D_OP_TRACKBALLSIZE)) * (float)M_PI; /* Before applying the sensitivity this is rotating 1:1, * where the cursor would match the surface of a sphere in the view. */ angle *= U.view_rotate_sensitivity_trackball; /* Allow for rotation beyond the interval [-pi, pi] */ angle = angle_wrap_rad(angle); /* This relation is used instead of the actual angle between vectors * so that the angle of rotation is linearly proportional to * the distance that the mouse is dragged. */ cross_v3_v3v3(axis, vod->init.trackvec, newvec); axis_angle_to_quat(q1, axis, angle); mul_qt_qtqt(vod->curr.viewquat, q1, vod->init.quat); viewrotate_apply_dyn_ofs(vod, vod->curr.viewquat); } else { float quat_local_x[4], quat_global_z[4]; float m[3][3]; float m_inv[3][3]; const float zvec_global[3] = {0.0f, 0.0f, 1.0f}; float xaxis[3]; /* Radians per-pixel. */ const float sensitivity = U.view_rotate_sensitivity_turntable / U.dpi_fac; /* Get the 3x3 matrix and its inverse from the quaternion */ quat_to_mat3(m, vod->curr.viewquat); invert_m3_m3(m_inv, m); /* Avoid Gimbal Lock * * Even though turn-table mode is in use, this can occur when the user exits the camera view * or when aligning the view to a rotated object. * * We have gimbal lock when the user's view is rotated +/- 90 degrees along the view axis. * In this case the vertical rotation is the same as the sideways turntable motion. * Making it impossible to get out of the gimbal locked state without resetting the view. * * The logic below lets the user exit out of this state without any abrupt 'fix' * which would be disorienting. * * This works by blending two horizons: * - Rotated-horizon: `cross_v3_v3v3(xaxis, zvec_global, m_inv[2])` * When only this is used, this turntable rotation works - but it's side-ways * (as if the entire turn-table has been placed on its side) * While there is no gimbal lock, it's also awkward to use. * - Un-rotated-horizon: `m_inv[0]` * When only this is used, the turntable rotation can have gimbal lock. * * The solution used here is to blend between these two values, * so the severity of the gimbal lock is used to blend the rotated horizon. * Blending isn't essential, it just makes the transition smoother. * * This allows sideways turn-table rotation on a Z axis that isn't world-space Z, * While up-down turntable rotation eventually corrects gimbal lock. */ #if 1 if (len_squared_v3v3(zvec_global, m_inv[2]) > 0.001f) { float fac; cross_v3_v3v3(xaxis, zvec_global, m_inv[2]); if (dot_v3v3(xaxis, m_inv[0]) < 0) { negate_v3(xaxis); } fac = angle_normalized_v3v3(zvec_global, m_inv[2]) / (float)M_PI; fac = fabsf(fac - 0.5f) * 2; fac = fac * fac; interp_v3_v3v3(xaxis, xaxis, m_inv[0], fac); } else { copy_v3_v3(xaxis, m_inv[0]); } #else copy_v3_v3(xaxis, m_inv[0]); #endif /* Determine the direction of the x vector (for rotating up and down) */ /* This can likely be computed directly from the quaternion. */ /* Perform the up/down rotation */ axis_angle_to_quat(quat_local_x, xaxis, sensitivity * -(event_xy[1] - vod->prev.event_xy[1])); mul_qt_qtqt(quat_local_x, vod->curr.viewquat, quat_local_x); /* Perform the orbital rotation */ axis_angle_to_quat_single( quat_global_z, 'Z', sensitivity * vod->reverse * (event_xy[0] - vod->prev.event_xy[0])); mul_qt_qtqt(vod->curr.viewquat, quat_local_x, quat_global_z); viewrotate_apply_dyn_ofs(vod, vod->curr.viewquat); } /* avoid precision loss over time */ normalize_qt(vod->curr.viewquat); /* use a working copy so view rotation locking doesn't overwrite the locked * rotation back into the view we calculate with */ copy_qt_qt(rv3d->viewquat, vod->curr.viewquat); /* Check for view snap, * NOTE: don't apply snap to `vod->viewquat` so the view won't jam up. */ if (vod->axis_snap) { viewrotate_apply_snap(vod); } vod->prev.event_xy[0] = event_xy[0]; vod->prev.event_xy[1] = event_xy[1]; ED_view3d_camera_lock_sync(vod->depsgraph, vod->v3d, rv3d); ED_region_tag_redraw(vod->region); } static int viewrotate_modal(bContext *C, wmOperator *op, const wmEvent *event) { ViewOpsData *vod = op->customdata; short event_code = VIEW_PASS; bool use_autokey = false; int ret = OPERATOR_RUNNING_MODAL; /* execute the events */ if (event->type == MOUSEMOVE) { event_code = VIEW_APPLY; } else if (event->type == EVT_MODAL_MAP) { switch (event->val) { case VIEW_MODAL_CONFIRM: event_code = VIEW_CONFIRM; break; case VIEWROT_MODAL_AXIS_SNAP_ENABLE: vod->axis_snap = true; event_code = VIEW_APPLY; break; case VIEWROT_MODAL_AXIS_SNAP_DISABLE: vod->rv3d->persp = vod->init.persp; vod->axis_snap = false; event_code = VIEW_APPLY; break; case VIEWROT_MODAL_SWITCH_ZOOM: WM_operator_name_call(C, "VIEW3D_OT_zoom", WM_OP_INVOKE_DEFAULT, NULL); event_code = VIEW_CONFIRM; break; case VIEWROT_MODAL_SWITCH_MOVE: WM_operator_name_call(C, "VIEW3D_OT_move", WM_OP_INVOKE_DEFAULT, NULL); event_code = VIEW_CONFIRM; break; } } else if (event->type == vod->init.event_type && event->val == KM_RELEASE) { event_code = VIEW_CONFIRM; } if (event_code == VIEW_APPLY) { viewrotate_apply(vod, event->xy); if (ED_screen_animation_playing(CTX_wm_manager(C))) { use_autokey = true; } } else if (event_code == VIEW_CONFIRM) { use_autokey = true; ret = OPERATOR_FINISHED; } if (use_autokey) { ED_view3d_camera_lock_autokey(vod->v3d, vod->rv3d, C, true, true); } if (ret & OPERATOR_FINISHED) { viewops_data_free(C, op->customdata); op->customdata = NULL; } return ret; } static int viewrotate_invoke(bContext *C, wmOperator *op, const wmEvent *event) { ViewOpsData *vod; const bool use_cursor_init = RNA_boolean_get(op->ptr, "use_cursor_init"); /* makes op->customdata */ vod = op->customdata = viewops_data_create( C, event, viewops_flag_from_prefs() | VIEWOPS_FLAG_PERSP_ENSURE | (use_cursor_init ? VIEWOPS_FLAG_USE_MOUSE_INIT : 0)); ED_view3d_smooth_view_force_finish(C, vod->v3d, vod->region); if (ELEM(event->type, MOUSEPAN, MOUSEROTATE)) { /* Rotate direction we keep always same */ int event_xy[2]; if (event->type == MOUSEPAN) { if (event->is_direction_inverted) { event_xy[0] = 2 * event->xy[0] - event->prev_xy[0]; event_xy[1] = 2 * event->xy[1] - event->prev_xy[1]; } else { copy_v2_v2_int(event_xy, event->prev_xy); } } else { /* MOUSEROTATE performs orbital rotation, so y axis delta is set to 0 */ copy_v2_v2_int(event_xy, event->prev_xy); } viewrotate_apply(vod, event_xy); viewops_data_free(C, op->customdata); op->customdata = NULL; return OPERATOR_FINISHED; } /* add temp handler */ WM_event_add_modal_handler(C, op); return OPERATOR_RUNNING_MODAL; } static void viewrotate_cancel(bContext *C, wmOperator *op) { viewops_data_free(C, op->customdata); op->customdata = NULL; } void VIEW3D_OT_rotate(wmOperatorType *ot) { /* identifiers */ ot->name = "Rotate View"; ot->description = "Rotate the view"; ot->idname = "VIEW3D_OT_rotate"; /* api callbacks */ ot->invoke = viewrotate_invoke; ot->modal = viewrotate_modal; ot->poll = view3d_rotation_poll; ot->cancel = viewrotate_cancel; /* flags */ ot->flag = OPTYPE_BLOCKING | OPTYPE_GRAB_CURSOR_XY; view3d_operator_properties_common(ot, V3D_OP_PROP_USE_MOUSE_INIT); } /** \} */