UI: Do Not Join Aligned Area Edges

As we move screen area edges, if any line up exactly then we join them
together so that they act as one. They become a single edge permanently
without a way to break them. This PR removes that functionality. Under
the hood what currently happens is that area corner vertexes are merged
if duplicated. As are area edges. This PR just turns off this
deduplication. It then adds an "extend" option to allow moving multiple
edges if they are close together. And an "merge edge" operator to turn
multiple aligned edges into a single edge.

Pull Request: https://projects.blender.org/blender/blender/pulls/151389
This commit is contained in:
Harley Acheson 2026-04-15 19:49:50 +02:00 • committed by Harley Acheson
parent dc27626ce3
commit ca57a880af
5 changed files with 329 additions and 34 deletions

View file

@ -921,6 +921,7 @@ def km_screen_editing(params):
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("snap", True)]}),
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, None),
("screen.area_options", {"type": 'RIGHTMOUSE', "value": 'PRESS'}, None),
])

View file

@ -103,8 +103,7 @@ ScrArea *area_split(const wmWindow *win,
bScreen *screen,
ScrArea *area,
const eScreenAxis dir_axis,
const float fac,
const bool merge)
const float fac)
{
ScrArea *newa = nullptr;
@ -187,10 +186,6 @@ ScrArea *area_split(const wmWindow *win,
ED_area_data_copy(newa, area, true);
}
/* remove double vertices en edges */
if (merge) {
BKE_screen_remove_double_scrverts(screen);
}
BKE_screen_remove_double_scredges(screen);
BKE_screen_remove_unused_scredges(screen);
@ -457,7 +452,6 @@ static bool screen_area_join_aligned(
}
screen_delarea(C, screen, sa2);
BKE_screen_remove_double_scrverts(screen);
/* Update preview thumbnail */
BKE_icon_changed(screen->id.icon_id);
@ -478,7 +472,7 @@ static ScrArea *screen_area_trim(
((*area)->v3->vec.y - (*area)->v1->vec.y));
fac = (reverse == vertical) ? 1.0f - fac : fac;
ScrArea *newsa = area_split(
CTX_wm_window(C), screen, *area, vertical ? SCREEN_AXIS_V : SCREEN_AXIS_H, fac, true);
CTX_wm_window(C), screen, *area, vertical ? SCREEN_AXIS_V : SCREEN_AXIS_H, fac);
/* area_split always returns smallest of the two areas, so might have to swap. */
if (((fac > 0.5f) == vertical) != reverse) {

View file

@ -408,4 +408,145 @@ void screen_geom_select_connected_edge(const wmWindow *win, ScrEdge *edge)
}
}
bool screen_geom_edge_can_extend(const wmWindow *win, ScrEdge *edge)
{
bScreen *screen = WM_window_get_active_screen(win);
bool can_extend = false;
const eScreenAxis dir_axis = (edge->v1->vec.x == edge->v2->vec.x) ? SCREEN_AXIS_V :
SCREEN_AXIS_H;
screen_geom_select_connected_edge(win, edge);
for (ScrEdge &se : screen->edgebase) {
if (se.v1->flag + se.v2->flag != 0) {
continue;
}
if (dir_axis == SCREEN_AXIS_H) {
for (ScrVert &v : screen->vertbase) {
if (v.flag && v.vec.x == se.v1->vec.x &&
(abs(v.vec.y - se.v1->vec.y) < EDGE_ALIGN_TOLERANCE ||
abs(v.vec.y - se.v2->vec.y) < EDGE_ALIGN_TOLERANCE))
{
se.v1->flag = se.v2->flag = 1;
can_extend = true;
break;
}
}
}
else if (dir_axis == SCREEN_AXIS_V) {
for (ScrVert &v : screen->vertbase) {
if (v.flag && v.vec.y == se.v1->vec.y &&
(abs(v.vec.x - se.v1->vec.x) < EDGE_ALIGN_TOLERANCE ||
abs(v.vec.x - se.v2->vec.x) < EDGE_ALIGN_TOLERANCE))
{
se.v1->flag = se.v2->flag = 1;
can_extend = true;
break;
}
}
}
}
ED_screen_verts_iter(win, screen, sv)
{
sv->flag = 0;
}
return can_extend;
}
void screen_geom_select_extended_edge(const wmWindow *win, ScrEdge *edge)
{
bScreen *screen = WM_window_get_active_screen(win);
const eScreenAxis dir_axis = (edge->v1->vec.x == edge->v2->vec.x) ? SCREEN_AXIS_V :
SCREEN_AXIS_H;
ED_screen_verts_iter(win, screen, sv)
{
sv->flag = 0;
}
for (ScrVert &v : screen->vertbase) {
if (dir_axis == SCREEN_AXIS_H) {
if (abs(v.vec.y - edge->v1->vec.y) < EDGE_ALIGN_TOLERANCE) {
v.flag = 1;
}
}
else if (dir_axis == SCREEN_AXIS_V) {
if (abs(v.vec.x - edge->v1->vec.x) < EDGE_ALIGN_TOLERANCE) {
v.flag = 1;
}
}
}
}
void screen_geom_edge_aligned_merge(const wmWindow *win, ScrEdge *edge)
{
bScreen *screen = WM_window_get_active_screen(win);
screen_geom_select_extended_edge(win, edge);
const eScreenAxis dir_axis = (edge->v1->vec.x == edge->v2->vec.x) ? SCREEN_AXIS_V :
SCREEN_AXIS_H;
/* Align the vertices if close. */
for (ScrVert &v : screen->vertbase) {
if (dir_axis == SCREEN_AXIS_V && abs(v.vec.x - edge->v2->vec.x) < EDGE_ALIGN_TOLERANCE) {
v.vec.x = edge->v2->vec.x;
}
else if (abs(v.vec.y - edge->v2->vec.y) < EDGE_ALIGN_TOLERANCE) {
v.vec.y = edge->v2->vec.y;
}
}
for (ScrVert &v : screen->vertbase) {
if (v.flag == 1 && v.newv == nullptr) { /* !!! */
ScrVert *v1 = v.next;
while (v1) {
if (v1->newv == nullptr) { /* !?! */
if (abs(v1->vec.x - v.vec.x) < EDGE_ALIGN_TOLERANCE &&
abs(v1->vec.y - v.vec.y) < EDGE_ALIGN_TOLERANCE)
{
v1->newv = &v;
}
}
v1 = v1->next;
}
}
}
/* Replace pointers in edges and faces. */
for (ScrEdge &se : screen->edgebase) {
if (se.v1->newv) {
se.v1 = se.v1->newv;
}
if (se.v2->newv) {
se.v2 = se.v2->newv;
}
BKE_screen_sort_scrvert(&(se.v1), &(se.v2));
}
for (ScrArea &area : screen->areabase) {
if (area.v1->newv) {
area.v1 = area.v1->newv;
}
if (area.v2->newv) {
area.v2 = area.v2->newv;
}
if (area.v3->newv) {
area.v3 = area.v3->newv;
}
if (area.v4->newv) {
area.v4 = area.v4->newv;
}
}
/* Remove. */
for (ScrVert &v : screen->vertbase.items_mutable()) {
if (v.newv) {
BLI_remlink(&screen->vertbase, &v);
MEM_delete(&v);
}
}
ED_screen_verts_iter(win, screen, sv)
{
sv->flag = 0;
}
}
} // namespace blender

