Fix bone select failing with end-points outside the view

Apply the same fix for T32214 (edge-select failing) to bones
which also failed when their end-points were outside of the view.

- Add V3D_PROJ_TEST_CLIP_CONTENT support for edit & pose bone iterator
  and use for selection operators.
- Remove unnecessarily complicated checks with pose-mode lasso tagging.
- Correct error in pose-mode LassoSelectUserData.is_changed
  (currently harmless as it's not read back).
This commit is contained in:
Campbell Barton 2021-06-22 14:04:30 +10:00
parent 9ac56bad4c
commit 338be95874
2 changed files with 295 additions and 177 deletions

View File

@ -97,6 +97,110 @@ static int content_planes_from_clip_flag(const ARegion *region,
return planes_len;
}
/**
* Edge projection is more involved since part of the edge may be behind the view
* or extend beyond the far limits. In the case of single points, these can be ignored.
* However it just may still be visible on screen, so constrained the edge to planes
* defined by the port to ensure both ends of the edge can be projected, see T32214.
*
* \note This is unrelated to #V3D_PROJ_TEST_CLIP_BB which must be checked separately.
*/
static bool view3d_project_segment_to_screen_with_content_clip_planes(
const ARegion *region,
const float v_a[3],
const float v_b[3],
const eV3DProjTest clip_flag,
const rctf *win_rect,
const float content_planes[][4],
const int content_planes_len,
/* Output. */
float r_screen_co_a[2],
float r_screen_co_b[2])
{
/* Clipping already handled, no need to check in projection. */
eV3DProjTest clip_flag_nowin = clip_flag & ~V3D_PROJ_TEST_CLIP_WIN;
const eV3DProjStatus status_a = ED_view3d_project_float_object(
region, v_a, r_screen_co_a, clip_flag_nowin);
const eV3DProjStatus status_b = ED_view3d_project_float_object(
region, v_b, r_screen_co_b, clip_flag_nowin);
if ((status_a == V3D_PROJ_RET_OK) && (status_b == V3D_PROJ_RET_OK)) {
if (clip_flag & V3D_PROJ_TEST_CLIP_WIN) {
if (!BLI_rctf_isect_segment(win_rect, r_screen_co_a, r_screen_co_b)) {
return false;
}
}
}
else {
if (content_planes_len == 0) {
return false;
}
/* Both too near, ignore. */
if ((status_a & V3D_PROJ_TEST_CLIP_NEAR) && (status_b & V3D_PROJ_TEST_CLIP_NEAR)) {
return false;
}
/* Both too far, ignore. */
if ((status_a & V3D_PROJ_TEST_CLIP_FAR) && (status_b & V3D_PROJ_TEST_CLIP_FAR)) {
return false;
}
/* Simple cases have been ruled out, clip by viewport planes, then re-project. */
float v_a_clip[3], v_b_clip[3];
if (!clip_segment_v3_plane_n(
v_a, v_b, content_planes, content_planes_len, v_a_clip, v_b_clip)) {
return false;
}
if ((ED_view3d_project_float_object(region, v_a_clip, r_screen_co_a, clip_flag_nowin) !=
V3D_PROJ_RET_OK) ||
(ED_view3d_project_float_object(region, v_b_clip, r_screen_co_b, clip_flag_nowin) !=
V3D_PROJ_RET_OK)) {
return false;
}
/* No need for #V3D_PROJ_TEST_CLIP_WIN check here,
* clipping the segment by planes handle this. */
}
return true;
}
/**
* Project an edge, points that fail to project are tagged with #IS_CLIPPED.
*/
static bool view3d_project_segment_to_screen_with_clip_tag(const ARegion *region,
const float v_a[3],
const float v_b[3],
const eV3DProjTest clip_flag,
/* Output. */
float r_screen_co_a[2],
float r_screen_co_b[2])
{
int count = 0;
if (ED_view3d_project_float_object(region, v_a, r_screen_co_a, clip_flag) == V3D_PROJ_RET_OK) {
count++;
}
else {
r_screen_co_a[0] = IS_CLIPPED; /* weak */
/* screen_co_a[1]: intentionally don't set this so we get errors on misuse */
}
if (ED_view3d_project_float_object(region, v_b, r_screen_co_b, clip_flag) == V3D_PROJ_RET_OK) {
count++;
}
else {
r_screen_co_b[0] = IS_CLIPPED; /* weak */
/* screen_co_b[1]: intentionally don't set this so we get errors on misuse */
}
/* Caller may want to know this value, for now it's not needed. */
return count != 0;
}
/** \} */
/* -------------------------------------------------------------------- */
@ -261,76 +365,6 @@ void mesh_foreachScreenVert(
/** \name Edit-Mesh: For Each Screen Mesh Edge
* \{ */
/**
* Edge projection is more involved since part of the edge may be behind the view
* or extend beyond the far limits. In the case of single points, these can be ignored.
* However it just may still be visible on screen, so constrained the edge to planes
* defined by the port to ensure both ends of the edge can be projected, see T32214.
*
* \note This is unrelated to #V3D_PROJ_TEST_CLIP_BB which must be checked separately.
*/
static bool mesh_foreachScreenEdge_shared_project_and_test(const ARegion *region,
const float v_a[3],
const float v_b[3],
const eV3DProjTest clip_flag,
const rctf *win_rect,
const float content_planes[][4],
const int content_planes_len,
/* Output. */
float r_screen_co_a[2],
float r_screen_co_b[2])
{
/* Clipping already handled, no need to check in projection. */
eV3DProjTest clip_flag_nowin = clip_flag & ~V3D_PROJ_TEST_CLIP_WIN;
const eV3DProjStatus status_a = ED_view3d_project_float_object(
region, v_a, r_screen_co_a, clip_flag_nowin);
const eV3DProjStatus status_b = ED_view3d_project_float_object(
region, v_b, r_screen_co_b, clip_flag_nowin);
if ((status_a == V3D_PROJ_RET_OK) && (status_b == V3D_PROJ_RET_OK)) {
if (clip_flag & V3D_PROJ_TEST_CLIP_WIN) {
if (!BLI_rctf_isect_segment(win_rect, r_screen_co_a, r_screen_co_b)) {
return false;
}
}
}
else {
if (content_planes_len == 0) {
return false;
}
/* Both too near, ignore. */
if ((status_a & V3D_PROJ_TEST_CLIP_NEAR) && (status_b & V3D_PROJ_TEST_CLIP_NEAR)) {
return false;
}
/* Both too far, ignore. */
if ((status_a & V3D_PROJ_TEST_CLIP_FAR) && (status_b & V3D_PROJ_TEST_CLIP_FAR)) {
return false;
}
/* Simple cases have been ruled out, clip by viewport planes, then re-project. */
float v_a_clip[3], v_b_clip[3];
if (!clip_segment_v3_plane_n(
v_a, v_b, content_planes, content_planes_len, v_a_clip, v_b_clip)) {
return false;
}
if ((ED_view3d_project_float_object(region, v_a_clip, r_screen_co_a, clip_flag_nowin) !=
V3D_PROJ_RET_OK) ||
(ED_view3d_project_float_object(region, v_b_clip, r_screen_co_b, clip_flag_nowin) !=
V3D_PROJ_RET_OK)) {
return false;
}
/* No need for #V3D_PROJ_TEST_CLIP_WIN check here,
* clipping the segment by planes handle this. */
}
return true;
}
static void mesh_foreachScreenEdge__mapFunc(void *userData,
int index,
const float v_a[3],
@ -343,15 +377,15 @@ static void mesh_foreachScreenEdge__mapFunc(void *userData,
}
float screen_co_a[2], screen_co_b[2];
if (!mesh_foreachScreenEdge_shared_project_and_test(data->vc.region,
v_a,
v_b,
data->clip_flag,
&data->win_rect,
data->content_planes,
data->content_planes_len,
screen_co_a,
screen_co_b)) {
if (!view3d_project_segment_to_screen_with_content_clip_planes(data->vc.region,
v_a,
v_b,
data->clip_flag,
&data->win_rect,
data->content_planes,
data->content_planes_len,
screen_co_a,
screen_co_b)) {
return;
}
@ -430,15 +464,15 @@ static void mesh_foreachScreenEdge_clip_bb_segment__mapFunc(void *userData,
}
float screen_co_a[2], screen_co_b[2];
if (!mesh_foreachScreenEdge_shared_project_and_test(data->vc.region,
v_a_clip,
v_b_clip,
data->clip_flag,
&data->win_rect,
data->content_planes,
data->content_planes_len,
screen_co_a,
screen_co_b)) {
if (!view3d_project_segment_to_screen_with_content_clip_planes(data->vc.region,
v_a_clip,
v_b_clip,
data->clip_flag,
&data->win_rect,
data->content_planes,
data->content_planes_len,
screen_co_a,
screen_co_b)) {
return;
}
@ -728,35 +762,51 @@ void armature_foreachScreenBone(struct ViewContext *vc,
ED_view3d_check_mats_rv3d(vc->rv3d);
float content_planes[6][4];
int content_planes_len;
rctf win_rect;
if (clip_flag & V3D_PROJ_TEST_CLIP_CONTENT) {
content_planes_len = content_planes_from_clip_flag(
vc->region, vc->obedit, clip_flag, content_planes);
win_rect.xmin = 0;
win_rect.ymin = 0;
win_rect.xmax = vc->region->winx;
win_rect.ymax = vc->region->winy;
}
else {
content_planes_len = 0;
}
for (ebone = arm->edbo->first; ebone; ebone = ebone->next) {
if (EBONE_VISIBLE(arm, ebone)) {
float screen_co_a[2], screen_co_b[2];
int points_proj_tot = 0;
if (!EBONE_VISIBLE(arm, ebone)) {
continue;
}
/* project head location to screenspace */
if (ED_view3d_project_float_object(vc->region, ebone->head, screen_co_a, clip_flag) ==
V3D_PROJ_RET_OK) {
points_proj_tot++;
}
else {
screen_co_a[0] = IS_CLIPPED; /* weak */
/* screen_co_a[1]: intentionally don't set this so we get errors on misuse */
}
float screen_co_a[2], screen_co_b[2];
const float *v_a = ebone->head, *v_b = ebone->tail;
/* project tail location to screenspace */
if (ED_view3d_project_float_object(vc->region, ebone->tail, screen_co_b, clip_flag) ==
V3D_PROJ_RET_OK) {
points_proj_tot++;
}
else {
screen_co_b[0] = IS_CLIPPED; /* weak */
/* screen_co_b[1]: intentionally don't set this so we get errors on misuse */
}
if (points_proj_tot) { /* at least one point's projection worked */
func(userData, ebone, screen_co_a, screen_co_b);
if (clip_flag & V3D_PROJ_TEST_CLIP_CONTENT) {
if (!view3d_project_segment_to_screen_with_content_clip_planes(vc->region,
v_a,
v_b,
clip_flag,
&win_rect,
content_planes,
content_planes_len,
screen_co_a,
screen_co_b)) {
continue;
}
}
else {
if (!view3d_project_segment_to_screen_with_clip_tag(
vc->region, v_a, v_b, clip_flag, screen_co_a, screen_co_b)) {
continue;
}
}
func(userData, ebone, screen_co_a, screen_co_b);
}
}
@ -783,36 +833,52 @@ void pose_foreachScreenBone(struct ViewContext *vc,
ED_view3d_check_mats_rv3d(vc->rv3d);
float content_planes[6][4];
int content_planes_len;
rctf win_rect;
if (clip_flag & V3D_PROJ_TEST_CLIP_CONTENT) {
content_planes_len = content_planes_from_clip_flag(
vc->region, ob_eval, clip_flag, content_planes);
win_rect.xmin = 0;
win_rect.ymin = 0;
win_rect.xmax = vc->region->winx;
win_rect.ymax = vc->region->winy;
}
else {
content_planes_len = 0;
}
for (pchan = pose->chanbase.first; pchan; pchan = pchan->next) {
if (PBONE_VISIBLE(arm_eval, pchan->bone)) {
bPoseChannel *pchan_eval = BKE_pose_channel_find_name(ob_eval->pose, pchan->name);
float screen_co_a[2], screen_co_b[2];
int points_proj_tot = 0;
if (!PBONE_VISIBLE(arm_eval, pchan->bone)) {
continue;
}
/* project head location to screenspace */
if (ED_view3d_project_float_object(
vc->region, pchan_eval->pose_head, screen_co_a, clip_flag) == V3D_PROJ_RET_OK) {
points_proj_tot++;
}
else {
screen_co_a[0] = IS_CLIPPED; /* weak */
/* screen_co_a[1]: intentionally don't set this so we get errors on misuse */
}
bPoseChannel *pchan_eval = BKE_pose_channel_find_name(ob_eval->pose, pchan->name);
float screen_co_a[2], screen_co_b[2];
const float *v_a = pchan_eval->pose_head, *v_b = pchan_eval->pose_tail;
/* project tail location to screenspace */
if (ED_view3d_project_float_object(
vc->region, pchan_eval->pose_tail, screen_co_b, clip_flag) == V3D_PROJ_RET_OK) {
points_proj_tot++;
}
else {
screen_co_b[0] = IS_CLIPPED; /* weak */
/* screen_co_b[1]: intentionally don't set this so we get errors on misuse */
}
if (points_proj_tot) { /* at least one point's projection worked */
func(userData, pchan, screen_co_a, screen_co_b);
if (clip_flag & V3D_PROJ_TEST_CLIP_CONTENT) {
if (!view3d_project_segment_to_screen_with_content_clip_planes(vc->region,
v_a,
v_b,
clip_flag,
&win_rect,
content_planes,
content_planes_len,
screen_co_a,
screen_co_b)) {
continue;
}
}
else {
if (!view3d_project_segment_to_screen_with_clip_tag(
vc->region, v_a, v_b, clip_flag, screen_co_a, screen_co_b)) {
continue;
}
}
func(userData, pchan, screen_co_a, screen_co_b);
}
}

View File

@ -525,39 +525,12 @@ static void do_lasso_select_pose__do_tag(void *userData,
return;
}
bool is_point_done = false;
int points_proj_tot = 0;
/* project head location to screenspace */
if (screen_co_a[0] != IS_CLIPPED) {
points_proj_tot++;
if (BLI_rcti_isect_pt(data->rect, UNPACK2(screen_co_a)) &&
BLI_lasso_is_point_inside(
data->mcoords, data->mcoords_len, UNPACK2(screen_co_a), INT_MAX)) {
is_point_done = true;
}
}
/* project tail location to screenspace */
if (screen_co_b[0] != IS_CLIPPED) {
points_proj_tot++;
if (BLI_rcti_isect_pt(data->rect, UNPACK2(screen_co_b)) &&
BLI_lasso_is_point_inside(
data->mcoords, data->mcoords_len, UNPACK2(screen_co_b), INT_MAX)) {
is_point_done = true;
}
}
/* if one of points selected, we skip the bone itself */
if ((is_point_done == true) || ((is_point_done == false) && (points_proj_tot == 2) &&
BLI_lasso_is_edge_inside(data->mcoords,
data->mcoords_len,
UNPACK2(screen_co_a),
UNPACK2(screen_co_b),
INT_MAX))) {
if (BLI_rctf_isect_segment(data->rect_fl, screen_co_a, screen_co_b) &&
BLI_lasso_is_edge_inside(
data->mcoords, data->mcoords_len, UNPACK2(screen_co_a), UNPACK2(screen_co_b), INT_MAX)) {
pchan->bone->flag |= BONE_DONE;
data->is_changed = true;
}
data->is_changed |= is_point_done;
}
static void do_lasso_tag_pose(ViewContext *vc,
Object *ob,
@ -581,7 +554,11 @@ static void do_lasso_tag_pose(ViewContext *vc,
ED_view3d_init_mats_rv3d(vc_tmp.obact, vc->rv3d);
pose_foreachScreenBone(&vc_tmp, do_lasso_select_pose__do_tag, &data, V3D_PROJ_TEST_CLIP_DEFAULT);
/* Treat bones as clipped segments (no joints). */
pose_foreachScreenBone(&vc_tmp,
do_lasso_select_pose__do_tag,
&data,
V3D_PROJ_TEST_CLIP_DEFAULT | V3D_PROJ_TEST_CLIP_CONTENT_DEFAULT);
}
static bool do_lasso_select_objects(ViewContext *vc,
@ -1071,6 +1048,34 @@ static void do_lasso_select_armature__doSelectBone(void *userData,
ebone->temp.i = is_inside_flag | (is_ignore_flag >> 16);
}
static void do_lasso_select_armature__doSelectBone_clip_content(void *userData,
EditBone *ebone,
const float screen_co_a[2],
const float screen_co_b[2])
{
LassoSelectUserData *data = userData;
bArmature *arm = data->vc->obedit->data;
if (!EBONE_VISIBLE(arm, ebone)) {
return;
}
const int is_ignore_flag = ebone->temp.i << 16;
int is_inside_flag = ebone->temp.i & ~0xFFFF;
/* - When #BONESEL_BONE is set, there is nothing to do.
* - When #BONE_ROOTSEL or #BONE_TIPSEL have been set - they take priority over bone selection.
*/
if (is_inside_flag & (BONESEL_BONE | BONE_ROOTSEL | BONE_TIPSEL)) {
return;
}
if (BLI_lasso_is_edge_inside(
data->mcoords, data->mcoords_len, UNPACK2(screen_co_a), UNPACK2(screen_co_b), INT_MAX)) {
is_inside_flag |= BONESEL_BONE;
}
ebone->temp.i = is_inside_flag | (is_ignore_flag >> 16);
}
static bool do_lasso_select_armature(ViewContext *vc,
const int mcoords[][2],
@ -1094,9 +1099,18 @@ static bool do_lasso_select_armature(ViewContext *vc,
ED_view3d_init_mats_rv3d(vc->obedit, vc->rv3d);
/* Operate on fully visible (non-clipped) points. */
armature_foreachScreenBone(
vc, do_lasso_select_armature__doSelectBone, &data, V3D_PROJ_TEST_CLIP_DEFAULT);
/* Operate on bones as segments clipped to the viewport bounds
* (needed to handle bones with both points outside the view).
* A separate pass is needed since clipped coordinates can't be used for selecting joints. */
armature_foreachScreenBone(vc,
do_lasso_select_armature__doSelectBone_clip_content,
&data,
V3D_PROJ_TEST_CLIP_DEFAULT | V3D_PROJ_TEST_CLIP_CONTENT_DEFAULT);
data.is_changed |= ED_armature_edit_select_op_from_tagged(vc->obedit->data, sel_op);
if (data.is_changed) {
@ -4102,8 +4116,11 @@ static bool pose_circle_select(ViewContext *vc,
ED_view3d_init_mats_rv3d(vc->obact, vc->rv3d); /* for foreach's screen/vert projection */
pose_foreachScreenBone(
vc, do_circle_select_pose__doSelectBone, &data, V3D_PROJ_TEST_CLIP_DEFAULT);
/* Treat bones as clipped segments (no joints). */
pose_foreachScreenBone(vc,
do_circle_select_pose__doSelectBone,
&data,
V3D_PROJ_TEST_CLIP_DEFAULT | V3D_PROJ_TEST_CLIP_CONTENT_DEFAULT);
if (data.is_changed) {
ED_pose_bone_select_tag_update(vc->obact);
@ -4150,7 +4167,11 @@ static void do_circle_select_armature__doSelectBone(void *userData,
return;
}
/* When true, ignore in the next pass. */
ebone->temp.i = false;
bool is_point_done = false;
bool is_edge_done = false;
int points_proj_tot = 0;
/* project head location to screenspace */
@ -4178,17 +4199,39 @@ static void do_circle_select_armature__doSelectBone(void *userData,
* otherwise there is no way to circle select joints alone */
if ((is_point_done == false) && (points_proj_tot == 2) &&
edge_inside_circle(data->mval_fl, data->radius, screen_co_a, screen_co_b)) {
if (data->select) {
ebone->flag |= (BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL);
}
else {
ebone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL);
}
SET_FLAG_FROM_TEST(ebone->flag, data->select, BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL);
is_edge_done = true;
data->is_changed = true;
}
if (is_point_done || is_edge_done) {
ebone->temp.i = true;
}
data->is_changed |= is_point_done;
}
static void do_circle_select_armature__doSelectBone_clip_content(void *userData,
struct EditBone *ebone,
const float screen_co_a[2],
const float screen_co_b[2])
{
CircleSelectUserData *data = userData;
bArmature *arm = data->vc->obedit->data;
if (!(data->select ? EBONE_SELECTABLE(arm, ebone) : EBONE_VISIBLE(arm, ebone))) {
return;
}
/* Set in the first pass, needed so circle select prioritizes joints. */
if (ebone->temp.i == true) {
return;
}
if (edge_inside_circle(data->mval_fl, data->radius, screen_co_a, screen_co_b)) {
SET_FLAG_FROM_TEST(ebone->flag, data->select, BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL);
data->is_changed = true;
}
}
static bool armature_circle_select(ViewContext *vc,
const eSelectOp sel_op,
const int mval[2],
@ -4207,9 +4250,18 @@ static bool armature_circle_select(ViewContext *vc,
ED_view3d_init_mats_rv3d(vc->obedit, vc->rv3d);
/* Operate on fully visible (non-clipped) points. */
armature_foreachScreenBone(
vc, do_circle_select_armature__doSelectBone, &data, V3D_PROJ_TEST_CLIP_DEFAULT);
/* Operate on bones as segments clipped to the viewport bounds
* (needed to handle bones with both points outside the view).
* A separate pass is needed since clipped coordinates can't be used for selecting joints. */
armature_foreachScreenBone(vc,
do_circle_select_armature__doSelectBone_clip_content,
&data,
V3D_PROJ_TEST_CLIP_DEFAULT | V3D_PROJ_TEST_CLIP_CONTENT_DEFAULT);
if (data.is_changed) {
ED_armature_edit_sync_selection(arm->edbo);
ED_armature_edit_validate_active(arm);