Files
integration/luprex/core/cpp/planemap.cpp

833 lines
28 KiB
C++
Raw Normal View History

2022-07-11 02:32:12 -04:00
// PlaneTree is an octree-like data structure.
//
// THE COORDINATE SPACE
//
// The tree stores objects which have (x,y,z) coordinates where each of x,y,z is
// an unsigned 16-bit integer. The API uses floating-point coordinates, but
// these are converted to uint16_t for use in the tree.
//
// In an octree, each node has a 2x2x2 array of children (total: 8 children). In
// this tree, however, each node has a 4x4x4 array of children (total: 64
// children).
//
// The depth of the tree is fixed. It always has one leaf level (level 0), plus
// 8 internal levels (levels 1-8). The root of the tree is level 8. Tangibles
// are always are always stored in the leaf-level. Since there are 8
// subdivisions, at 4x4x4 per subdivision, that divides the space into 65536 x
// 65536 x 65536. Hence the 16-bit integer coordinates.
//
// Here are the sizes of the levels:
//
// * Level 8 (internal) is 1 cubed.
// * Level 7 (internal) is 4 cubed.
// * Level 6 (internal) is 16 cubed.
// * Level 5 (internal) is 64 cubed.
// * Level 4 (internal) is 256 cubed.
// * Level 3 (internal) is 1024 cubed.
// * Level 2 (internal) is 4096 cubed.
// * Level 1 (internal) is 16385 cubed.
// * Level 0 (leaf) is 65536 cubed.
//
// NODE IDS AND THE NODE TABLES
//
// Every tree node has a unique "NodeID", which consists of it's level and the
// lowest (x,y,z) coordinate in its region. Every tree node is stored in a hash
// table that maps NodeID to the data for that tree node.
//
// Tree nodes don't contain any child pointers or parent pointers. Instead, you
// can calculate the NodeIDs of the parent and children from the NodeID of a
// node. So, you can just look up parents and children in the hash table. Even
// though PlaneTree really is a tree, it doesn't use pointers at all.
//
// There are two separate hash tables - one for storing internal nodes, and one
// for storing leaf nodes. The only data stored in an internal node is a
// bitvector indicating which of its 64 children are nonempty. The only data
// stored in a leaf node is a list of PlaneItems. The list of PlaneItems is
// stored as an intrusive doubly-linked circular list.
//
// THE PLANETREE BOUNDING BOX, OUTLIERS, AND ADAPTIVE RESIZING
//
// The planetree as a whole has a bounding box. For now, the bounding box is
// fixed at (-65536, -65536, -65536) to (65536, 65536, 65536). That makes the
// size of the leaf cells two meters. However, it is my plan to eventually have
// the bounding box selected adaptively.
//
// "Outliers" are objects that are outside the PlaneTree's bounding box. When an
// object is an outlier, its (X,Y,Z) coordinates are clamped to the tree's
// bounding box for purposes of storing it in the tree. This puts all the
// outliers into the cells on the edge of the tree. When we need to scan a
// region that includes areas outside the tree's bounding box, we scan the edge
// cells.
//
// The tree keeps a count of the number of outliers. The intended use for these
// outlier counts is to implement adaptive logic that says "are there too many
// outliers? If so, expand the tree's bounding box." But we haven't written the
// adaptive algorithm yet, so the outlier counts don't serve any purpose yet.
//
// We probably *don't* want to capture 100% of the outliers. It seems likely
// that the programmer will deliberately put a few objects way off in the
// distance. For example, you might imagine somebody creating a "LoginManager"
// object and putting it way off at (1000000, 0, 0) in order to get it out of
// the way. If we were to expand the bounding box to include such objects, we
// would make the tree's bounding box unreasonably large. I have an idea for a
// heuristic: expand the bounding box until it covers 90% of the tangibles, then
// expand it by 25% more, then stop. I have another idea for a different
// heuristic: keep expanding the bounding box as long as making a small
// increment will capture significantly more tangibles, then stop. I don't know
// if either of those heuristics will work well.
//
// Any adaptive algorithm we create must take another factor into account: we
// don't want the cell size to be too small. Overly small cell sizes create lots
// of extra work when an object moves around. So sometimes, it's advantageous
// to deliberately expand the bounding box way beyond what is actually needed to
// capture the outliers, just to make the cell size large enough. Doing this
// isn't wasteful: octrees are very good at handling big empty spaces inside the
// bounding box. As long as the cell size is large enough, but not too large,
// and there aren't too many outliers, everything will work well. Unfortunately,
// I haven't devised a good heuristic to decide when the cell size is "right."
//
// Another thought I have, periodically, is that writing a lot of logic just to
// choose a cell size seems like a lot of unnecessary work: we could just
// configure the cell size for the game in the script.
//
// BYPASSING THE TREE
//
// In many cases, it's possible to jump directly to a leaf node and start
// processing. For example, let's say you want to move a tangible. You know
// its current position, therefore, you know exactly the NodeID of the leaf node
// where it is stored. There is no need to walk the tree to get there: you can
// just access it directly using the hash table.
//
#include "luastack.hpp"
#include "util.hpp"
2021-01-23 14:44:06 -05:00
#include "planemap.hpp"
#include <algorithm>
#include <cmath>
2022-07-11 02:32:12 -04:00
using NodeID = uint64_t;
using ChildBits = uint64_t;
using IdVector = util::IdVector;
2022-07-11 02:32:12 -04:00
// These need to be static floats to encourage gcc to generate
// efficient code in NodeInfo.
static float k_lo = 0.0f;
static float k_hi = 65535.0f;
static constexpr ChildBits child_bit(int i) {
return (uint64_t(1) << i);
}
2022-07-11 02:32:12 -04:00
static constexpr uint8_t node_get_level(NodeID node) {
return node >> 48;
}
2022-07-11 02:32:12 -04:00
static constexpr uint16_t node_get_x(NodeID node) {
return uint16_t(node >> 32);
}
2022-07-11 02:32:12 -04:00
static constexpr uint16_t node_get_y(NodeID node) {
return uint16_t(node >> 16);
}
static constexpr uint16_t node_get_z(NodeID node) {
return uint16_t(node >> 0);
}
static constexpr NodeID node_lxyz(uint8_t level, uint16_t x, uint16_t y, uint16_t z) {
return (NodeID(level) << 48) | (NodeID(x) << 32) | (NodeID(y) << 16) | (NodeID(z) << 0);
}
2022-07-11 02:32:12 -04:00
static constexpr NodeID node_parent(NodeID node) {
uint8_t level = node_get_level(node);
return ((node & node_lxyz(0, 0xFFFC, 0xFFFC, 0xFFFC)) >> 2) | node_lxyz(level + 1, 0, 0, 0);
}
2022-07-11 02:32:12 -04:00
static constexpr uint8_t node_childindex(NodeID node) {
uint64_t masked = node & node_lxyz(0, 0x0003, 0x0003, 0x0003);
return (masked >> 0) | (masked >> 14) | (masked >> 28);
}
2022-07-11 02:32:12 -04:00
static constexpr ChildBits node_childbit(NodeID node) {
return child_bit(node_childindex(node));
}
static constexpr NodeID node_nthchild(NodeID node, int i) {
int level = node_get_level(node);
uint16_t x = (i >> 4)&3;
uint16_t y = (i >> 2)&3;
uint16_t z = (i >> 0)&3;
return ((node & node_lxyz(0, 0xFFFF, 0xFFFF, 0xFFFF)) << 2) | node_lxyz(level - 1, x, y, z);
}
static void print_node_id(NodeID node, std::ostream *os) {
int level = node_get_level(node);
if (level >= 8) {
(*os) << "L" << level << ":root";
return;
}
2022-07-11 02:32:12 -04:00
uint16_t x = node_get_x(node);
uint16_t y = node_get_y(node);
uint16_t z = node_get_z(node);
auto fmt = util::hex.width(4 - (level >> 1)).fill('0');
(*os) << "L" << level << ":" << fmt.val(x) << "," << fmt.val(y) << "," << fmt.val(z);
}
enum BBoxCheck { INSIDE_BBOX, JUST_OUTSIDE_BBOX, WAY_OUTSIDE_BBOX };
struct NodeInfo {
NodeID node;
BBoxCheck bbcheck;
NodeInfo(float scale, float x, float y, float z) {
float sx = (x * scale);
float sy = (y * scale);
float sz = (z * scale);
float dist = std::max({std::abs(sx), std::abs(sy), std::abs(sz)});
if (dist >= 32768.0f) {
bbcheck = (dist > (32768.0f + 8192.0f)) ? WAY_OUTSIDE_BBOX : JUST_OUTSIDE_BBOX;
float clampx = std::min(k_hi, std::max(k_lo, sx + 32768.0f));
float clampy = std::min(k_hi, std::max(k_lo, sy + 32768.0f));
float clampz = std::min(k_hi, std::max(k_lo, sz + 32768.0f));
node = node_lxyz(0, clampx, clampy, clampz);
} else {
node = node_lxyz(0, sx + 32768.0f, sy + 32768.0f, sz + 32768.0f);
bbcheck = INSIDE_BBOX;
}
}
2022-07-11 02:32:12 -04:00
};
// Class PlaneTree. Everything here is 'public', but this class
// is only visible inside this one C++ file.
class PlaneTree : public eng::opnew {
public:
void set_radius(float r);
using NodeID = uint64_t;
using ChildBits = uint64_t;
using IdVector = util::IdVector;
// The PlaneMap that this tree is a part of.
PlaneMap *planemap_;
// The name of this plane.
eng::string plane_;
// Internal nodes in the tree just have bits indicating
// which children exist.
eng::bytell_hash_map<NodeID, ChildBits> internal_nodes_;
// Leaf nodes in the tree contain a doubly-linked
// intrusive ring.
eng::bytell_hash_map<NodeID, PlaneItem *> leaf_nodes_;
// The radius of the bounding box.
double radius_;
// A conversion factor to convert float coordinates to
// integral coordinates. Equal to 32k / radius.
float scale_;
// total number of items in the planetree.
int total_count_;
// total number of outliers in the planetree. Outliers are
// classified as just outside or way outside the bbox.
int just_outside_bbox_;
int way_outside_bbox_;
public:
// Untrack all planeitems. This is for unit testing and for destructors. We
// don't use PlaneItem::untrack, because that would create problems with
// removing items from a list while iterating over that list.
void untrack_all() {
for (auto &l : leaf_nodes_) {
PlaneItem *first = l.second;
PlaneItem *pi = first;
assert(pi != nullptr);
while (true) {
PlaneItem *next = pi->next_;
pi->tree_= nullptr;
pi = next;
if (pi == first) break;
}
}
leaf_nodes_.clear();
internal_nodes_.clear();
}
2022-07-11 02:32:12 -04:00
// This just sets the radius.
// It verifies that the tree is empty first.
void set_radius_of_empty_tree(float r) {
assert(total_count_ == 0);
radius_ = r;
scale_ = 32768.0 / r;
}
2022-07-11 02:32:12 -04:00
// Get a PlaneTree by plane name.
static PlaneTree *get(PlaneMap *pmap, const eng::string &plane) {
std::unique_ptr<PlaneTree> &result = pmap->planes_[plane];
if (result == nullptr) {
result.reset(new PlaneTree(pmap, plane));
}
return result.get();
}
// Remove an item from the specified leaf node.
// Returns true if we removed the last item from the leaf.
bool remove_planeitem_from_leaf(NodeID node, PlaneItem *item) {
if (item->next_ == item) {
leaf_nodes_.erase(node);
// Note: these next two assignments are only needed for sanity checking.
item->next_ = nullptr;
item->prev_ = nullptr;
return true;
} else {
item->prev_->next_ = item->next_;
item->next_->prev_ = item->prev_;
// Note: these next two assignments are only needed for sanity checking.
item->next_ = nullptr;
item->prev_ = nullptr;
return false;
}
}
// Insert an item into the specified leaf node.
// Returns true if we inserted the first item into the leaf.
bool insert_planeitem_into_leaf(NodeID node, PlaneItem *item) {
PlaneItem *&newcell = leaf_nodes_[node];
if (newcell == nullptr) {
newcell = item;
item->next_ = item;
item->prev_ = item;
return true;
} else {
PlaneItem *next = newcell;
PlaneItem *prev = newcell->prev_;
item->next_ = next;
item->prev_ = prev;
prev->next_ = item;
next->prev_ = item;
return false;
}
}
// Update the parent to reflect the fact that the child was added.
// This will propagage all the way up the tree.
void insert_child_into_childbits(NodeID child) {
uint8_t childlevel = node_get_level(child);
uint8_t parentlevel = childlevel + 1;
NodeID parent = node_parent(child);
ChildBits &inode = internal_nodes_[parent];
bool waszero = (inode == 0);
inode |= node_childbit(child);
if (waszero) {
if (parentlevel < 8) insert_child_into_childbits(parent);
}
}
// Update the parent to reflect the fact that the child was removed.
// This will propagage all the way up the tree.
void remove_child_from_childbits(NodeID child) {
uint8_t childlevel = node_get_level(child);
uint8_t parentlevel = childlevel + 1;
NodeID parent = node_parent(child);
auto iter = internal_nodes_.find(parent);
iter->second &= (~node_childbit(child));
if (iter->second == 0) {
internal_nodes_.erase(parent);
if (parentlevel < 8) remove_child_from_childbits(parent);
}
}
// Update the counters to reflect the removal of one item from the tree.
void decrement_planeitem_counters(BBoxCheck bbcheck) {
total_count_ -= 1;
if (bbcheck == JUST_OUTSIDE_BBOX) just_outside_bbox_ -= 1;
if (bbcheck == WAY_OUTSIDE_BBOX) way_outside_bbox_ -= 1;
}
// Update the counters to reflect the insertion of one item into the tree.
void increment_planeitem_counters(BBoxCheck bbcheck) {
total_count_ += 1;
if (bbcheck == JUST_OUTSIDE_BBOX) just_outside_bbox_ += 1;
if (bbcheck == WAY_OUTSIDE_BBOX) way_outside_bbox_ += 1;
}
// Remove a planeitem from whatever tree it is in, preserving
// all invariants. The planeitem ends up being an untracked PlaneItem.
static void remove_planeitem(PlaneItem *item) {
PlaneTree *tree = item->tree_;
assert(tree != nullptr);
NodeInfo info(tree->scale_, item->x_, item->y_, item->z_);
tree->decrement_planeitem_counters(info.bbcheck);
if (tree->remove_planeitem_from_leaf(info.node, item)) {
tree->remove_child_from_childbits(info.node);
}
item->tree_ = nullptr;
}
// Insert a planeitem into whatever tree is specified, preserving
// all invariants. The planeitem must be an untracked PlaneItem.
void insert_planeitem(PlaneItem *item) {
PlaneTree *tree = this;
assert(item->tree_ == nullptr);
NodeInfo info(tree->scale_, item->x_, item->y_, item->z_);
tree->increment_planeitem_counters(info.bbcheck);
if (tree->insert_planeitem_into_leaf(info.node, item)) {
tree->insert_child_into_childbits(info.node);
}
item->tree_ = tree;
item->plane_ = tree->plane_;
}
// Scan a planetree.
void scan(const PlaneScan &scan, IdVector *result) {
// Convert the bounding box to integral coordinates.
NodeInfo bblo(scale_, scan.lo_.x, scan.lo_.y, scan.lo_.z);
NodeInfo bbhi(scale_, scan.hi_.x, scan.hi_.y, scan.hi_.z);
// NOT IMPLEMENTED YET
}
void print_planeitem_ids(PlaneItem *first, std::ostream *os) {
util::IdVector ids;
if (first == nullptr) {
(*os) << "invalid null list";
return;
}
PlaneItem *pi = first;
while (true) {
PlaneItem *next = pi->next_;
ids.push_back(pi->id());
if (next == first) break;
pi = next;
}
std::sort(ids.begin(), ids.end());
if (ids.size() > 0) {
(*os) << ids[0];
}
for (int i = 1; i < int(ids.size()); i++) {
(*os) << "," << ids[i];
}
}
void print_tree_r(NodeID node, std::ostream *os) {
int level = node_get_level(node);
int indent = 8 - level;
if (level == 0) {
(*os) << "| ";
print_node_id(node, os);
(*os) << " ";
auto iter = leaf_nodes_.find(node);
if (iter == leaf_nodes_.end()) {
(*os) << "no such leaf";
} else {
print_planeitem_ids(iter->second, os);
}
} else {
auto iter = internal_nodes_.find(node);
ChildBits cb = 0;
if (iter != internal_nodes_.end()) {
cb = iter->second;
}
if ((level & 1) == 0) {
(*os) << "|";
for (int i = 0; i < indent; i++) (*os) << " ";
print_node_id(node, os);
if ((cb == 0) && (level != 8)) {
(*os) << " (invalid empty node)";
}
}
for (int i = 0; i < 64; i++) {
if (cb & child_bit(i)) {
NodeID child = node_nthchild(node, i);
assert(node_childindex(child) == i);
print_tree_r(child, os);
}
}
}
}
eng::string tree_debug_string() {
eng::ostringstream oss;
print_tree_r(node_lxyz(8,0,0,0), &oss);
return oss.str();
}
eng::string outliers_debug_string() {
eng::ostringstream oss;
oss << "total:" << total_count_ << " justout:" << just_outside_bbox_ << " wayout:" << way_outside_bbox_;
return oss.str();
}
// Construct a PlaneTree.
PlaneTree(PlaneMap *pmap, const eng::string &plane) {
planemap_ = pmap;
plane_ = plane;
total_count_ = 0;
just_outside_bbox_ = 0;
way_outside_bbox_ = 0;
set_radius_of_empty_tree(pmap->default_radius_);
}
// Destructor: the PlaneTree doesn't own the PlaneItems, so it doesn't
// delete them, but it needs to untrack all the PlaneItems.
~PlaneTree() {
untrack_all();
}
};
void PlaneItem::track(PlaneMap *pmap) {
// If we're already in a PlaneMap, and it's not the
// PlaneMap we want to be in, remove from the old PlaneMap.
if ((tree_ != nullptr) && (tree_->planemap_ != pmap)) {
PlaneTree::remove_planeitem(this);
}
// If we're supposed to be in a PlaneMap, and we're not
// already in the PlaneMap, insert it.
if ((tree_ == nullptr) && (pmap != nullptr)) {
PlaneTree::get(pmap, plane_)->insert_planeitem(this);
}
}
void PlaneItem::set_pos(const eng::string &plane, float x, float y, float z) {
2022-07-11 02:32:12 -04:00
// If we're not in a PlaneMap, nothing to do but set the variables.
if (tree_ == nullptr) {
plane_ = plane;
x_ = x;
y_ = y;
z_ = z;
return;
}
2022-07-11 02:32:12 -04:00
// When moving within a plane (not warping), use set_xyz, which is faster.
if (plane_ == plane) {
set_xyz(x, y, z);
return;
}
// We're warping from one plane to another. That means we're removing
// ourself from one planetree and inserting ourself into a different one.
PlaneTree *newtree = PlaneTree::get(tree_->planemap_, plane);
PlaneTree::remove_planeitem(this);
2021-01-18 00:52:35 -05:00
x_ = x;
y_ = y;
2022-07-11 02:32:12 -04:00
z_ = z;
newtree->insert_planeitem(this);
}
2022-07-11 02:32:12 -04:00
void PlaneItem::set_xyz(float x, float y, float z) {
// If we're not in a PlaneMap, nothing to do but set the variables.
if (tree_ == nullptr) {
x_ = x;
y_ = y;
z_ = z;
return;
}
2021-01-12 14:14:38 -05:00
2022-07-11 02:32:12 -04:00
// We could implement this function using 'PlaneTree::remove_planeitem'
// and 'PlaneTree::insert_planeitem', which would be less code, but it
// would also be slower.
NodeInfo old_cell(tree_->scale_, x_, y_, z_);
NodeInfo new_cell(tree_->scale_, x, y, z);
// Update the variables.
x_ = x;
y_ = y;
z_ = z;
// Update the outliers counters (unlikely).
if (old_cell.bbcheck != new_cell.bbcheck) {
tree_->decrement_planeitem_counters(old_cell.bbcheck);
tree_->increment_planeitem_counters(new_cell.bbcheck);
}
2022-07-11 02:32:12 -04:00
// If we have changed cells, update the tree.
// We have to remove the child from the old leaf before inserting
// it into the new leaf, because the 'next' and 'prev' pointers are
// intrusive and we need them to be unused to do the insert.
// However, inserting the child into the childbits first is faster
// and poses no problems.
if (new_cell.node != old_cell.node) {
bool leaf_removed = tree_->remove_planeitem_from_leaf(old_cell.node, this);
bool leaf_created = tree_->insert_planeitem_into_leaf(new_cell.node, this);
if (leaf_created) tree_->insert_child_into_childbits(new_cell.node);
if (leaf_removed) tree_->remove_child_from_childbits(old_cell.node);
}
}
2022-07-11 02:32:12 -04:00
PlaneItem::PlaneItem() {
id_ = 0;
tree_ = nullptr;
next_ = nullptr;
prev_ = nullptr;
x_ = y_ = z_ = 0.0;
}
PlaneItem::~PlaneItem() {
2021-01-12 14:14:38 -05:00
untrack();
}
2022-07-11 02:32:12 -04:00
PlaneMap::PlaneMap() : default_radius_(32768.0) {}
PlaneMap::~PlaneMap() {}
IdVector PlaneMap::scan(const PlaneScan &scan) const {
IdVector result;
int startpos = 0;
if (scan.special_ != 0) {
if (!scan.omit_special_) {
result.push_back(scan.special_);
startpos = 1;
}
}
2022-07-11 02:32:12 -04:00
if (scan.omit_nowhere_ && (scan.plane_ == "nowhere")) {
return result;
}
auto piter = planes_.find(scan.plane_);
if (piter != planes_.end()) {
2022-07-11 02:32:12 -04:00
const std::unique_ptr<PlaneTree> &tree = piter->second;
tree->scan(scan, &result);
}
if (scan.sorted_) {
std::sort(result.begin() + startpos, result.end());
}
return result;
}
2022-07-11 02:32:12 -04:00
eng::string PlaneMap::tree_debug_string(const eng::string &plane) const {
auto iter = planes_.find(plane);
if (iter == planes_.end()) {
return "no such plane";
} else {
return iter->second->tree_debug_string();
}
}
2022-07-11 02:32:12 -04:00
eng::string PlaneMap::outliers_debug_string(const eng::string &plane) const {
auto iter = planes_.find(plane);
if (iter == planes_.end()) {
return "no such plane";
} else {
return iter->second->outliers_debug_string();
}
}
2022-07-11 02:32:12 -04:00
void PlaneMap::untrack_all() {
for (const auto &pair : planes_) {
pair.second->untrack_all();
}
}
2022-07-11 02:32:12 -04:00
static eng::string tdb(const PlaneMap &pm) {
return pm.tree_debug_string("p");
}
2022-07-11 02:32:12 -04:00
static eng::string odb(const PlaneMap &pm) {
return pm.outliers_debug_string("p");
}
2022-07-11 02:32:12 -04:00
// The default radius is set such that float coordinates map directly to
// integer coordinates, with an offset of 0x8000. This makes unit testing
// a lot easier.
//
LuaDefine(unittests_planemap, "", "some unit tests") {
PlaneMap pm;
PlaneItem pi123, pi456;
pi123.set_id(123);
pi456.set_id(456);
// Test that PlaneItems can be manipulated when they're not
// yet tracking a PlaneMap.
pi123.set_pos("p", 0x38, 0x16, 0x87);
LuaAssert(L, pi123.plane() == "p");
LuaAssert(L, pi123.x() == 0x38);
LuaAssert(L, pi123.y() == 0x16);
LuaAssert(L, pi123.z() == 0x87);
2022-07-11 02:32:12 -04:00
// TESTS OF TREE MANIPULATION FOLLOW.
2022-07-11 02:32:12 -04:00
// Test track.
pi123.set_pos("p", 0x38, 0x16, 0x87);
pi123.track(&pm);
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,8,8"
"| L4:80,80,80"
"| L2:803,801,808"
"| L0:8038,8016,8087 123");
// Test untrack.
pi123.untrack();
LuaAssertStrEq(L, tdb(pm),
"|L8:root");
2022-07-11 02:32:12 -04:00
// Track two items at a time, not in the same cell.
pi456.set_pos("p", 0x12, 0x17, 0xAC);
pi123.track(&pm);
pi456.track(&pm);
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,8,8"
"| L4:80,80,80"
"| L2:801,801,80a"
"| L0:8012,8017,80ac 456"
"| L2:803,801,808"
"| L0:8038,8016,8087 123");
2022-07-11 02:32:12 -04:00
// Move one of the items into the same cell as the other.
pi456.set_xyz(0x38, 0x16, 0x87);
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,8,8"
"| L4:80,80,80"
"| L2:803,801,808"
"| L0:8038,8016,8087 123,456");
// Move item 456 back out of the cell.
pi456.set_xyz(0x27, 0x11, 0x31);
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,8,8"
"| L4:80,80,80"
"| L2:802,801,803"
"| L0:8027,8011,8031 456"
"| L2:803,801,808"
"| L0:8038,8016,8087 123");
2022-07-11 02:32:12 -04:00
// Move item 123 to follow 456.
pi123.set_xyz(0x27, 0x11, 0x31);
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,8,8"
"| L4:80,80,80"
"| L2:802,801,803"
"| L0:8027,8011,8031 123,456");
// TESTS OF OUTLIER CLAMPING FOLLOW.
// Move item 456 close to, but not quite on the positive edge.
pi123.untrack();
pi456.set_xyz(0x23, 0x7FFE, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,fffe,8027 456");
// Move item 456 so that it's on the positive edge, but not an outlier.
pi456.set_xyz(0x23, 0x7FFF, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,ffff,8027 456");
// Move item 456 so that it's even closer to the positive edge, but not an outlier.
pi456.set_xyz(0x23, 0x7FFF + 0.99, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,ffff,8027 456");
// Move item 456 so that it's just barely a positive outlier.
pi456.set_xyz(0x23, 0x8000, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:1 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,ffff,8027 456");
// Move item 456 so that it's considerably past the positive edge.
pi456.set_xyz(0x23, 0x8048, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:1 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,ffff,8027 456");
// Move item 456 so that it's way past the positive edge.
pi456.set_xyz(0x23, 0x83748, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:1");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,f,8"
"| L4:80,ff,80"
"| L2:802,fff,802"
"| L0:8023,ffff,8027 456");
// Move item 456 close to, but not quite on the negative edge.
pi456.set_xyz(0x23, -0x7fff, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,0,8"
"| L4:80,00,80"
"| L2:802,000,802"
"| L0:8023,0001,8027 456");
// Move item 456 so that it's on the negative edge, but not an outlier.
pi456.set_xyz(0x23, -0x7fff - 0.5, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,0,8"
"| L4:80,00,80"
"| L2:802,000,802"
"| L0:8023,0000,8027 456");
// Move item 456 so that it's just barely a negative outlier.
pi456.set_xyz(0x23, -0x8000, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:1 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,0,8"
"| L4:80,00,80"
"| L2:802,000,802"
"| L0:8023,0000,8027 456");
// Move item 456 so that it's significantly past the negative edge.
pi456.set_xyz(0x23, -0x8048, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:1 wayout:0");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,0,8"
"| L4:80,00,80"
"| L2:802,000,802"
"| L0:8023,0000,8027 456");
// Move item 456 so that it's way past the negative edge.
pi456.set_xyz(0x23, -0x83048, 0x27);
LuaAssertStrEq(L, odb(pm), "total:1 justout:0 wayout:1");
LuaAssertStrEq(L, tdb(pm),
"|L8:root"
"| L6:8,0,8"
"| L4:80,00,80"
"| L2:802,000,802"
"| L0:8023,0000,8027 456");
2021-01-22 17:35:23 -05:00
return 0;
}