View file

@ -69,6 +69,9 @@ enum class AreaDockTarget {
#define AREAJOINTOLERANCEX (AREAMINX * UI_SCALE_FAC)
#define AREAJOINTOLERANCEY (HEADERY * UI_SCALE_FAC)
/* Edges must be within this amount to allow aligned edge merging and moving. */
#define EDGE_ALIGN_TOLERANCE (7 * UI_SCALE_FAC)
/**
* Expanded interaction influence of area borders.
*/
@ -151,12 +154,8 @@ void screen_change_update(bContext *C, wmWindow *win, bScreen *screen);
*/
void screen_change_prepare(
bScreen *screen_old, bScreen *screen_new, Main *bmain, bContext *C, wmWindow *win);
ScrArea *area_split(const wmWindow *win,
bScreen *screen,
ScrArea *area,
eScreenAxis dir_axis,
float fac,
bool merge);
ScrArea *area_split(
const wmWindow *win, bScreen *screen, ScrArea *area, eScreenAxis dir_axis, float fac);
/**
* Join any two neighboring areas. Might involve complex changes.
*/
@ -232,6 +231,21 @@ short screen_geom_find_area_split_point(const ScrArea *area,
*/
void screen_geom_select_connected_edge(const wmWindow *win, ScrEdge *edge);
/**
* Select all edges that are aligned with \a edge.
*/
void screen_geom_select_extended_edge(const wmWindow *win, ScrEdge *edge);
/**
* True if the edge can be extended.
*/
bool screen_geom_edge_can_extend(const wmWindow *win, ScrEdge *edge);
/**
* Merge aligned edges into a single edge.
*/
void screen_geom_edge_aligned_merge(const wmWindow *win, ScrEdge *edge);
/* `screen_context.cc` */
/**

View file

@ -1787,6 +1787,8 @@ struct sAreaMoveData {
int bigger, smaller, origval, step;
eScreenAxis dir_axis;
AreaMoveSnapType snap_type;
bool can_extend; /* There are other aligned edges. */
bool extending; /* Currently moving multiple edges. */
bScreen *screen;
ScrArea *area1, *area2;
double start_time;
@ -1905,9 +1907,64 @@ static void area_move_out_draw_cb(const wmWindow *win, void *userdata)
screen_draw_move_highlight(win, md->screen, md->dir_axis, factor);
}
/* When changing to/from edge extension we need to reinitialize
* everything, including movement limits, starting positions, etc. */
static bool area_move_reinit(bContext *C, wmOperator *op, bool extend, const int xy[2])
{
sAreaMoveData *md = static_cast<sAreaMoveData *>(op->customdata);
bScreen *screen = CTX_wm_screen(C);
wmWindow *win = CTX_wm_window(C);
ED_screen_verts_iter(win, screen, v1)
{
v1->editflag = 0;
}
ScrEdge *actedge = screen_geom_find_active_scredge(win, screen, xy[0], xy[1]);
if (actedge == nullptr) {
md->can_extend = false;
md->extending = false;
return false;
}
RNA_int_set(op->ptr, "x", xy[0]);
RNA_int_set(op->ptr, "y", xy[1]);
if (md->dir_axis == SCREEN_AXIS_H) {
md->origval = actedge->v1->vec.y;
}
else {
md->origval = actedge->v1->vec.x;
}
md->can_extend = screen_geom_edge_can_extend(win, actedge);
if (md->can_extend && extend) {
screen_geom_select_extended_edge(win, actedge);
md->extending = true;
}
else {
screen_geom_select_connected_edge(win, actedge);
md->extending = false;
}
/* Vertices with 'flag == 1' can be moved. Move this to editflag. */
ED_screen_verts_iter(win, screen, v1)
{
v1->editflag = v1->flag;
}
bool use_bigger_smaller_snap = false;
area_move_set_limits(
win, screen, md->dir_axis, &md->bigger, &md->smaller, &use_bigger_smaller_snap);
md->start_time = BLI_time_now_seconds();
md->end_time = md->start_time + AREA_MOVE_LINE_FADEIN;
return true;
}
/* validate selection inside screen, set variables OK */
/* return false: init failed */
static bool area_move_init(bContext *C, wmOperator *op)
static bool area_move_init(bContext *C, wmOperator *op, bool extend)
{
bScreen *screen = CTX_wm_screen(C);
wmWindow *win = CTX_wm_window(C);
@ -1947,7 +2004,16 @@ static bool area_move_init(bContext *C, wmOperator *op)
md->origval = actedge->v1->vec.x;
}
screen_geom_select_connected_edge(win, actedge);
md->can_extend = screen_geom_edge_can_extend(win, actedge);
if (md->can_extend && extend) {
screen_geom_select_extended_edge(win, actedge);
md->extending = true;
}
else {
screen_geom_select_connected_edge(win, actedge);
md->extending = false;
}
/* now all vertices with 'flag == 1' are the ones that can be moved. Move this to editflag */
ED_screen_verts_iter(win, screen, v1)
{
@ -2183,6 +2249,9 @@ static void area_move_apply_do(bContext *C, int delta, sAreaMoveData *md)
status.item(IFACE_("Confirm"), ICON_MOUSE_LMB);
status.item(IFACE_("Cancel"), ICON_EVENT_ESC);
status.item_bool(IFACE_("Snap"), md->snap_type == SNAP_FRACTION_AND_ADJACENT, ICON_EVENT_CTRL);
if (md->can_extend) {
status.item_bool(IFACE_("Extend"), md->extending, ICON_EVENT_SHIFT);
}
short final_loc = -1;
bool doredraw = false;
@ -2272,8 +2341,6 @@ static void area_move_exit(bContext *C, wmOperator *op)
md->win = CTX_wm_window(C);
md->draw_callback = WM_draw_cb_activate(md->win, area_move_out_draw_cb, md);
/* this makes sure aligned edges will result in aligned grabbing */
BKE_screen_remove_double_scrverts(CTX_wm_screen(C));
BKE_screen_remove_double_scredges(CTX_wm_screen(C));
ED_workspace_status_text(C, nullptr);
@ -2282,7 +2349,7 @@ static void area_move_exit(bContext *C, wmOperator *op)
static wmOperatorStatus area_move_exec(bContext *C, wmOperator *op)
{
if (!area_move_init(C, op)) {
if (!area_move_init(C, op, false)) {
return OPERATOR_CANCELLED;
}
@ -2298,7 +2365,7 @@ static wmOperatorStatus area_move_invoke(bContext *C, wmOperator *op, const wmEv
RNA_int_set(op->ptr, "x", event->xy[0]);
RNA_int_set(op->ptr, "y", event->xy[1]);
if (!area_move_init(C, op)) {
if (!area_move_init(C, op, event->modifier & KM_SHIFT)) {
return OPERATOR_PASS_THROUGH;
}
@ -2308,6 +2375,9 @@ static wmOperatorStatus area_move_invoke(bContext *C, wmOperator *op, const wmEv
status.item(IFACE_("Confirm"), ICON_MOUSE_LMB);
status.item(IFACE_("Cancel"), ICON_EVENT_ESC);
status.item_bool(IFACE_("Snap"), md->snap_type == SNAP_FRACTION_AND_ADJACENT, ICON_EVENT_CTRL);
if (md->can_extend) {
status.item_bool(IFACE_("Extend"), md->extending, ICON_EVENT_SHIFT);
}
/* add temp handler */
screen_modal_action_begin();
@ -2331,6 +2401,14 @@ static wmOperatorStatus area_move_modal(bContext *C, wmOperator *op, const wmEve
/* execute the events */
switch (event->type) {
case EVT_RIGHTSHIFTKEY:
case EVT_LEFTSHIFTKEY: {
if (!md->can_extend || !area_move_reinit(C, op, event->val == KM_PRESS, event->xy)) {
md->extending = false;
}
WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, nullptr);
break;
}
case MOUSEMOVE: {
int x = RNA_int_get(op->ptr, "x");
int y = RNA_int_get(op->ptr, "y");
@ -2339,6 +2417,15 @@ static wmOperatorStatus area_move_modal(bContext *C, wmOperator *op, const wmEve
RNA_int_set(op->ptr, "delta", delta);
area_move_apply(C, op);
wmWindow *win = CTX_wm_window(C);
bScreen *screen = CTX_wm_screen(C);
if (!md->extending) {
ScrEdge *actedge = screen_geom_find_active_scredge(
win, screen, event->xy[0], event->xy[1]);
md->can_extend = actedge && screen_geom_edge_can_extend(md->win, actedge);
}
break;
}
case RIGHTMOUSE: {
@ -2367,11 +2454,6 @@ static wmOperatorStatus area_move_modal(bContext *C, wmOperator *op, const wmEve
}
break;
}
WorkspaceStatus status(C);
status.item(IFACE_("Confirm"), ICON_MOUSE_LMB);
status.item(IFACE_("Cancel"), ICON_EVENT_ESC);
status.item_bool(
IFACE_("Snap"), md->snap_type == SNAP_FRACTION_AND_ADJACENT, ICON_EVENT_CTRL);
break;
}
default: {
@ -2379,6 +2461,14 @@ static wmOperatorStatus area_move_modal(bContext *C, wmOperator *op, const wmEve
}
}
WorkspaceStatus status(C);
status.item(IFACE_("Confirm"), ICON_MOUSE_LMB);
status.item(IFACE_("Cancel"), ICON_EVENT_ESC);
status.item_bool(IFACE_("Snap"), md->snap_type == SNAP_FRACTION_AND_ADJACENT, ICON_EVENT_CTRL);
if (md->can_extend) {
status.item_bool(IFACE_("Extend"), md->extending, ICON_EVENT_SHIFT);
}
return OPERATOR_RUNNING_MODAL;
}
@ -2411,6 +2501,53 @@ static void SCREEN_OT_area_move(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Merge Edge Operator
* \{ */
static wmOperatorStatus area_edge_merge_exec(bContext *C, wmOperator *op)
{
int cursor[2];
RNA_int_get_array(op->ptr, "cursor", cursor);
bScreen *screen = CTX_wm_screen(C);
wmWindow *win = CTX_wm_window(C);
ScrEdge *actedge = screen_geom_find_active_scredge(win, screen, cursor[0], cursor[1]);
if (!actedge) {
return OPERATOR_CANCELLED;
}
if (!screen_geom_edge_can_extend(win, actedge)) {
return OPERATOR_CANCELLED;
}
screen_geom_edge_aligned_merge(win, actedge);
WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, nullptr);
return OPERATOR_FINISHED;
}
static void SCREEN_OT_edge_merge(wmOperatorType *ot)
{
PropertyRNA *prop;
/* identifiers */
ot->name = "Merge Area Edge";
ot->description = "Merge aligned area edges";
ot->idname = "SCREEN_OT_edge_merge";
ot->exec = area_edge_merge_exec;
/* flags */
ot->flag = OPTYPE_INTERNAL;
/* rna */
prop = RNA_def_int_vector(
ot->srna, "cursor", 2, nullptr, INT_MIN, INT_MAX, "Cursor", "", INT_MIN, INT_MAX);
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Split Area Operator
* \{ */
@ -2577,7 +2714,7 @@ static bool area_split_apply(bContext *C, wmOperator *op)
return false;
}
sd->narea = area_split(win, screen, sd->sarea, dir_axis, fac, false); /* false = no merge */
sd->narea = area_split(win, screen, sd->sarea, dir_axis, fac);
if (sd->narea == nullptr) {
return false;
@ -2637,8 +2774,6 @@ static void area_split_exit(bContext *C, wmOperator *op)
WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, nullptr);
ED_workspace_status_text(C, nullptr);
/* this makes sure aligned edges will result in aligned grabbing */
BKE_screen_remove_double_scrverts(CTX_wm_screen(C));
BKE_screen_remove_double_scredges(CTX_wm_screen(C));
screen_modal_action_end();
@ -4430,8 +4565,7 @@ void static area_docking_apply(bContext *C, wmOperator *op)
fac = 1.0f - fac;
}
ScrArea *newa = area_split(
jd->win2, WM_window_get_active_screen(jd->win2), jd->sa2, dir, fac, true);
ScrArea *newa = area_split(jd->win2, WM_window_get_active_screen(jd->win2), jd->sa2, dir, fac);
if (jd->factor <= 0.5f) {
jd->sa2 = newa;
@ -4990,8 +5124,7 @@ static wmOperatorStatus area_join_modal(bContext *C, wmOperator *op, const wmEve
WM_window_get_active_screen(jd->win1),
jd->sa1,
jd->split_dir,
jd->split_fac,
true);
jd->split_fac);
const bool large_v = jd->split_dir == SCREEN_AXIS_V &&
((jd->start_x < event->xy[0] && jd->split_fac > 0.5f) ||
@ -5129,7 +5262,8 @@ static wmOperatorStatus screen_area_options_invoke(bContext *C,
const wmEvent *event)
{
ScrArea *sa1, *sa2;
if (screen_area_edge_from_cursor(C, event->xy, &sa1, &sa2) == nullptr) {
ScrEdge *edge = screen_area_edge_from_cursor(C, event->xy, &sa1, &sa2);
if (!edge) {
return OPERATOR_CANCELLED;
}
@ -5197,6 +5331,16 @@ static wmOperatorStatus screen_area_options_invoke(bContext *C,
RNA_int_set_array(&ptr, "cursor", event->xy);
}
if (screen_geom_edge_can_extend(CTX_wm_window(C), edge)) {
layout.separator();
ptr = layout.op("SCREEN_OT_edge_merge",
IFACE_("Merge Edge"),
ICON_NONE,
blender::wm::OpCallContext::ExecDefault,
UI_ITEM_NONE);
RNA_int_set_array(&ptr, "cursor", event->xy);
}
popup_menu_end(C, pup);
return OPERATOR_INTERFACE;
@ -5205,7 +5349,7 @@ static wmOperatorStatus screen_area_options_invoke(bContext *C,
static void SCREEN_OT_area_options(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Area Options";
ot->name = "Area Edge Options";
ot->description = "Operations for splitting and merging";
ot->idname = "SCREEN_OT_area_options";
@ -7335,6 +7479,7 @@ void ED_operatortypes_screen()
/* Screen tools. */
WM_operatortype_append(SCREEN_OT_area_move);
WM_operatortype_append(SCREEN_OT_edge_merge);
WM_operatortype_append(SCREEN_OT_area_split);
WM_operatortype_append(SCREEN_OT_area_join);
WM_operatortype_append(SCREEN_OT_area_close);