-
-
Notifications
You must be signed in to change notification settings - Fork 25.9k
Add new default algorithm of automatic focus strategy for Control nodes
#120631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,42 @@ bool Rect2::intersects_segment(const Point2 &p_from, const Point2 &p_to, Point2 | |
| return true; | ||
| } | ||
|
|
||
| bool Rect2::intersects_ray(const Point2 &p_from, const Vector2 &p_dir, Point2 *r_pos) const { | ||
| #ifdef MATH_CHECKS | ||
| if (unlikely(size.x < 0 || size.y < 0)) { | ||
| ERR_PRINT("Rect2 size is negative, this is not supported. Use Rect2.abs() to get a Rect2 with a positive size."); | ||
| } | ||
| #endif | ||
| // Slab method for ray-AABB intersection. See https://tavianator.com/fast-branchless-raybounding-box-intersections/ | ||
|
|
||
| Vector2 dir = p_dir.normalized(); | ||
| // Avoid division by zero by ensuring an epsilon value. | ||
| if (Math::is_zero_approx(dir.x)) { | ||
| dir.x = 1e-10; | ||
| } | ||
| if (Math::is_zero_approx(dir.y)) { | ||
| dir.y = 1e-10; | ||
| } | ||
|
Comment on lines
+124
to
+129
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we can rely on IEEE 754 behavior, we can avoid this check and allow division by zero. This results in INF / -INF values which are correctly handled by this algorithm. See https://tavianator.com/fast-branchless-raybounding-box-intersections/ Not sure if we can do that though, as e.g. I've found explicit warning silence |
||
|
|
||
| real_t t_x1 = (position.x - p_from.x) / dir.x; | ||
| real_t t_x2 = (position.x + size.x - p_from.x) / dir.x; | ||
| real_t t_y1 = (position.y - p_from.y) / dir.y; | ||
| real_t t_y2 = (position.y + size.y - p_from.y) / dir.y; | ||
|
|
||
| real_t t_min = MAX(MIN(t_x1, t_x2), MIN(t_y1, t_y2)); | ||
| real_t t_max = MIN(MAX(t_x1, t_x2), MAX(t_y1, t_y2)); | ||
|
|
||
| if (t_max < 0 || t_min > t_max) { | ||
| return false; | ||
| } | ||
|
|
||
| if (r_pos) { | ||
| *r_pos = p_from + dir * (t_min >= 0 ? t_min : t_max); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool Rect2::intersects_transformed(const Transform2D &p_xform, const Rect2 &p_rect) const { | ||
| #ifdef MATH_CHECKS | ||
| if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1233,6 +1233,11 @@ | |||||
| <member name="filesystem/import/fbx2gltf/enabled.web" type="bool" setter="" getter="" default="false"> | ||||||
| Override for [member filesystem/import/fbx2gltf/enabled] on the Web where FBX2glTF can't easily be accessed from Godot. | ||||||
| </member> | ||||||
| <member name="gui/common/auto_focus_strategy" type="int" setter="" getter="" default="0"> | ||||||
| Determines what strategy to use when inferring the next [Control] to focus if no neighbor is defined. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| - [b]Legacy[/b] is the old focus strategy up until Godot 4.8. It shouldn't be used for new projects, as the new default (Balloon) yields more intuitive results. | ||||||
| - [b]Balloon[/b] searches for both the closest and most aligned [Control] node in the input direction, akin to a expanding balloon which stops at the first node it finds. | ||||||
| </member> | ||||||
| <member name="gui/common/default_scroll_deadzone" type="int" setter="" getter="" default="0"> | ||||||
| Default value for [member ScrollContainer.scroll_deadzone], which will be used for all [ScrollContainer]s unless overridden. | ||||||
| </member> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,7 @@ STATIC_ASSERT_INCOMPLETE_TYPE(class, RenderingServer); | |||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/config/engine.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/config/project_settings.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/input/input_map.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/math/geometry_2d.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/math/transform_2d.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/object/callable_mp.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
| #include "core/object/class_db.h" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3325,6 +3326,7 @@ NodePath Control::get_focus_previous() const { | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #define MAX_NEIGHBOR_SEARCH_COUNT 512 | ||||||||||||||||||||||||||||||||||||||||||||||||
| #define NO_SCORE 1e10 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Control *Control::_get_focus_neighbor(Side p_side, int p_count) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function still has some logic which I believe is tied to the now legacy algorithm, especially when concerning nodes within a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Snippets of what I mean, which have been left mostly untouched: Lines 3358 to 3366 in fd98f84
Lines 3382 to 3395 in fd98f84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This refers to a situation where focus moves from a sibling control of the |
||||||||||||||||||||||||||||||||||||||||||||||||
| ERR_FAIL_INDEX_V((int)p_side, 4, nullptr); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3345,7 +3347,7 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { | |||||||||||||||||||||||||||||||||||||||||||||||
| return c; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t square_of_dist = 1e14; | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t score = NO_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
| Control *result = nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const Vector2 dir[4] = { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3395,7 +3397,7 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (follow_focus || sc_maxd > maxd) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(vdir, base, r, clamp, maxd, square_of_dist, &result); | ||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(vdir, base, r, clamp, maxd, score, &result); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (result == nullptr) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3441,7 +3443,7 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { | |||||||||||||||||||||||||||||||||||||||||||||||
| return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(vdir, base, r, clamp, maxd, square_of_dist, &result); | ||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(vdir, base, r, clamp, maxd, score, &result); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3450,7 +3452,7 @@ Control *Control::find_valid_focus_neighbor(Side p_side) const { | |||||||||||||||||||||||||||||||||||||||||||||||
| return const_cast<Control *>(this)->_get_focus_neighbor(p_side); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min, real_t &r_closest_dist_squared, Control **r_closest) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min, real_t &r_score, Control **r_closest) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (Object::cast_to<Viewport>(p_at)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; // Bye. | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3462,45 +3464,22 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons | |||||||||||||||||||||||||||||||||||||||||||||||
| bool in_container = container ? container->is_ancestor_of(this) : false; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (c && c != this && ((c->get_focus_mode_with_override() == FOCUS_ALL) || (ac_enabled && c->get_focus_mode_with_override() == FOCUS_ACCESSIBILITY)) && !in_container && p_clamp.intersects(c->get_global_rect())) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| Rect2 r_c = c->get_global_rect(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| r_c = r_c.intersection(p_clamp); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t begin_d = p_dir.dot(r_c.get_position()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t end_d = p_dir.dot(r_c.get_end()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t max = MAX(begin_d, end_d); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Use max to allow navigation to overlapping controls (for ScrollContainer case). | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (max > (p_min + CMP_EPSILON)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Calculate the shortest distance. (No shear transform) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Flip along axis(es) so that C falls in the first quadrant of c (as origin) for easy calculation. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The same transformation would put the direction vector in the positive direction (+x or +y). | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | ------------- | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | |-----C-----| | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ----|---a | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | b------------ | ||||||||||||||||||||||||||||||||||||||||||||||||
| // -|---c---|-----------------------> | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ----|---- | ||||||||||||||||||||||||||||||||||||||||||||||||
| // cC = ca + ab + bC | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The shortest distance is the vector ab's length or its positive projection length. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 cC_origin = r_c.get_center() - p_rect.get_center(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 cC = cC_origin.abs(); // Converted to fall in the first quadrant of c. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 ab = cC - 0.5 * r_c.get_size() - 0.5 * p_rect.get_size(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t min_d_squared = 0.0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ab.x > 0.0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| min_d_squared += ab.x * ab.x; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ab.y > 0.0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| min_d_squared += ab.y * ab.y; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t score = NO_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
| switch (GLOBAL_GET_CACHED(int, "gui/common/auto_focus_strategy")) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| case AutoFocusStrategy::STRATEGY_LEGACY: | ||||||||||||||||||||||||||||||||||||||||||||||||
| score = _focus_strategy_legacy(p_dir, *c, p_rect, p_clamp, p_min); | ||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||
| case AutoFocusStrategy::STRATEGY_BALLOON: | ||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||
| score = _focus_strategy_balloon(p_dir, *c, p_rect, p_clamp, p_min); | ||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (min_d_squared < r_closest_dist_squared || *r_closest == nullptr) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| r_closest_dist_squared = min_d_squared; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (score != NO_SCORE) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (score < r_score || *r_closest == nullptr) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| r_score = score; | ||||||||||||||||||||||||||||||||||||||||||||||||
| *r_closest = c; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (min_d_squared == r_closest_dist_squared) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (score == r_score) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Tie-breaking aims to address situations where a potential focus neighbor's bounding rect | ||||||||||||||||||||||||||||||||||||||||||||||||
| // is right next to the currently focused control (e.g. in BoxContainer with | ||||||||||||||||||||||||||||||||||||||||||||||||
| // separation overridden to 0). This needs specific handling so that the correct | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3511,7 +3490,7 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons | |||||||||||||||||||||||||||||||||||||||||||||||
| Point2 closest_center = closest->get_global_rect().get_center(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Tie-break in favor of the control most aligned with p_dir. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (Math::abs(p_dir.cross(cC_origin)) < Math::abs(p_dir.cross(closest_center - p_center))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (Math::abs(p_dir.cross(c->get_rect().get_center() - p_center)) < Math::abs(p_dir.cross(closest_center - p_center))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| *r_closest = c; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3543,8 +3522,119 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons | |||||||||||||||||||||||||||||||||||||||||||||||
| continue; // Already searched in it, skip it. | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(p_dir, child, p_rect, intersection, p_min, r_closest_dist_squared, r_closest); | ||||||||||||||||||||||||||||||||||||||||||||||||
| _window_find_focus_neighbor(p_dir, child, p_rect, intersection, p_min, r_score, r_closest); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t Control::_focus_strategy_legacy(const Vector2 &p_dir, const Control &p_c, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| Rect2 r_c = p_c.get_global_rect(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| r_c = r_c.intersection(p_clamp); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t begin_d = p_dir.dot(r_c.get_position()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t end_d = p_dir.dot(r_c.get_end()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t max = MAX(begin_d, end_d); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Use max to allow navigation to overlapping controls (for ScrollContainer case). | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (max > (p_min + CMP_EPSILON)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Calculate the shortest distance. (No shear transform) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Flip along axis(es) so that C falls in the first quadrant of c (as origin) for easy calculation. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The same transformation would put the direction vector in the positive direction (+x or +y). | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | ------------- | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | |-----C-----| | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ----|---a | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | b------------ | ||||||||||||||||||||||||||||||||||||||||||||||||
| // -|---c---|-----------------------> | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | | | | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ----|---- | ||||||||||||||||||||||||||||||||||||||||||||||||
| // cC = ca + ab + bC | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The shortest distance is the vector ab's length or its positive projection length. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 cC_origin = r_c.get_center() - p_rect.get_center(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 cC = cC_origin.abs(); // Converted to fall in the first quadrant of c. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 ab = cC - 0.5 * r_c.get_size() - 0.5 * p_rect.get_size(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t min_d_squared = 0.0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ab.x > 0.0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| min_d_squared += ab.x * ab.x; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ab.y > 0.0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| min_d_squared += ab.y * ab.y; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return min_d_squared; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return NO_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 Control::_focus_strategy_balloon_line_segment(const Vector2 &p_start, const Vector2 &p_dir, const Vector2 &p1, const Vector2 &p2) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Slightly tweaked line intersection algorithm, which clamps out-of-bounds intersections | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 line_dir = p2 - p1; | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 normal = Vector2(-line_dir.y, line_dir.x).normalized(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 intersect_dir = p_dir + normal; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 touch(Math::INF, Math::INF); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Geometry2D::line_intersects_line(p_start, intersect_dir, p1, line_dir, touch); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Clamp resulting intersection to P1-P2 segment | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ((touch - p2).dot(-line_dir) < 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| touch = p2; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ((touch - p1).dot(line_dir) < 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| touch = p1; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return touch; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t Control::_focus_strategy_balloon(const Vector2 &p_dir, const Control &p_candidate, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Algorithm proposed and designed by Rune Skovbo Johansen & Adriaan de Jongh | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @runevision @AdriaandeJongh I advocate for proper attribution when possible, and I want to make it clear the origins of this algorithm. Let me know what do you think of this attribution and any changes if you want 🙂
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I appreciate the shoutout! But... not sure how common this is in the Godot codebase 😅 @AThousandShips wdyt? |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Compute the starting point by intersecting the input direction to a normalized Rect2. | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 starting_point; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const Rect2 normalized_rect = Rect2(0, 0, 1, 1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!normalized_rect.intersects_ray(normalized_rect.get_center(), p_dir, &starting_point)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Should be impossible to not have an intersection for rays starting within the Rect2 (even for dir == Vector2.ZER0, as the algorithm applies an epsilon) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Do we assert? [[ unlikely ]]? ERR_FAIL_COND? | ||||||||||||||||||||||||||||||||||||||||||||||||
| return NO_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3598
to
+3600
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need clarification on how Godot handles theoretically-impossible scenarios. |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert the normalized intersection by first restoring local size, then multiplying with global transform. | ||||||||||||||||||||||||||||||||||||||||||||||||
| starting_point *= get_size(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| starting_point = get_global_transform_const().xform(starting_point); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| real_t score = -1; | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 candidate_point; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Fetch the corners of the Control's rect in global coordinates. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const Rect2 candidate_rect = p_candidate.get_global_rect().intersection(p_clamp); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const Transform2D candidate_transform = p_candidate.get_global_transform_const(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 point_a = candidate_transform.xform(Vector2()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 point_b = candidate_transform.xform(Vector2(candidate_rect.size.x, 0)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 point_c = candidate_transform.xform(candidate_rect.size); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 point_d = candidate_transform.xform(Vector2(0, candidate_rect.size.y)); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const auto testCandidate = [&](const Vector2 &point_1, const Vector2 &point_2) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 new_touch = _focus_strategy_balloon_line_segment(starting_point, p_dir, point_1, point_2); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Vector2 hit = new_touch - starting_point; | ||||||||||||||||||||||||||||||||||||||||||||||||
| real_t new_score = p_dir.dot(hit) / hit.length_squared(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (new_score > score) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| score = new_score; | ||||||||||||||||||||||||||||||||||||||||||||||||
| candidate_point = new_touch; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Test all four edges of a Control's Rect | ||||||||||||||||||||||||||||||||||||||||||||||||
| testCandidate(point_a, point_b); | ||||||||||||||||||||||||||||||||||||||||||||||||
| testCandidate(point_b, point_c); | ||||||||||||||||||||||||||||||||||||||||||||||||
| testCandidate(point_c, point_d); | ||||||||||||||||||||||||||||||||||||||||||||||||
| testCandidate(point_d, point_a); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Valid scores are in the range of ]0, 1]. For this algorithm, higher scores are better; however, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Godot is looking for minimal score, so we invert the range. | ||||||||||||||||||||||||||||||||||||||||||||||||
| return score > 0 ? (1 - score) : NO_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Rendering. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd make the new algorithm the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current state, any newly created project will use the new algorithm by default, while current projects maintain the legacy one. This follows the same "maintaing old behavior" mechanism as some other settings (e.g. Jolt Physics on 3D, D3D12 renderer on Windows).
So far this algorithm behaves better in all scenarios I've tested (still need to check ScrollContainer's though), and I agree in making this the default option going forward, but since there is concern about behavior changes in the issue/proposal discussion, I've left the legacy behavior for current projects.