Skip to content

Commit 4ef748c

Browse files
committed
Loose improvements to the spatial navigation library.
1 parent c7ae627 commit 4ef748c

File tree

4 files changed

+455
-186
lines changed

4 files changed

+455
-186
lines changed

source/source/lib/spatial_navigation/readme.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Espyo's GUI spatial navigation algorithm.
66
> * [Quick example](#quick-example)
77
> * [Features](#features)
88
9+
910
## Overview
1011

1112
This is a source-only C++ library that implements a spatial navigation algorithm for graphical user interfaces. The way it works is pretty simple:
@@ -19,6 +20,7 @@ This is a source-only C++ library that implements a spatial navigation algorithm
1920

2021
Spatial navigation is surprisingly tricky. Guessing which GUI item the user wanted to focus is difficult, and depends on the program, the GUI, and more. There are also a few things that take work to implement, like the ability to loop around once the edge of the GUI is reached, or how to handle GUI items with more items inside. This library aims to ease those burdens.
2122

23+
2224
## Quick example
2325

2426
```cpp
@@ -41,13 +43,44 @@ void myProgram::doSpatialNavigation(SpatNav::DIRECTION direction) {
4143
4244
## Features
4345
44-
* Support for looping around the edges of the GUI.
45-
* Support for parent items that have children items inside.
46-
* Customizable heuristics.
47-
* Basic debugging logic.
46+
* Support for looping around the edges of the GUI (see `Manager::settings`).
47+
* Support for parent items that have children items that can overflow inside (see `Manager::setParentItem`).
48+
* Customizable heuristics (see `Manager::heuristics`).
49+
* Basic debugging logic (see `SPAT_NAV_DEBUG`).
4850
* Fairly light, and fairly simple.
4951
* Very agnostic, and with no external dependencies.
50-
* Unit tests.
52+
* Simple unit test coverage for the library itself.
53+
54+
55+
## Important information
56+
57+
* For simplicity's sake, each item is identified with a `void *`. In your application you probably identify your widgets with an index number or a pointer, so just cast that to a `void *` freely. `nullptr` (zero) is reserved for "none", however.
58+
* To work with parent and children items:
59+
* Note that parent items are never eligible for being focused.
60+
* For children items, specify their position in normal GUI coordinates, even if they overflow past the parent's borders.
61+
* If you don't provide a valid starting point for navigation, it will use 0,0 for the focus position and size. Your users will probably prefer if you pick the first available widget, or the one closest to the mouse cursor.
62+
63+
64+
## Troubleshooting
65+
66+
* When looping through an edge of the GUI, it's skipping over an item.
67+
* Make sure there's at least a small gap between the items and the edges of the GUI. If it doesn't, you can manually grow the limits by 0.01 without any real drawback.
68+
* The algorithm is picking the wrong item and I can't understand why.
69+
* Try defining `SPAT_NAV_DEBUG` (or uncomment its line near the top of the header file). Then on every frame draw onto the window the contents provided by `Manager::lastNavInfo` in whatever way you want. This should help you understand what the algorithm is doing and what you can customize to make it do what you want.
70+
* There's an item a bit out of the way, but I can't seem to reach it.
71+
* Try `Manager::heuristics::singleLoopPass`.
72+
73+
74+
## Inner workings notes
75+
76+
* For parent and children items:
77+
* Children items that are overflowing get flattened. In essence the algorithm pretends they exist at the border of the parent, just very very thin.
78+
* This approach allows the relative position between items to be kept, whilst also ensuring that navigating into the parent item correctly navigates to the most obvious child item.
79+
* Other approaches were possible, but much more complex to implement.
80+
* Because of this approach, you have to be wary that while hierarchies can be deeper than one level, the deeper you go the sooner you run into floating-point imprecisions. It should be fine for any normal application though.
81+
* The algorithm rotates all widgets such that right (positive X) points to the direction of the navigation.
82+
* Navigation only works in the four cardinal directions. Free angles were considered, but deemed to be way too unreasonable to implement.
83+
5184
5285
## Research
5386

source/source/lib/spatial_navigation/spatial_navigation.cpp

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ bool Interface::addItem(void* id, float x, float y, float w, float h) {
6262
*/
6363
bool Interface::checkHeuristicsPass(
6464
double itemRelX, double itemRelY, double itemRelW, double itemRelH
65-
) {
65+
) const {
6666
if(
6767
heuristics.minBlindspotAngle >= 0.0f ||
6868
heuristics.maxBlindspotAngle >= 0.0f
@@ -171,8 +171,11 @@ void* Interface::doNavigation(
171171
direction, focusX, focusY, focusW, focusH
172172
);
173173

174-
double limitX1, limitY1, limitX2, limitY2;
175-
getLimits(&limitX1, &limitY1, &limitX2, &limitY2);
174+
double limitX1 = settings.limitX1;
175+
double limitY1 = settings.limitY1;
176+
double limitX2 = settings.limitX2;
177+
double limitY2 = settings.limitY2;
178+
getItemLimitsFlattened(&limitX1, &limitY1, &limitX2, &limitY2);
176179

177180
std::map<void*, ItemWithRelUnits> nonLoopedItems;
178181
std::map<void*, ItemWithRelUnits> loopedItems;
@@ -215,18 +218,14 @@ void* Interface::doNavigation(
215218
* This only affects children items that are completely outside, not partially.
216219
*/
217220
void Interface::flattenItems() {
218-
if(
219-
settings.limitX1 == settings.limitX2 ||
220-
settings.limitY1 == settings.limitY2
221-
) {
222-
//Malformed limits.
223-
for(auto& i : items) {
224-
i.second->flatX = i.second->x;
225-
i.second->flatY = i.second->y;
226-
i.second->flatW = i.second->w;
227-
i.second->flatH = i.second->h;
228-
}
229-
return;
221+
double limitX1 = settings.limitX1;
222+
double limitY1 = settings.limitY1;
223+
double limitX2 = settings.limitX2;
224+
double limitY2 = settings.limitY2;
225+
226+
if(limitX1 == limitX2 || limitY1 == limitY2) {
227+
//No specified limits.
228+
getItemLimitsNonFlattened(&limitX1, &limitY1, &limitX2, &limitY2);
230229
}
231230

232231
//Start with the top-level items.
@@ -235,10 +234,7 @@ void Interface::flattenItems() {
235234
if(getItemParent(i.first)) continue;
236235
list.push_back(i.second);
237236
}
238-
flattenItemsInList(
239-
list,
240-
settings.limitX1, settings.limitY1, settings.limitX2, settings.limitY2
241-
);
237+
flattenItemsInList(list, limitX1, limitY1, limitX2, limitY2);
242238
}
243239

244240

@@ -313,7 +309,7 @@ void Interface::flattenItemsInList(
313309
void Interface::getBestItem(
314310
const std::map<void*, ItemWithRelUnits>& list,
315311
double* bestScore, void** bestItemId, bool loopedItems
316-
) {
312+
) const {
317313
for(auto& i : list) {
318314
if(
319315
!checkHeuristicsPass(
@@ -353,12 +349,12 @@ void Interface::getBestItem(
353349
* @param id Identifier of the item whose children to check.
354350
* @return The children, or an empty vector if none.
355351
*/
356-
std::vector<Interface::Item*> Interface::getItemChildren(void* id) {
352+
std::vector<Interface::Item*> Interface::getItemChildren(void* id) const {
357353
std::vector<Item*> result;
358354
const auto& it = children.find(id);
359355
if(it != children.end()) {
360356
for(size_t c = 0; c < it->second.size(); c++) {
361-
result.push_back(items[it->second[c]]);
357+
result.push_back(items.at(it->second[c]));
362358
}
363359
}
364360
return result;
@@ -380,7 +376,7 @@ std::vector<Interface::Item*> Interface::getItemChildren(void* id) {
380376
void Interface::getItemDiffs(
381377
float focusX, float focusY, float focusW, float focusH,
382378
Item* iPtr, DIRECTION direction, double* outDiffX, double* outDiffY
383-
) {
379+
) const {
384380
double focusX1 = focusX - focusW / 2.0f;
385381
double focusY1 = focusY - focusH / 2.0f;
386382
double focusX2 = focusX + focusW / 2.0f;
@@ -429,6 +425,50 @@ void Interface::getItemDiffs(
429425
}
430426

431427

428+
/**
429+
* @brief Returns the limits of all items, using their already-flattened
430+
* coordinates.
431+
*
432+
* @param limitX1 The top-left corner's X coordinate is returned here.
433+
* @param limitY1 The top-left corner's Y coordinate is returned here.
434+
* @param limitX2 The bottom-right corner's X coordinate is returned here.
435+
* @param limitY2 The bottom-right corner's Y coordinate is returned here.
436+
*/
437+
void Interface::getItemLimitsFlattened(
438+
double* limitX1, double* limitY1, double* limitX2, double* limitY2
439+
) const {
440+
for(const auto& i : items) {
441+
Item* iPtr = i.second;
442+
*limitX1 = std::min(*limitX1, iPtr->flatX - iPtr->flatW / 2.0f);
443+
*limitY1 = std::min(*limitY1, iPtr->flatY - iPtr->flatH / 2.0f);
444+
*limitX2 = std::max(*limitX2, iPtr->flatX + iPtr->flatW / 2.0f);
445+
*limitY2 = std::max(*limitY2, iPtr->flatY + iPtr->flatH / 2.0f);
446+
}
447+
}
448+
449+
450+
/**
451+
* @brief Returns the limits of all items, using their normal,
452+
* non-flattened coordinates.
453+
*
454+
* @param limitX1 The top-left corner's X coordinate is returned here.
455+
* @param limitY1 The top-left corner's Y coordinate is returned here.
456+
* @param limitX2 The bottom-right corner's X coordinate is returned here.
457+
* @param limitY2 The bottom-right corner's Y coordinate is returned here.
458+
*/
459+
void Interface::getItemLimitsNonFlattened(
460+
double* limitX1, double* limitY1, double* limitX2, double* limitY2
461+
) const {
462+
for(const auto& i : items) {
463+
Item* iPtr = i.second;
464+
*limitX1 = std::min(*limitX1, (double) (iPtr->x - iPtr->w / 2.0f));
465+
*limitY1 = std::min(*limitY1, (double) (iPtr->y - iPtr->h / 2.0f));
466+
*limitX2 = std::max(*limitX2, (double) (iPtr->x + iPtr->w / 2.0f));
467+
*limitY2 = std::max(*limitY2, (double) (iPtr->y + iPtr->h / 2.0f));
468+
}
469+
}
470+
471+
432472
/**
433473
* @brief Converts the standard coordinates of an item to ones relative
434474
* to the current focus coordinates, and rotated so they're to its right.
@@ -448,7 +488,7 @@ void Interface::getItemRelativeUnits(
448488
Item* iPtr, DIRECTION direction,
449489
float focusX, float focusY, float focusW, float focusH,
450490
double* outRelX, double* outRelY, double* outRelW, double* outRelH
451-
) {
491+
) const {
452492
double resultX = 0.0f;
453493
double resultY = 0.0f;
454494
double resultW = 0.0f;
@@ -513,10 +553,10 @@ void Interface::getItemRelativeUnits(
513553
* @param id Identifier of the item whose parent to check.
514554
* @return The parent, or nullptr if none.
515555
*/
516-
Interface::Item* Interface::getItemParent(void* id) {
556+
Interface::Item* Interface::getItemParent(void* id) const {
517557
const auto& it = parents.find(id);
518558
if(it == parents.end()) return nullptr;
519-
return items[it->second];
559+
return items.at(it->second);
520560
}
521561

522562

@@ -531,7 +571,7 @@ Interface::Item* Interface::getItemParent(void* id) {
531571
*/
532572
double Interface::getItemScore(
533573
double itemRelX, double itemRelY, double itemRelW, double itemRelH
534-
) {
574+
) const {
535575
switch(heuristics.distCalcMethod) {
536576
case DIST_CALC_METHOD_EUCLIDEAN: {
537577
return itemRelX * itemRelX + itemRelY * itemRelY;
@@ -564,7 +604,7 @@ std::map<void*, Interface::ItemWithRelUnits>
564604
Interface::getItemsWithRelativeUnits(
565605
DIRECTION direction,
566606
float focusX, float focusY, float focusW, float focusH
567-
) {
607+
) const {
568608
std::map<void*, Interface::ItemWithRelUnits> result;
569609

570610
for(auto& i : items) {
@@ -589,32 +629,6 @@ Interface::getItemsWithRelativeUnits(
589629
}
590630

591631

592-
/**
593-
* @brief Returns the limits of the given items.
594-
*
595-
* @param limitX1 The top-left corner's X coordinate is returned here.
596-
* @param limitY1 The top-left corner's Y coordinate is returned here.
597-
* @param limitX2 The bottom-right corner's X coordinate is returned here.
598-
* @param limitY2 The bottom-right corner's Y coordinate is returned here.
599-
*/
600-
void Interface::getLimits(
601-
double* limitX1, double* limitY1, double* limitX2, double* limitY2
602-
) const {
603-
*limitX1 = settings.limitX1;
604-
*limitY1 = settings.limitY1;
605-
*limitX2 = settings.limitX2;
606-
*limitY2 = settings.limitY2;
607-
608-
for(const auto& i : items) {
609-
Item* iPtr = i.second;
610-
*limitX1 = std::min(*limitX1, iPtr->flatX - iPtr->flatW / 2.0f);
611-
*limitY1 = std::min(*limitY1, iPtr->flatY - iPtr->flatH / 2.0f);
612-
*limitX2 = std::max(*limitX2, iPtr->flatX + iPtr->flatW / 2.0f);
613-
*limitY2 = std::max(*limitY2, iPtr->flatY + iPtr->flatH / 2.0f);
614-
}
615-
}
616-
617-
618632
/**
619633
* @brief Returns whether an item has children.
620634
*

source/source/lib/spatial_navigation/spatial_navigation.h

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,17 @@ class Interface {
7373
//--- Members ---
7474

7575
//Top-left corner's X coordinate.
76+
//If not specified, i.e. left at the default values, the limits will
77+
//be automatically calculated based on the existing items.
7678
float limitX1 = 0.0f;
7779

78-
//Top-left corner's Y coordinate.
80+
//Same as limitX1, but for the top-left corner's Y coordinate.
7981
float limitY1 = 0.0f;
8082

81-
//Bottom-right corner's X coordinate.
83+
//Same as limitX1, but for the bottom-right corner's X coordinate.
8284
float limitX2 = 0.0f;
8385

84-
//Bottom-right corner's Y coordinate.
86+
//Same as limitX1, but for the bottom-right corner's Y coordinate.
8587
float limitY2 = 0.0f;
8688

8789
//Whether it loops around when it reaches a horizontal limit.
@@ -190,7 +192,7 @@ class Interface {
190192
* @brief Represents an item in the interface. It can be inside of a parent
191193
* item.
192194
*/
193-
class Item {
195+
struct Item {
194196

195197
public:
196198

@@ -267,7 +269,7 @@ class Interface {
267269

268270
bool checkHeuristicsPass(
269271
double itemRelX, double itemRelY, double itemRelW, double itemRelH
270-
);
272+
) const;
271273
bool checkLoopRelativeCoordinates(
272274
DIRECTION direction, double* itemRelX,
273275
double limitX1, double limitY1, double limitX2, double limitY2,
@@ -285,26 +287,29 @@ class Interface {
285287
void getBestItem(
286288
const std::map<void*, ItemWithRelUnits>& list,
287289
double* bestScore, void** bestItemId, bool loopedItems
288-
);
290+
) const;
289291
void getItemRelativeUnits(
290292
Item* iPtr, DIRECTION direction,
291293
float focusX, float focusY, float focusW, float focusH,
292294
double* outRelX, double* outRelY, double* outRelW, double* outRelH
293-
);
294-
std::vector<Item*> getItemChildren(void* id);
295+
) const;
296+
std::vector<Item*> getItemChildren(void* id) const;
295297
void getItemDiffs(
296298
float focusX, float focusY, float focusW, float focusH,
297299
Item* iPtr, DIRECTION direction, double* outDiffX, double* outDiffY
298-
);
299-
Item* getItemParent(void* id);
300+
) const;
301+
Item* getItemParent(void* id) const;
300302
double getItemScore(
301303
double itemRelX, double itemRelY, double itemRelW, double itemRelH
302-
);
304+
) const;
303305
std::map<void*, ItemWithRelUnits> getItemsWithRelativeUnits(
304306
DIRECTION direction,
305307
float focusX, float focusY, float focusW, float focusH
306-
);
307-
void getLimits(
308+
) const;
309+
void getItemLimitsFlattened(
310+
double* limitX1, double* limitY1, double* limitX2, double* limitY2
311+
) const;
312+
void getItemLimitsNonFlattened(
308313
double* limitX1, double* limitY1, double* limitX2, double* limitY2
309314
) const;
310315
bool itemHasChildren(void* id) const;

0 commit comments

Comments
 (0)