diff --git a/luprex/core/cpp/planemap.cpp b/luprex/core/cpp/planemap.cpp index 6aada94e..cfefce3e 100644 --- a/luprex/core/cpp/planemap.cpp +++ b/luprex/core/cpp/planemap.cpp @@ -260,6 +260,9 @@ public: // It is only relevant during the scan. const PlaneScan *scan_config_; IdVector *scan_result_; + util::XYZ scan_lo_; + util::XYZ scan_hi_; + util::XYZ scan_invradius_; int scan_bbxlo_[9], scan_bbxhi_[9]; int scan_bbylo_[9], scan_bbyhi_[9]; int scan_bbzlo_[9], scan_bbzhi_[9]; @@ -462,10 +465,32 @@ public: } } - void calculate_search_bboxes(const PlaneScan &scan) { + // The final filtering step sometimes uses the inverse of the + // radius. In the case that the radius is 0, we want to use a huge + // number for the inverse radius, but not infinity, because using infinity + // would result in the final filtering step calculating (inf*0). + // In the case that the radius is infinite, we want to use zero for the + // inverse radius. + static float inverse_radius(float f) { + if (f == 0) return std::numeric_limits::max(); + if (std::isinf(f)) return 0; + return 1.0f / f; + } + + // Given a PlaneScan, calculate the search bboxes, + // and all the other related configuration data. + void calculate_search_bboxes(const PlaneScan &sc) { + scan_config_ = ≻ + scan_lo_ = sc.center_ - sc.radius_; + scan_hi_ = sc.center_ + sc.radius_; + + scan_invradius_.x = inverse_radius(sc.radius_.x); + scan_invradius_.y = inverse_radius(sc.radius_.y); + scan_invradius_.z = inverse_radius(sc.radius_.z); + // Convert the scan's 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); + NodeInfo bblo(scale_, scan_lo_.x, scan_lo_.y, scan_lo_.z); + NodeInfo bbhi(scale_, scan_hi_.x, scan_hi_.y, scan_hi_.z); // Calculate the bounding box at each level of the tree. NodeID ibblo = bblo.node; @@ -503,37 +528,37 @@ public: return oss.str(); } - static inline void scan_push_id(int64_t id, int64_t special, IdVector *result) { - if (id != special) { + static inline void scan_push_id(int64_t id, int64_t near, IdVector *result) { + if (id != near) { result->push_back(id); } } - static void scan_planeitem(PlaneItem *pi, const PlaneScan &scan, IdVector *result) { - switch (scan.shape_) { + void scan_planeitem(PlaneItem *pi) { + switch (scan_config_->shape_) { case PlaneScan::BOX: { - if ((pi->x() >= scan.lo_.x) && (pi->x() <= scan.hi_.x) && - (pi->y() >= scan.lo_.y) && (pi->y() <= scan.hi_.y) && - (pi->z() >= scan.lo_.z) && (pi->z() <= scan.hi_.z)) { - scan_push_id(pi->id(), scan.special_, result); + if ((pi->x() >= scan_lo_.x) && (pi->x() <= scan_hi_.x) && + (pi->y() >= scan_lo_.y) && (pi->y() <= scan_hi_.y) && + (pi->z() >= scan_lo_.z) && (pi->z() <= scan_hi_.z)) { + scan_push_id(pi->id(), scan_config_->near_, scan_result_); } break; } case PlaneScan::SPHERE: { - float dx = (pi->x() - scan.center_.x) * scan.invradius_.x; - float dy = (pi->y() - scan.center_.y) * scan.invradius_.y; - float dz = (pi->z() - scan.center_.z) * scan.invradius_.z; + float dx = (pi->x() - scan_config_->center_.x) * scan_invradius_.x; + float dy = (pi->y() - scan_config_->center_.y) * scan_invradius_.y; + float dz = (pi->z() - scan_config_->center_.z) * scan_invradius_.z; if (dx*dx + dy*dy + dz*dz <= 1.0) { - scan_push_id(pi->id(), scan.special_, result); + scan_push_id(pi->id(), scan_config_->near_, scan_result_); } break; } case PlaneScan::CYLINDER: { - if ((pi->z() >= scan.lo_.z) && (pi->z() <= scan.hi_.z)) { - float dx = (pi->x() - scan.center_.x) * scan.invradius_.x; - float dy = (pi->y() - scan.center_.y) * scan.invradius_.y; + if ((pi->z() >= scan_lo_.z) && (pi->z() <= scan_hi_.z)) { + float dx = (pi->x() - scan_config_->center_.x) * scan_invradius_.x; + float dy = (pi->y() - scan_config_->center_.y) * scan_invradius_.y; if (dx*dx + dy*dy <= 1.0) { - scan_push_id(pi->id(), scan.special_, result); + scan_push_id(pi->id(), scan_config_->near_, scan_result_); } } break; @@ -557,7 +582,7 @@ public: PlaneItem *pi = first; while (true) { PlaneItem *next = pi->next_; - scan_planeitem(pi, *scan_config_, scan_result_); + scan_planeitem(pi); if (next == first) break; pi = next; } @@ -594,11 +619,10 @@ public: } // Scan a planetree. - void scan(const PlaneScan &sc, IdVector *result, std::ostream *debug) { - scan_config_ = ≻ - scan_result_ = result; + void scan(const PlaneScan &sc, IdVector *result, std::ostream *debug) { calculate_search_bboxes(sc); - + scan_result_ = result; + // We must only call 'scan_node' on nodes that actually exist. // So we check if the tree is empty, and if so, we don't scan the // root node. @@ -758,9 +782,9 @@ PlaneMap::~PlaneMap() {} IdVector PlaneMap::scan(const PlaneScan &sc) const { IdVector result; int startpos = 0; - if (sc.special_ != 0) { - if (!sc.omit_special_) { - result.push_back(sc.special_); + if (sc.near_ != 0) { + if (sc.include_near_) { + result.push_back(sc.near_); startpos = 1; } } @@ -789,12 +813,12 @@ eng::string PlaneMap::outliers_debug_string(const eng::string &plane) { return PlaneTree::get(this, plane)->outliers_debug_string(); } -eng::string PlaneMap::search_bboxes_debug_string(const eng::string &plane, const PlaneScan &scan) { - return PlaneTree::get(this, plane)->search_bboxes_debug_string(scan); +eng::string PlaneMap::search_bboxes_debug_string(const PlaneScan &scan) { + return PlaneTree::get(this, scan.plane_)->search_bboxes_debug_string(scan); } -eng::string PlaneMap::scan_steps_debug_string(const eng::string &plane, const PlaneScan &scan) { - return PlaneTree::get(this, plane)->scan_steps_debug_string(scan); +eng::string PlaneMap::scan_steps_debug_string(const PlaneScan &scan) { + return PlaneTree::get(this, scan.plane_)->scan_steps_debug_string(scan); } void PlaneMap::untrack_all() { @@ -803,12 +827,188 @@ void PlaneMap::untrack_all() { } } -static eng::string tdb(PlaneMap &pm) { - return pm.tree_debug_string("p"); +eng::string PlaneScan::configure(const LuaStack &LS0, LuaSlot config) { + LuaVar val, vx, vy, vz; + LuaStack LS(LS0.state(), val, vx, vy, vz); + + if (!LS.istable(config)) { + return "scan configuration is not a table"; + } + + bool have_plane = false; + bool have_center = false; + bool have_radius = false; + bool have_shape = false; + bool have_near = false; + int parameters = 0; + + LS.rawget(val, config, "plane"); + if (!LS.isnil(val)) { + if (!LS.isstring(val)) { + return "scan configuration: 'plane' must be a string"; + } + plane_ = LS.ckstring(val); + have_plane = true; + parameters += 1; + } + + LS.rawget(vx, config, "centerx"); + LS.rawget(vy, config, "centery"); + LS.rawget(vz, config, "centerz"); + if ((!LS.isnil(vx)) || (!LS.isnil(vy)) || (!LS.isnil(vz))) { + if (!LS.isnumber(vx)) { + return "scan configuration: 'centerx' must be a number"; + } + if (!LS.isnumber(vy)) { + return "scan configuration: 'centery' must be a number"; + } + if (!LS.isnumber(vz)) { + return "scan configuration: 'centerz' must be a number"; + } + center_.x = LS.cknumber(vx); + center_.y = LS.cknumber(vy); + center_.z = LS.cknumber(vz); + have_center = true; + parameters += 3; + } + + LS.rawget(val, config, "radius"); + if (!LS.isnil(val)) { + if (!LS.isnumber(val)) { + return "scan configuration: 'radius' must be a number"; + } + radius_.x = LS.cknumber(val); + radius_.y = radius_.z = radius_.x; + have_radius = true; + parameters += 1; + } + + LS.rawget(vx, config, "radiusx"); + LS.rawget(vy, config, "radiusy"); + LS.rawget(vz, config, "radiusz"); + if ((!LS.isnil(vx)) || (!LS.isnil(vy)) || (!LS.isnil(vz))) { + if (!LS.isnumber(vx)) { + return "scan configuration: 'radiusx' must be a number"; + } + if (!LS.isnumber(vy)) { + return "scan configuration: 'radiusy' must be a number"; + } + if (!LS.isnumber(vz)) { + return "scan configuration: 'radiusz' must be a number"; + } + if (have_radius) { + return "scan configuration: specified both 'radius' and 'radiusx'"; + } + radius_.x = LS.cknumber(vx); + radius_.y = LS.cknumber(vy); + radius_.z = LS.cknumber(vz); + have_radius = true; + parameters += 3; + } + + LS.rawget(val, config, "shape"); + if (!LS.isnil(val)) { + if (!LS.isstring(val)) { + return "scan configuration: 'shape' must be a string"; + } + eng::string shape = LS.ckstring(val); + if (shape == "box") { + shape_ = BOX; + } else if (shape == "sphere") { + shape_ = SPHERE; + } else if (shape == "cylinder") { + shape_ = CYLINDER; + } else { + return util::ss("scan configuration: unknown shape ", shape); + } + have_shape = true; + parameters += 1; + } + + LS.rawget(val, config, "near"); + if (!LS.isnil(val)) { + int64_t id = LS.tanid(val); + if (id == 0) { + return "scan configuration: 'near' must be a tangible"; + } + if (have_center) { + return "scan configuration: specified both 'center' and 'near'"; + } + if (have_plane) { + return "scan configuration: specified both 'plane' and 'near'"; + } + near_ = id; + have_center = true; + have_plane = true; + have_near = true; + parameters += 1; + } + + LS.rawget(val, config, "include"); + if (!LS.isnil(val)) { + if (!LS.isboolean(val)) { + return "scan configuration: 'include' must be a boolean"; + } + if (!have_near) { + return "scan configuration: 'include' specified without 'near'"; + } + include_near_ = LS.ckboolean(val); + parameters += 1; + } + + LS.rawget(val, config, "wholeplane"); + if (!LS.isnil(val)) { + if (!LS.isstring(val)) { + return "scan configuration: 'wholeplane' must be a string"; + } + if (have_plane || have_center || have_radius || have_shape) { + return "scan configuration: do not specify plane, center, shape, or radius with 'wholeplane'"; + } + plane_ = LS.ckstring(val); + set_whole_plane(); + have_plane = true; + have_center = true; + have_radius = true; + have_shape = true; + parameters += 1; + } + + if (lua_nkeys(LS.state(), config.index()) != parameters) { + return "scan configuration: unrecognized parameters in table"; + } + + if (!have_plane) { + return "scan configuration: did not specify plane"; + } + if (!have_radius) { + return "scan configuration: did not specify radius"; + } + if (!have_center) { + return "scan configuration: did not specify center"; + } + if (!have_shape) { + shape_ = SPHERE; // default value. + } + return ""; } -static eng::string odb(PlaneMap &pm) { - return pm.outliers_debug_string("p"); +eng::string PlaneScan::debug_string() const { + eng::ostringstream oss; + oss << "plane:" << plane_ << " center:" << center_ << " radius:" << radius_; + if (shape_ == BOX) oss << " shape:box"; + else if (shape_ == SPHERE) oss << " shape:sphere"; + else if (shape_ == CYLINDER) oss << " shape:cylinder"; + else oss << " shape:unknown"; + if (near_ != 0) { + oss << " near:" << near_ << " include:" << include_near_; + } + if (omit_nowhere_) { + oss << " omit_nowhere:true"; + } + if (!sorted_) { + oss << " sorted:false"; + } + return oss.str(); } // The default radius is set such that float coordinates map directly to @@ -835,7 +1035,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Test track. pi123.set_pos("p", 0x38, 0x16, 0x87); pi123.track(&pm); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" @@ -844,14 +1044,14 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Test untrack. pi123.untrack(); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root"); // 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), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" @@ -862,7 +1062,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Move one of the items into the same cell as the other. pi456.set_xyz(0x38, 0x16, 0x87); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" @@ -871,7 +1071,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Move item 456 back out of the cell. pi456.set_xyz(0x27, 0x11, 0x31); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" @@ -882,7 +1082,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Move item 123 to follow 456. pi123.set_xyz(0x27, 0x11, 0x31); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" @@ -894,8 +1094,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -904,8 +1104,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -914,8 +1114,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -924,8 +1124,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -934,8 +1134,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -944,8 +1144,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:1"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" @@ -954,8 +1154,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" @@ -964,8 +1164,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" @@ -974,8 +1174,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" @@ -984,8 +1184,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" @@ -994,8 +1194,8 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // 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), + LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:1"); + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" @@ -1005,14 +1205,14 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Test the calculation of search bboxes. // The two corners are deliberately not in low-high order. scan.clear(); - scan.set_bbox_given_two_corners(util::XYZ(0x23, 0x97, 0x103), - util::XYZ(0x309, 0x412, 0x27)); - LuaAssertStrEq(L, pm.search_bboxes_debug_string("p", scan), + scan.set_plane("p"); + scan.set_bbox_given_center_radius(util::XYZ(0x23, 0x97, 0x103), 2.0f); + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" - "|Level 4 80,80,80 - 83,84,81" - "|Level 2 802,809,802 - 830,841,810" - "|Level 0 8023,8097,8027 - 8309,8412,8103"); + "|Level 4 80,80,81 - 80,80,81" + "|Level 2 802,809,810 - 802,809,810" + "|Level 0 8021,8095,8101 - 8025,8099,8105"); // TESTS OF SCANNING @@ -1025,8 +1225,9 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // right on the one object. Check the bboxes to make // sure they only include the one cell. scan.clear(); + scan.set_plane("p"); scan.set_bbox_given_center_radius(util::XYZ(0x12, 0x34, 0x45), 0.0); - LuaAssertStrEq(L, pm.search_bboxes_debug_string("p", scan), + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" "|Level 4 80,80,80 - 80,80,80" @@ -1034,7 +1235,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { "|Level 0 8012,8034,8045 - 8012,8034,8045"); // Run the scan with radius zero. It should find the one object. - LuaAssertStrEq(L, pm.scan_steps_debug_string("p", scan), + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" @@ -1050,8 +1251,9 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // bboxes as before, since a half-unit isn't enough to shift // from one cell to the next. scan.clear(); + scan.set_plane("p"); scan.set_bbox_given_center_radius(util::XYZ(0x12 + 0.5, 0x34, 0x45), 0.0); - LuaAssertStrEq(L, pm.search_bboxes_debug_string("p", scan), + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" "|Level 4 80,80,80 - 80,80,80" @@ -1062,7 +1264,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // from the object. It should encounter the cell containing the // one object, but the object should get removed in the final // filtering step. - LuaAssertStrEq(L, pm.scan_steps_debug_string("p", scan), + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" @@ -1077,8 +1279,9 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Next, expand the scan radius to huge. Examine the bboxes // to make sure they cover the entire PlaneTree. scan.clear(); + scan.set_plane("p"); scan.set_bbox_given_center_radius(util::XYZ(0x12, 0x34, 0x45), 100000.0); - LuaAssertStrEq(L, pm.search_bboxes_debug_string("p", scan), + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 0,0,0 - f,f,f" "|Level 4 00,00,00 - ff,ff,ff" @@ -1088,7 +1291,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // Walk the tree using the expansive search bboxes. It should // find the one object, and it should still only traverse the same // cells, because those are the only cells that exist. - LuaAssertStrEq(L, pm.scan_steps_debug_string("p", scan), + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" @@ -1104,7 +1307,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // using the expansive search. It should find both objects. pi456.set_pos("p", 0x14, 0x35, 0x30); pi456.track(&pm); - LuaAssertStrEq(L, pm.scan_steps_debug_string("p", scan), + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" @@ -1120,13 +1323,66 @@ LuaDefine(unittests_planemap, "", "some unit tests") { "| L0:8012,8034,8045 123" "|Result: 123,456"); + // We're going to test that sphere of radius 0.0 works. This is an important + // test because the sphere calculation involves calculating the inverse of + // the radius, which has to be special-cased when radius is zero. We need + // special-case code for this. In this test case, we set up a scan with two + // objects in the same cell, just a smidge apart. Use a sphere scan with + // radius zero, centered right on object 123 (but missing object 456). + pm.untrack_all(); + pi123.set_pos("p", 0x12 + 0.1, 0x34, 0x45); + pi456.set_pos("p", 0x12 + 0.2, 0x34, 0x45); + pi123.track(&pm); + pi456.track(&pm); + scan.clear(); + scan.set_plane("p"); + scan.set_shape(PlaneScan::SPHERE); + scan.set_bbox_given_center_radius(util::XYZ(0x12 + 0.1, 0x34, 0x45), 0.0); + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), + "|L8:root" + "| L7:2,2,2" + "| L6:8,8,8" + "| L5:20,20,20" + "| L4:80,80,80" + "| L3:200,200,201" + "| L2:801,803,804" + "| L1:2004,200d,2011" + "| L0:8012,8034,8045 123,456" + "|Result: 123"); + + // We're going to test that 'whole plane' searches work. + // These use an infinite radius. + pm.untrack_all(); + pi123.set_pos("p", 0x12, 0x34, 0x45); + pi123.track(&pm); + scan.clear(); + scan.set_plane("p"); + scan.set_whole_plane(); + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), + "|Level 8 0,0,0 - 0,0,0" + "|Level 6 0,0,0 - f,f,f" + "|Level 4 00,00,00 - ff,ff,ff" + "|Level 2 000,000,000 - fff,fff,fff" + "|Level 0 0000,0000,0000 - ffff,ffff,ffff"); + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), + "|L8:root" + "| L7:2,2,2" + "| L6:8,8,8" + "| L5:20,20,20" + "| L4:80,80,80" + "| L3:200,200,201" + "| L2:801,803,804" + "| L1:2004,200d,2011" + "| L0:8012,8034,8045 123" + "|Result: 123"); + // Set up a tree with a single object that's outside // the tree's bounding box (an outlier). Print the tree // to verify that the object ended up on the edge. pm.untrack_all(); pi123.set_pos("p", 0x100000, 0x16, 0x23); pi123.track(&pm); - LuaAssertStrEq(L, tdb(pm), + LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:f,8,8" "| L4:ff,80,80" @@ -1138,8 +1394,9 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // contains the outlier. It also contains a few other cells // because the radius is nonzero. scan.clear(); + scan.set_plane("p"); scan.set_bbox_given_center_radius(util::XYZ(0x100000, 0x16, 0x23), 0.2); - LuaAssertStrEq(L, pm.search_bboxes_debug_string("p", scan), + LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 f,8,8 - f,8,8" "|Level 4 ff,80,80 - ff,80,80" @@ -1147,7 +1404,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { "|Level 0 ffff,8015,8022 - ffff,8016,8023"); // Confirm that the scan finds the outlier. - LuaAssertStrEq(L, pm.scan_steps_debug_string("p", scan), + LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:3,2,2" "| L6:f,8,8" diff --git a/luprex/core/cpp/planemap.hpp b/luprex/core/cpp/planemap.hpp index 58cda1ce..ffdbd741 100644 --- a/luprex/core/cpp/planemap.hpp +++ b/luprex/core/cpp/planemap.hpp @@ -97,7 +97,7 @@ private: eng::string plane_; // The bounding box of the scan. - util::XYZ lo_, hi_, center_, invradius_; + util::XYZ center_, radius_; // If you scan a cylinder or SPHERE, it actually // scans the bounding box first, then clips out @@ -109,10 +109,10 @@ private: // nondeterminism. Scans by lua should always be sorted. bool sorted_; - // The special ID, if nonzero, is either PREPENDED to the - // results, or OMITTED from the results, depending on omit_special. - int64_t special_; - bool omit_special_; + // The near ID, if nonzero, is either PREPENDED to the + // results, or OMITTED from the results, depending on include_near_. + int64_t near_; + bool include_near_; // If this is true, items on the nowhere plane are not scanned. bool omit_nowhere_; @@ -122,44 +122,52 @@ public: plane_ = ""; shape_ = BOX; sorted_ = true; - special_ = 0; - omit_special_ = false; + near_ = 0; + include_near_ = false; omit_nowhere_ = false; - lo_ = hi_ = center_ = util::XYZ(); + radius_ = center_ = util::XYZ(); } PlaneScan() { clear(); } // Convert a lua table into a scan configuration. - void configure(LuaStack &LS, LuaSlot slot); + // + // If there is an error in the configuration, returns an + // error message, otherwise returns empty string. + // + // Caution: if this detects the configuration flag 'near', + // then it stores the near ID. However, it doesn't fetch + // the center and plane. It is the caller's responsibility + // to check if 'near' has been set, and if so, store a center + // and plane. This is admittedly ugly, but it eliminates + // a dependency on the world module. + // + eng::string configure(const LuaStack &LS, LuaSlot slot); - // Set the bounding box given two corners. - void set_bbox_given_two_corners(const util::XYZ &a, const util::XYZ &b) { - lo_.x = std::min(a.x, b.x); - lo_.y = std::min(a.y, b.y); - lo_.z = std::min(a.z, b.z); - hi_.x = std::max(a.x, b.x); - hi_.y = std::max(a.y, b.y); - hi_.z = std::max(a.z, b.z); - center_ = (lo_ + hi_) * 0.5; - invradius_.x = 2.0 / (hi_.x - lo_.x); - invradius_.y = 2.0 / (hi_.y - lo_.y); - invradius_.z = 2.0 / (hi_.z - lo_.z); - } - - // Set the bounding box given a center and a radius. void set_bbox_given_center_radius(const util::XYZ ¢er, float r) { - util::XYZ offset(r, r, r); - lo_ = center - offset; - hi_ = center + offset; - center_ = center; - invradius_.x = invradius_.y = invradius_.z = (1.0 / r); + set_center(center); + set_radius(r); } + void set_whole_plane() { + shape_ = BOX; + center_ = util::XYZ(); + radius_.x = std::numeric_limits::infinity(); + radius_.y = radius_.z = radius_.x; + } + + void set_center(const util::XYZ ¢er) { center_ = center; } + void set_radius(const util::XYZ &radius) { radius_ = radius; } + void set_radius(float f) { radius_.x = radius_.y = radius_.z = f; } void set_plane(std::string_view p) { plane_ = p; } void set_shape(Shape s) { shape_ = s; } void set_sorted(bool s) { sorted_ = s; } - void set_special(int64_t id, bool omit) { special_ = id; omit_special_ = omit; } + void set_near(int64_t id, bool inc) { near_ = id; include_near_ = inc; } void set_omit_nowhere(bool b) { omit_nowhere_ = b; } + + int64_t near() const { return near_; } + + // Reveal the contents of the PlaneScan as a string. + eng::string debug_string() const; }; class PlaneItem : public eng::nevernew { @@ -222,9 +230,9 @@ public: // This is for unit testing. eng::string tree_debug_string(const eng::string &plane); eng::string outliers_debug_string(const eng::string &plane); - eng::string search_bboxes_debug_string(const eng::string &plane, const PlaneScan &scan); - eng::string scan_steps_debug_string(const eng::string &plane, const PlaneScan &scan); - + eng::string search_bboxes_debug_string(const PlaneScan &scan); + eng::string scan_steps_debug_string(const PlaneScan &scan); + // Untrack all planeitems. This is for unit testing. void untrack_all(); diff --git a/luprex/core/cpp/util.hpp b/luprex/core/cpp/util.hpp index 3f269606..a15beb4c 100644 --- a/luprex/core/cpp/util.hpp +++ b/luprex/core/cpp/util.hpp @@ -407,5 +407,9 @@ inline std::ostream &operator<<(std::ostream &oss, util::FormattedNumber return oss; } +inline std::ostream &operator<<(std::ostream &oss, const util::XYZ &xyz) { + oss << xyz.x << "," << xyz.y << "," << xyz.z; + return oss; +} #endif // UTIL_HPP diff --git a/luprex/core/cpp/world-accessor.cpp b/luprex/core/cpp/world-accessor.cpp index 445be5d6..c9772913 100644 --- a/luprex/core/cpp/world-accessor.cpp +++ b/luprex/core/cpp/world-accessor.cpp @@ -272,7 +272,7 @@ LuaDefine(tangible_near, "tan,radius,omit_nowhere,omit_self", scan.set_bbox_given_center_radius(aqback.xyz(), LS.cknumber(lradius)); scan.set_shape(PlaneScan::SPHERE); scan.set_sorted(true); - scan.set_special(tan->id(), LS.ckboolean(lomit_self)); + scan.set_near(tan->id(), !LS.ckboolean(lomit_self)); scan.set_omit_nowhere(LS.ckboolean(lomit_nowhere)); util::IdVector idv = w->plane_map_.scan(scan); @@ -301,6 +301,85 @@ LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere", return LS.result(); } +LuaDefine(tangible_find, "config", + "|Find tangibles by their location." + "|" + "|There are multiple ways to specify the plane, the bounding" + "|box, and the shape of the search. The most basic is to" + "|include these parameters in the table:" + "|" + "| plane : the plane to search (a string)" + "| centerx : x-coordinate of the center of the search" + "| centery : y-coordinate of the center of the search" + "| centerz : z-coordinate of the center of the search" + "| radius : the radius of the search" + "| shape : 'box', 'sphere', or 'cylinder'" + "|" + "|Shape has a default: 'sphere'. The other parameters do" + "|not have default values." + "|" + "|Instead of specifying the radius as a single float," + "|you may optionally specify separate radii for each dimension." + "|" + "| radiusx : the radius in the x-dimension." + "| radiusy : the radius in the y-dimension." + "| radiusz : the radius in the z-dimension." + "|" + "|If you specify different radii in each dimension, then the" + "|'sphere' shape will actually be a spheroid, and the 'cylinder'" + "|shape will actually be a cylindroid." + "|" + "|Instead of specifying the center and plane, you can specify" + "|a tangible to search near:" + "|" + "| near : a tangible in whose vicinity the search occurs" + "|" + "|If you specify 'near', then by default, the near tangible is" + "|filtered out of the search. If you want to include it in the" + "|results, set the 'include' flag to true. In this case, the" + "|near tangible will always be the first search result:" + "|" + "| include : include the 'near' object in the results" + "|" + "|If you want to search an entire plane, you can use the" + "|wholeplane option, which replaces all the other options:" + "|" + "| wholeplane: the name of the plane to scan in its entirety" + "|" + "|It is valid to use 0.0 as the radius. If you do so, then only" + "|objects at the exact center you specify will be found." + "|" + "|If you are making a 2D game, it works fine to set all object Z" + "|coordinates to zero, and then do searches with centerz=0.0" + "|and radiusz=0.0." + "|") { + LuaArg config; + LuaRet result; + LuaStack LS(L, config, result); + PlaneScan scan; + eng::string error = scan.configure(LS, config); + if (!error.empty()) { + luaL_error(L, "%s", error.c_str()); + } + // When the configure routine sees the 'near' flag, it stores the tangible + // ID, but not the center and plane, because doing so would require it to + // know about world models. We have to handle center and plane for 'near' + // separately. + World *w = World::fetch_global_pointer(L); + int64_t near = scan.near(); + if (near != 0) { + Tangible *t = w->tangible_get(near); + assert(t != nullptr); // Should never happen. + const AnimStep &aqback = t->anim_queue_.back(); + scan.set_plane(aqback.plane()); + scan.set_center(aqback.xyz()); + } + // Do the scan. + util::IdVector idv = w->plane_map_.scan(scan); + tangible_getall(LS, result, idv); + return LS.result(); +} + LuaDefine(tangible_start, "tangible,function,arg1,arg2...", "|Start a thread." "|" @@ -471,6 +550,9 @@ LuaDefine(math_random, "(args...)", "| (high) - an int between 1 and high inclusive" "| (low, high) - an int between low and high inclusive" "|" + "|You may also pass in a randomstate table" + "|as an optional first argument." + "|" "|math.random tries to cooperate with predictive" "|reexecution to be as predictable as possible." "|To achieve predictability, we used an ad-hoc" diff --git a/luprex/core/cpp/world-core.cpp b/luprex/core/cpp/world-core.cpp index 4589c26f..f889d7f5 100644 --- a/luprex/core/cpp/world-core.cpp +++ b/luprex/core/cpp/world-core.cpp @@ -235,7 +235,7 @@ util::IdVector World::get_near(int64_t player_id, float radius, bool exclude_now scan.set_shape(PlaneScan::SPHERE); scan.set_sorted(sorted); scan.set_omit_nowhere(exclude_nowhere); - scan.set_special(player_id, omit_player); + scan.set_near(player_id, !omit_player); return plane_map_.scan(scan); } diff --git a/luprex/core/cpp/world.hpp b/luprex/core/cpp/world.hpp index d4e70bd6..b506d91d 100644 --- a/luprex/core/cpp/world.hpp +++ b/luprex/core/cpp/world.hpp @@ -552,6 +552,7 @@ private: friend int lfn_tangible_nopredict(lua_State *L); friend int lfn_tangible_near(lua_State *L); friend int lfn_tangible_scan(lua_State *L); + friend int lfn_tangible_find(lua_State *L); friend int lfn_tangible_start(lua_State *L); friend int lfn_math_random(lua_State *L); friend int lfn_math_randomstate(lua_State *L);