From 8f3db988528a1d49827962f4886cbf1e4ffca47e Mon Sep 17 00:00:00 2001 From: Dmitrijs Date: Fri, 23 Jan 2026 22:46:23 +0100 Subject: [PATCH 1/4] Initial attempt to switch to quadtree based search and remove the "Grid" limitation --- src/CMakeLists.txt | 2 + src/map/SpatialDb.cpp | 89 +++--- src/map/SpatialDb.h | 50 +++- src/map/SpatialIndex.cpp | 596 +++++++++++++++++++++++++++++++++++++++ src/map/SpatialIndex.h | 261 +++++++++++++++++ src/map/World.cpp | 11 +- src/map/coordinate.h | 14 + 7 files changed, 950 insertions(+), 73 deletions(-) create mode 100644 src/map/SpatialIndex.cpp create mode 100644 src/map/SpatialIndex.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b485245b8..80f72b617 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -308,6 +308,8 @@ set(mmapper_SRCS map/ServerIdMap.h map/SpatialDb.cpp map/SpatialDb.h + map/SpatialIndex.cpp + map/SpatialIndex.h map/TinyRoomIdSet.cpp map/TinyRoomIdSet.h map/World-BaseMap.cpp diff --git a/src/map/SpatialDb.cpp b/src/map/SpatialDb.cpp index e4114515d..90df830e7 100644 --- a/src/map/SpatialDb.cpp +++ b/src/map/SpatialDb.cpp @@ -1,85 +1,66 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2024 The MMapper Authors + #include "SpatialDb.h" #include "../global/AnsiOstream.h" #include "../global/progresscounter.h" -NODISCARD static bool mightBeOnBoundary(const Coordinate &coord, const Bounds &bounds) +const RoomId *SpatialDb::findUnique(const Coordinate &key) const { -#define CHECK(_axis) ((bounds.min._axis) == (coord._axis) || (bounds.max._axis) == (coord._axis)) - return CHECK(x) || CHECK(y) || CHECK(z); -#undef CHECK + m_cachedFindResult = m_index.findFirst(key); + if (m_cachedFindResult.has_value()) { + return &m_cachedFindResult.value(); + } + return nullptr; } -const RoomId *SpatialDb::findUnique(const Coordinate &key) const +TinyRoomIdSet SpatialDb::findRooms(const Coordinate &key) const { - return m_unique.find(key); + return m_index.findAt(key); } -void SpatialDb::remove(const RoomId /*id*/, const Coordinate &coord) +std::optional SpatialDb::findFirst(const Coordinate &key) const { - m_unique.erase(coord); - - if (!m_bounds || mightBeOnBoundary(coord, *m_bounds)) { - m_needsBoundsUpdate = true; - } + return m_index.findFirst(key); } -void SpatialDb::add(const RoomId id, const Coordinate &coord) +bool SpatialDb::hasRoomAt(const Coordinate &key) const { - if (!m_bounds) { - m_bounds.emplace(coord, coord); - } else { - m_bounds->insert(coord); - } - m_unique.set(coord, id); + return m_index.hasRoomAt(key); } -void SpatialDb::move(const RoomId id, const Coordinate &from, const Coordinate &to) +TinyRoomIdSet SpatialDb::findInBounds(const Bounds &bounds) const { - if (from == to) { - return; - } - - remove(id, from); - add(id, to); + return m_index.findInBounds(bounds); } -void SpatialDb::updateBounds(ProgressCounter &pc) +TinyRoomIdSet SpatialDb::findInRadius(const Coordinate ¢er, const int radius) const { - m_bounds.reset(); - m_needsBoundsUpdate = false; - if (m_unique.empty()) { - return; - } + return m_index.findInRadius(center, radius); +} - const auto &c = m_unique.begin()->first; - m_bounds.emplace(c, c); - pc.increaseTotalStepsBy(m_unique.size()); - m_unique.for_each([&](const auto &kv) { - m_bounds->insert(kv.first); - pc.step(); - }); +void SpatialDb::remove(const RoomId id, const Coordinate &coord) +{ + m_index.remove(id, coord); } -void SpatialDb::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const +void SpatialDb::add(const RoomId id, const Coordinate &coord) { - if (m_bounds.has_value()) { - const auto &bounds = m_bounds.value(); - const Coordinate &max = bounds.max; - const Coordinate &min = bounds.min; + m_index.insert(id, coord); +} - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); +void SpatialDb::move(const RoomId id, const Coordinate &from, const Coordinate &to) +{ + m_index.move(id, from, to); +} - auto show = [&os](std::string_view prefix, int lo, int hi) { - os << prefix << ColoredValue(green, hi - lo + 1) << " (" << ColoredValue(green, lo) - << " to " << ColoredValue(green, hi) << ").\n"; - }; +void SpatialDb::updateBounds(ProgressCounter &pc) +{ + m_index.updateBounds(pc); +} - os << "\n"; - show("Width (West to East): ", min.x, max.x); - show("Height (South to North): ", min.y, max.y); - show("Layers (Down to Up): ", min.z, max.z); - } +void SpatialDb::printStats(ProgressCounter &pc, AnsiOstream &os) const +{ + m_index.printStats(pc, os); } diff --git a/src/map/SpatialDb.h b/src/map/SpatialDb.h index 9a4d5675e..6101c5bcd 100644 --- a/src/map/SpatialDb.h +++ b/src/map/SpatialDb.h @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2021 The MMapper Authors -#include "../global/ImmUnorderedMap.h" #include "../global/macros.h" +#include "SpatialIndex.h" +#include "TinyRoomIdSet.h" #include "coordinate.h" #include "roomid.h" @@ -12,26 +13,38 @@ class AnsiOstream; class ProgressCounter; +/// SpatialDb provides spatial indexing for rooms. +/// Now backed by SpatialIndex (quadtree) which supports multiple rooms per coordinate. struct NODISCARD SpatialDb final { private: - /// Value is the last room assigned to the coordinate. - ImmUnorderedMap m_unique; - -private: - std::optional m_bounds; - bool m_needsBoundsUpdate = true; + SpatialIndex m_index; public: - NODISCARD bool needsBoundsUpdate() const - { - return m_needsBoundsUpdate || !m_bounds.has_value(); - } - NODISCARD const std::optional &getBounds() const { return m_bounds; } + NODISCARD bool needsBoundsUpdate() const { return m_index.needsBoundsUpdate(); } + NODISCARD std::optional getBounds() const { return m_index.getBounds(); } public: + /// Find first room at coordinate (legacy interface, returns nullptr if none) + /// @deprecated Use findRooms() for new code NODISCARD const RoomId *findUnique(const Coordinate &key) const; + /// Find all rooms at coordinate (new interface) + NODISCARD TinyRoomIdSet findRooms(const Coordinate &key) const; + + /// Find first room at coordinate (new interface, cleaner than findUnique) + NODISCARD std::optional findFirst(const Coordinate &key) const; + + /// Check if any room exists at coordinate + NODISCARD bool hasRoomAt(const Coordinate &key) const; + +public: + /// Find all rooms within bounding box + NODISCARD TinyRoomIdSet findInBounds(const Bounds &bounds) const; + + /// Find all rooms within radius of center + NODISCARD TinyRoomIdSet findInRadius(const Coordinate ¢er, int radius) const; + public: void remove(RoomId id, const Coordinate &coord); void add(RoomId id, const Coordinate &coord); @@ -45,11 +58,18 @@ struct NODISCARD SpatialDb final void for_each(Callback &&callback) const { static_assert(std::is_invocable_r_v); - m_unique.for_each([&callback](const auto &p) { callback(p.first, p.second); }); + m_index.forEach([&callback](const RoomId id, const Coordinate &coord) { + callback(coord, id); + }); } - NODISCARD auto size() const { return m_unique.size(); } + + NODISCARD size_t size() const { return m_index.size(); } public: - NODISCARD bool operator==(const SpatialDb &rhs) const { return m_unique == rhs.m_unique; } + NODISCARD bool operator==(const SpatialDb &rhs) const { return m_index == rhs.m_index; } NODISCARD bool operator!=(const SpatialDb &rhs) const { return !(rhs == *this); } + +private: + /// Cached result for findUnique (to return pointer) + mutable std::optional m_cachedFindResult; }; diff --git a/src/map/SpatialIndex.cpp b/src/map/SpatialIndex.cpp new file mode 100644 index 000000000..59465177e --- /dev/null +++ b/src/map/SpatialIndex.cpp @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2025 The MMapper Authors + +#include "SpatialIndex.h" + +#include "../global/AnsiOstream.h" +#include "../global/progresscounter.h" + +#include +#include + +namespace spatial { + +// ============================================================================ +// QuadtreeNode implementation +// ============================================================================ + +QuadtreeNode::QuadtreeNode(const int minX, const int minY, const int maxX, const int maxY) + : m_minX{minX} + , m_minY{minY} + , m_maxX{maxX} + , m_maxY{maxY} +{} + +QuadtreeNode::~QuadtreeNode() = default; + +QuadtreeNode::QuadtreeNode(const QuadtreeNode &other) + : m_minX{other.m_minX} + , m_minY{other.m_minY} + , m_maxX{other.m_maxX} + , m_maxY{other.m_maxY} + , m_rooms{other.m_rooms} + , m_isLeaf{other.m_isLeaf} +{ + for (size_t i = 0; i < NUM_QUADRANTS; ++i) { + if (other.m_children[i]) { + m_children[i] = std::make_unique(*other.m_children[i]); + } + } +} + +bool QuadtreeNode::contains(const int x, const int y) const +{ + return x >= m_minX && x < m_maxX && y >= m_minY && y < m_maxY; +} + +QuadtreeNode::Quadrant QuadtreeNode::getQuadrant(const int x, const int y) const +{ + const int centerX = getCenterX(); + const int centerY = getCenterY(); + + if (x < centerX) { + return (y >= centerY) ? Quadrant::NorthWest : Quadrant::SouthWest; + } else { + return (y >= centerY) ? Quadrant::NorthEast : Quadrant::SouthEast; + } +} + +bool QuadtreeNode::shouldSubdivide() const +{ + if (!m_isLeaf) { + return false; + } + + // Don't subdivide if too small + if (getWidth() <= QuadtreeConfig::MIN_SQUARE_SIZE + || getHeight() <= QuadtreeConfig::MIN_SQUARE_SIZE) { + return false; + } + + // Count total rooms across all coordinates + size_t totalRooms = 0; + for (const auto &[coord, rooms] : m_rooms) { + totalRooms += rooms.size(); + } + + return totalRooms > QuadtreeConfig::MAX_LEAF_ROOMS; +} + +void QuadtreeNode::subdivide() +{ + if (!m_isLeaf) { + return; + } + + const int centerX = getCenterX(); + const int centerY = getCenterY(); + + // Create child nodes + m_children[static_cast(Quadrant::NorthWest)] = + std::make_unique(m_minX, centerY, centerX, m_maxY); + m_children[static_cast(Quadrant::NorthEast)] = + std::make_unique(centerX, centerY, m_maxX, m_maxY); + m_children[static_cast(Quadrant::SouthWest)] = + std::make_unique(m_minX, m_minY, centerX, centerY); + m_children[static_cast(Quadrant::SouthEast)] = + std::make_unique(centerX, m_minY, m_maxX, centerY); + + m_isLeaf = false; + + // Move existing rooms to children + for (auto &[coord, rooms] : m_rooms) { + for (const RoomId id : rooms) { + insertIntoChild(id, coord.x, coord.y); + } + } + m_rooms.clear(); +} + +void QuadtreeNode::insertIntoChild(const RoomId id, const int x, const int y) +{ + const auto quadrant = getQuadrant(x, y); + auto &child = m_children[static_cast(quadrant)]; + + // Create child on-demand if it doesn't exist + if (!child) { + const int centerX = getCenterX(); + const int centerY = getCenterY(); + + switch (quadrant) { + case Quadrant::NorthWest: + child = std::make_unique(m_minX, centerY, centerX, m_maxY); + break; + case Quadrant::NorthEast: + child = std::make_unique(centerX, centerY, m_maxX, m_maxY); + break; + case Quadrant::SouthWest: + child = std::make_unique(m_minX, m_minY, centerX, centerY); + break; + case Quadrant::SouthEast: + child = std::make_unique(centerX, m_minY, m_maxX, centerY); + break; + } + } + + child->insert(id, x, y); +} + +void QuadtreeNode::insert(const RoomId id, const int x, const int y) +{ + if (m_isLeaf) { + const Coordinate2i coord{x, y}; + m_rooms[coord].insert(id); + + if (shouldSubdivide()) { + subdivide(); + } + } else { + insertIntoChild(id, x, y); + } +} + +void QuadtreeNode::remove(const RoomId id, const int x, const int y) +{ + if (m_isLeaf) { + const Coordinate2i coord{x, y}; + if (auto it = m_rooms.find(coord); it != m_rooms.end()) { + it->second.erase(id); + if (it->second.empty()) { + m_rooms.erase(it); + } + } + } else { + const auto quadrant = getQuadrant(x, y); + if (auto &child = m_children[static_cast(quadrant)]) { + child->remove(id, x, y); + } + } +} + +TinyRoomIdSet QuadtreeNode::findAt(const int x, const int y) const +{ + if (m_isLeaf) { + const Coordinate2i coord{x, y}; + if (auto it = m_rooms.find(coord); it != m_rooms.end()) { + return it->second; + } + return {}; + } + + const auto quadrant = getQuadrant(x, y); + if (const auto &child = m_children[static_cast(quadrant)]) { + return child->findAt(x, y); + } + return {}; +} + +TinyRoomIdSet QuadtreeNode::findInBounds(const int minX, + const int minY, + const int maxX, + const int maxY) const +{ + // Check if this node intersects the query bounds + if (m_maxX <= minX || m_minX >= maxX || m_maxY <= minY || m_minY >= maxY) { + return {}; + } + + TinyRoomIdSet result; + + if (m_isLeaf) { + for (const auto &[coord, rooms] : m_rooms) { + if (coord.x >= minX && coord.x < maxX && coord.y >= minY && coord.y < maxY) { + result.insertAll(rooms); + } + } + } else { + for (const auto &child : m_children) { + if (child) { + result.insertAll(child->findInBounds(minX, minY, maxX, maxY)); + } + } + } + + return result; +} + +size_t QuadtreeNode::countRooms() const +{ + if (m_isLeaf) { + size_t count = 0; + for (const auto &[coord, rooms] : m_rooms) { + count += rooms.size(); + } + return count; + } + + size_t count = 0; + for (const auto &child : m_children) { + if (child) { + count += child->countRooms(); + } + } + return count; +} + +// ============================================================================ +// Plane implementation +// ============================================================================ + +Plane::Plane(const int z) + : m_z{z} +{} + +Plane::~Plane() = default; + +Plane::Plane(const Plane &other) + : m_z{other.m_z} +{ + if (other.m_root) { + m_root = std::make_unique(*other.m_root); + } +} + +void Plane::ensureContains(const int x, const int y) +{ + if (!m_root) { + // Create initial root centered around the first point + const int halfSize = QuadtreeConfig::INITIAL_HALF_SIZE; + m_root = std::make_unique(x - halfSize, y - halfSize, x + halfSize, y + halfSize); + return; + } + + // Expand root if point is outside bounds + // We must always double in both dimensions so old root fits exactly into one quadrant + while (!m_root->contains(x, y)) { + const int oldMinX = m_root->getMinX(); + const int oldMinY = m_root->getMinY(); + const int oldMaxX = m_root->getMaxX(); + const int oldMaxY = m_root->getMaxY(); + const int width = oldMaxX - oldMinX; + const int height = oldMaxY - oldMinY; + + // Determine expansion direction based on where the point is + // The old root will occupy one quadrant of the new root + const bool expandWest = (x < oldMinX); + const bool expandEast = (x >= oldMaxX); + const bool expandSouth = (y < oldMinY); + const bool expandNorth = (y >= oldMaxY); + + int newMinX, newMinY, newMaxX, newMaxY; + QuadtreeNode::Quadrant oldRootQuadrant; + + // Always double the size in both dimensions + // Choose direction based on where the point is, defaulting to NE expansion + if (expandWest || (!expandEast && !expandWest)) { + // Expand west (or default): old root goes to east side + newMinX = oldMinX - width; + newMaxX = oldMaxX; + } else { + // Expand east: old root goes to west side + newMinX = oldMinX; + newMaxX = oldMaxX + width; + } + + if (expandSouth || (!expandNorth && !expandSouth)) { + // Expand south (or default): old root goes to north side + newMinY = oldMinY - height; + newMaxY = oldMaxY; + } else { + // Expand north: old root goes to south side + newMinY = oldMinY; + newMaxY = oldMaxY + height; + } + + // Determine which quadrant the old root occupies in the new root + // Old root is at [oldMinX, oldMaxX) x [oldMinY, oldMaxY) + // New center is at (newMinX + width, newMinY + height) + const int newCenterX = newMinX + width; + const int newCenterY = newMinY + height; + + // Old root's center determines its quadrant + const int oldCenterX = oldMinX + width / 2; + const int oldCenterY = oldMinY + height / 2; + + if (oldCenterX < newCenterX) { + oldRootQuadrant = (oldCenterY >= newCenterY) ? QuadtreeNode::Quadrant::NorthWest + : QuadtreeNode::Quadrant::SouthWest; + } else { + oldRootQuadrant = (oldCenterY >= newCenterY) ? QuadtreeNode::Quadrant::NorthEast + : QuadtreeNode::Quadrant::SouthEast; + } + + // Create new root with doubled size + auto newRoot = std::make_unique(newMinX, newMinY, newMaxX, newMaxY); + + // The old root becomes one of the children - it should fit exactly + newRoot->m_children[static_cast(oldRootQuadrant)] = std::move(m_root); + newRoot->m_isLeaf = false; + + m_root = std::move(newRoot); + } +} + +void Plane::insert(const RoomId id, const int x, const int y) +{ + ensureContains(x, y); + m_root->insert(id, x, y); +} + +void Plane::remove(const RoomId id, const int x, const int y) +{ + if (m_root) { + m_root->remove(id, x, y); + } +} + +TinyRoomIdSet Plane::findAt(const int x, const int y) const +{ + if (m_root && m_root->contains(x, y)) { + return m_root->findAt(x, y); + } + return {}; +} + +TinyRoomIdSet Plane::findInBounds(const int minX, const int minY, const int maxX, const int maxY) const +{ + if (m_root) { + return m_root->findInBounds(minX, minY, maxX, maxY); + } + return {}; +} + +size_t Plane::countRooms() const +{ + return m_root ? m_root->countRooms() : 0; +} + +} // namespace spatial + +// ============================================================================ +// SpatialIndex implementation +// ============================================================================ + +SpatialIndex::SpatialIndex() = default; +SpatialIndex::~SpatialIndex() = default; + +SpatialIndex::SpatialIndex(const SpatialIndex &other) + : m_bounds{other.m_bounds} + , m_needsBoundsUpdate{other.m_needsBoundsUpdate} +{ + for (const auto &[z, plane] : other.m_planes) { + m_planes.emplace(z, std::make_unique(*plane)); + } +} + +SpatialIndex &SpatialIndex::operator=(const SpatialIndex &other) +{ + if (this != &other) { + m_planes.clear(); + for (const auto &[z, plane] : other.m_planes) { + m_planes.emplace(z, std::make_unique(*plane)); + } + m_bounds = other.m_bounds; + m_needsBoundsUpdate = other.m_needsBoundsUpdate; + } + return *this; +} + +spatial::Plane &SpatialIndex::getOrCreatePlane(const int z) +{ + auto it = m_planes.find(z); + if (it == m_planes.end()) { + auto [newIt, inserted] = m_planes.emplace(z, std::make_unique(z)); + return *newIt->second; + } + return *it->second; +} + +const spatial::Plane *SpatialIndex::findPlane(const int z) const +{ + auto it = m_planes.find(z); + return (it != m_planes.end()) ? it->second.get() : nullptr; +} + +void SpatialIndex::invalidateBounds() +{ + m_needsBoundsUpdate = true; +} + +void SpatialIndex::insert(const RoomId id, const Coordinate &coord) +{ + auto &plane = getOrCreatePlane(coord.z); + plane.insert(id, coord.x, coord.y); + invalidateBounds(); +} + +void SpatialIndex::remove(const RoomId id, const Coordinate &coord) +{ + if (auto *plane = findPlane(coord.z)) { + // Note: We use const_cast here because we need to modify the plane + // but findPlane returns const for const correctness in queries + const_cast(plane)->remove(id, coord.x, coord.y); + invalidateBounds(); + } +} + +void SpatialIndex::move(const RoomId id, const Coordinate &from, const Coordinate &to) +{ + if (from == to) { + return; + } + remove(id, from); + insert(id, to); +} + +TinyRoomIdSet SpatialIndex::findAt(const Coordinate &coord) const +{ + if (const auto *plane = findPlane(coord.z)) { + return plane->findAt(coord.x, coord.y); + } + return {}; +} + +std::optional SpatialIndex::findFirst(const Coordinate &coord) const +{ + const auto rooms = findAt(coord); + if (!rooms.empty()) { + return rooms.first(); + } + return std::nullopt; +} + +bool SpatialIndex::hasRoomAt(const Coordinate &coord) const +{ + return !findAt(coord).empty(); +} + +TinyRoomIdSet SpatialIndex::findInBounds(const Bounds &bounds) const +{ + TinyRoomIdSet result; + + for (const auto &[z, plane] : m_planes) { + if (z >= bounds.min.z && z <= bounds.max.z) { + result.insertAll(plane->findInBounds(bounds.min.x, bounds.min.y, bounds.max.x + 1, bounds.max.y + 1)); + } + } + + return result; +} + +TinyRoomIdSet SpatialIndex::findInRadius(const Coordinate ¢er, const int radius) const +{ + // For now, use bounding box approximation + // Future: could implement proper circular distance check + const Bounds bounds{ + Coordinate{center.x - radius, center.y - radius, center.z - radius}, + Coordinate{center.x + radius, center.y + radius, center.z + radius}}; + return findInBounds(bounds); +} + +size_t SpatialIndex::size() const +{ + size_t total = 0; + for (const auto &[z, plane] : m_planes) { + total += plane->countRooms(); + } + return total; +} + +bool SpatialIndex::empty() const +{ + for (const auto &[z, plane] : m_planes) { + if (plane->countRooms() > 0) { + return false; + } + } + return true; +} + +std::optional SpatialIndex::getBounds() const +{ + if (!m_needsBoundsUpdate && m_bounds.has_value()) { + return m_bounds; + } + + // Compute bounds by iterating all rooms + std::optional bounds; + forEach([&bounds](const RoomId /*id*/, const Coordinate &coord) { + if (!bounds.has_value()) { + bounds.emplace(coord, coord); + } else { + bounds->insert(coord); + } + }); + + m_bounds = bounds; + m_needsBoundsUpdate = false; + return m_bounds; +} + +void SpatialIndex::updateBounds(ProgressCounter &pc) +{ + m_bounds.reset(); + m_needsBoundsUpdate = false; + + if (empty()) { + return; + } + + const size_t total = size(); + pc.increaseTotalStepsBy(total); + + forEach([this, &pc](const RoomId /*id*/, const Coordinate &coord) { + if (!m_bounds.has_value()) { + m_bounds.emplace(coord, coord); + } else { + m_bounds->insert(coord); + } + pc.step(); + }); +} + +void SpatialIndex::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const +{ + const auto bounds = getBounds(); + if (bounds.has_value()) { + const Coordinate &max = bounds->max; + const Coordinate &min = bounds->min; + + static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); + + auto show = [&os](std::string_view prefix, int lo, int hi) { + os << prefix << ColoredValue(green, hi - lo + 1) << " (" << ColoredValue(green, lo) + << " to " << ColoredValue(green, hi) << ").\n"; + }; + + os << "\n"; + show("Width (West to East): ", min.x, max.x); + show("Height (South to North): ", min.y, max.y); + show("Layers (Down to Up): ", min.z, max.z); + + os << "\nSpatial Index Statistics:\n"; + os << " Total rooms: " << ColoredValue(green, size()) << "\n"; + os << " Z-planes: " << ColoredValue(green, m_planes.size()) << "\n"; + } +} + +bool SpatialIndex::operator==(const SpatialIndex &other) const +{ + if (size() != other.size()) { + return false; + } + + // Compare all rooms + bool equal = true; + forEach([&other, &equal](const RoomId id, const Coordinate &coord) { + if (equal) { + const auto otherRooms = other.findAt(coord); + if (!otherRooms.contains(id)) { + equal = false; + } + } + }); + + return equal; +} diff --git a/src/map/SpatialIndex.h b/src/map/SpatialIndex.h new file mode 100644 index 000000000..dd2772b2c --- /dev/null +++ b/src/map/SpatialIndex.h @@ -0,0 +1,261 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2025 The MMapper Authors + +#include "../global/macros.h" +#include "TinyRoomIdSet.h" +#include "coordinate.h" +#include "roomid.h" + +#include +#include +#include +#include +#include +#include + +class AnsiOstream; +class ProgressCounter; + +namespace spatial { + +/// Configuration constants for the quadtree +struct NODISCARD QuadtreeConfig final +{ + /// Maximum rooms in a leaf node before subdivision + static constexpr int MAX_LEAF_ROOMS = 32; + /// Minimum square size (won't subdivide smaller than this) + static constexpr int MIN_SQUARE_SIZE = 4; + /// Initial square half-size when creating a new plane + static constexpr int INITIAL_HALF_SIZE = 64; +}; + +/// A node in the quadtree, representing a square region of the XY plane +class NODISCARD QuadtreeNode final +{ + friend class Plane; // Plane needs access to expand the tree + +public: + enum class NODISCARD Quadrant : uint8_t + { + NorthWest = 0, // x < center, y >= center + NorthEast = 1, // x >= center, y >= center + SouthWest = 2, // x < center, y < center + SouthEast = 3 // x >= center, y < center + }; + static constexpr size_t NUM_QUADRANTS = 4; + +private: + /// Bounds of this node + int m_minX = 0; + int m_minY = 0; + int m_maxX = 0; + int m_maxY = 0; + + /// Child nodes (nullptr if leaf) + std::array, NUM_QUADRANTS> m_children; + + /// Rooms stored in this leaf node (empty if internal node) + /// Maps coordinate (x,y only, z is handled by Plane) to room set + std::unordered_map m_rooms; + + /// Whether this is a leaf node + bool m_isLeaf = true; + +public: + QuadtreeNode(int minX, int minY, int maxX, int maxY); + ~QuadtreeNode(); + + QuadtreeNode(const QuadtreeNode &other); + QuadtreeNode &operator=(const QuadtreeNode &) = delete; + QuadtreeNode(QuadtreeNode &&) = default; + QuadtreeNode &operator=(QuadtreeNode &&) = default; + +public: + NODISCARD int getMinX() const { return m_minX; } + NODISCARD int getMinY() const { return m_minY; } + NODISCARD int getMaxX() const { return m_maxX; } + NODISCARD int getMaxY() const { return m_maxY; } + NODISCARD int getCenterX() const { return m_minX + (m_maxX - m_minX) / 2; } + NODISCARD int getCenterY() const { return m_minY + (m_maxY - m_minY) / 2; } + NODISCARD int getWidth() const { return m_maxX - m_minX; } + NODISCARD int getHeight() const { return m_maxY - m_minY; } + NODISCARD bool isLeaf() const { return m_isLeaf; } + +public: + NODISCARD bool contains(int x, int y) const; + NODISCARD Quadrant getQuadrant(int x, int y) const; + +public: + void insert(RoomId id, int x, int y); + void remove(RoomId id, int x, int y); + NODISCARD TinyRoomIdSet findAt(int x, int y) const; + NODISCARD TinyRoomIdSet findInBounds(int minX, int minY, int maxX, int maxY) const; + + template + void forEach(Callback &&callback) const; + + NODISCARD size_t countRooms() const; + +private: + void subdivide(); + NODISCARD bool shouldSubdivide() const; + void insertIntoChild(RoomId id, int x, int y); +}; + +/// A plane represents all rooms at a single Z level +class NODISCARD Plane final +{ +private: + int m_z = 0; + std::unique_ptr m_root; + +public: + explicit Plane(int z); + ~Plane(); + + Plane(const Plane &other); + Plane &operator=(const Plane &) = delete; + Plane(Plane &&) = default; + Plane &operator=(Plane &&) = default; + +public: + NODISCARD int getZ() const { return m_z; } + +public: + void insert(RoomId id, int x, int y); + void remove(RoomId id, int x, int y); + NODISCARD TinyRoomIdSet findAt(int x, int y) const; + NODISCARD TinyRoomIdSet findInBounds(int minX, int minY, int maxX, int maxY) const; + + template + void forEach(Callback &&callback) const; + + NODISCARD size_t countRooms() const; + +private: + void ensureContains(int x, int y); +}; + +} // namespace spatial + +/// Main spatial index class that replaces SpatialDb +/// Supports multiple rooms per coordinate via quadtree organization +class NODISCARD SpatialIndex final +{ +private: + /// Maps Z coordinate to plane + std::unordered_map> m_planes; + + /// Cached bounds (lazily updated) + mutable std::optional m_bounds; + mutable bool m_needsBoundsUpdate = true; + +public: + SpatialIndex(); + ~SpatialIndex(); + + SpatialIndex(const SpatialIndex &other); + SpatialIndex &operator=(const SpatialIndex &other); + SpatialIndex(SpatialIndex &&) = default; + SpatialIndex &operator=(SpatialIndex &&) = default; + +public: + /// Insert a room at the given coordinate + void insert(RoomId id, const Coordinate &coord); + + /// Remove a room from the given coordinate + void remove(RoomId id, const Coordinate &coord); + + /// Move a room from one coordinate to another + void move(RoomId id, const Coordinate &from, const Coordinate &to); + +public: + /// Find all rooms at exact coordinate + NODISCARD TinyRoomIdSet findAt(const Coordinate &coord) const; + + /// Find first room at coordinate (for backward compatibility) + NODISCARD std::optional findFirst(const Coordinate &coord) const; + + /// Check if any room exists at coordinate + NODISCARD bool hasRoomAt(const Coordinate &coord) const; + +public: + /// Find all rooms within a bounding box + NODISCARD TinyRoomIdSet findInBounds(const Bounds &bounds) const; + + /// Find all rooms within radius (for future float coordinate support) + NODISCARD TinyRoomIdSet findInRadius(const Coordinate ¢er, int radius) const; + +public: + /// Iterate over all rooms + template + void forEach(Callback &&callback) const; + + /// Get total number of coordinate entries + NODISCARD size_t size() const; + + /// Check if index is empty + NODISCARD bool empty() const; + +public: + /// Get bounds of all rooms (computed lazily) + NODISCARD std::optional getBounds() const; + + /// Check if bounds need recalculation + NODISCARD bool needsBoundsUpdate() const { return m_needsBoundsUpdate; } + + /// Force bounds recalculation + void updateBounds(ProgressCounter &pc); + +public: + /// Print statistics + void printStats(ProgressCounter &pc, AnsiOstream &os) const; + +public: + NODISCARD bool operator==(const SpatialIndex &other) const; + NODISCARD bool operator!=(const SpatialIndex &other) const { return !operator==(other); } + +private: + spatial::Plane &getOrCreatePlane(int z); + const spatial::Plane *findPlane(int z) const; + void invalidateBounds(); +}; + +// Template implementations + +template +void spatial::QuadtreeNode::forEach(Callback &&callback) const +{ + if (m_isLeaf) { + for (const auto &[coord, rooms] : m_rooms) { + for (const RoomId id : rooms) { + callback(id, coord); + } + } + } else { + for (const auto &child : m_children) { + if (child) { + child->forEach(std::forward(callback)); + } + } + } +} + +template +void spatial::Plane::forEach(Callback &&callback) const +{ + if (m_root) { + m_root->forEach([this, &callback](RoomId id, const Coordinate2i &xy) { + callback(id, Coordinate{xy, m_z}); + }); + } +} + +template +void SpatialIndex::forEach(Callback &&callback) const +{ + for (const auto &[z, plane] : m_planes) { + plane->forEach(std::forward(callback)); + } +} diff --git a/src/map/World.cpp b/src/map/World.cpp index bba1d4444..21bf9eb63 100644 --- a/src/map/World.cpp +++ b/src/map/World.cpp @@ -593,10 +593,13 @@ void World::checkConsistency(ProgressCounter &counter) const auto checkPosition = [this](const RoomId id) { const Coordinate &coord = getPosition(id); - // Is there a unique owner of the coord? - if (const RoomId *const maybe = m_spatialDb.findUnique(coord); - maybe == nullptr || *maybe != id) { - throw MapConsistencyError("two rooms using the same coordinate found"); + // Is this room present in the spatial index at its coordinate? + const auto rooms = m_spatialDb.findRooms(coord); + if (!rooms.contains(id)) { + qWarning() << "checkPosition failed: room" << id.asUint32() << "at coord" + << coord.x << coord.y << coord.z << "not in spatial index. Found" + << rooms.size() << "rooms at that coord."; + throw MapConsistencyError("room not found at its coordinate in spatial index"); } }; diff --git a/src/map/coordinate.h b/src/map/coordinate.h index 6bad26540..5ca861812 100644 --- a/src/map/coordinate.h +++ b/src/map/coordinate.h @@ -51,6 +51,20 @@ struct NODISCARD Coordinate2i final } NODISCARD glm::ivec2 to_ivec2() const { return glm::ivec2{x, y}; } + + NODISCARD bool operator==(const Coordinate2i &rhs) const { return x == rhs.x && y == rhs.y; } + NODISCARD bool operator!=(const Coordinate2i &rhs) const { return !operator==(rhs); } +}; + +template<> +struct std::hash +{ + std::size_t operator()(const Coordinate2i &c) const noexcept + { + const auto hx = numeric_hash(c.x); + const auto hy = numeric_hash(c.y); + return hx ^ utils::rotate_bits64<32>(hy); + } }; struct NODISCARD Coordinate2f final From bee749e0700cb8ea80afb7ddd3af36eb9e1e63ad Mon Sep 17 00:00:00 2001 From: Dmitrijs Date: Sat, 24 Jan 2026 16:02:43 +0100 Subject: [PATCH 2/4] UI, World etc moved to multiple rooms per coordinate. --- src/display/connectionselection.cpp | 16 +++++ src/display/connectionselection.h | 10 +++ src/display/mapcanvas.cpp | 98 ++++++++++++++++++++++++----- src/display/mapcanvas.h | 6 ++ src/map/Map.cpp | 13 ++++ src/map/Map.h | 3 + src/map/SpatialDb.cpp | 9 --- src/map/SpatialDb.h | 10 +-- src/map/World.cpp | 27 +++++--- src/map/World.h | 5 ++ src/mapfrontend/mapfrontend.cpp | 12 +++- src/mapfrontend/mapfrontend.h | 5 +- 12 files changed, 168 insertions(+), 46 deletions(-) diff --git a/src/display/connectionselection.cpp b/src/display/connectionselection.cpp index 4d6e8d530..d1d7fda56 100644 --- a/src/display/connectionselection.cpp +++ b/src/display/connectionselection.cpp @@ -38,6 +38,22 @@ ConnectionSelection::ConnectionSelection(Badge, m_connectionDescriptor[0].direction = computeDirection(sel.pos); } +ConnectionSelection::ConnectionSelection(Badge, + MapFrontend &map, + const RoomHandle &room, + const MouseSel &sel) + : m_map{map} +{ + for (const auto &x : m_connectionDescriptor) { + assert(!x.room.exists()); + } + + if (room.exists()) { + this->receiveRoom(room); + } + m_connectionDescriptor[0].direction = computeDirection(sel.pos); +} + ConnectionSelection::~ConnectionSelection() = default; bool ConnectionSelection::isValid() const diff --git a/src/display/connectionselection.h b/src/display/connectionselection.h index 036ef77bf..5f71aabc1 100644 --- a/src/display/connectionselection.h +++ b/src/display/connectionselection.h @@ -71,12 +71,22 @@ class NODISCARD ConnectionSelection final : public std::enable_shared_from_this< { return std::make_shared(Badge{}, map, sel); } + NODISCARD static std::shared_ptr alloc(MapFrontend &map, + const RoomHandle &room, + const MouseSel &sel) + { + return std::make_shared(Badge{}, map, room, sel); + } NODISCARD static std::shared_ptr alloc(MapFrontend &map) { return std::make_shared(Badge{}, map); } explicit ConnectionSelection(Badge, MapFrontend &map, const MouseSel &sel); + explicit ConnectionSelection(Badge, + MapFrontend &map, + const RoomHandle &room, + const MouseSel &sel); explicit ConnectionSelection(Badge, MapFrontend &map); ~ConnectionSelection(); DELETE_CTORS_AND_ASSIGN_OPS(ConnectionSelection); diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index 936a69bfc..1234239d2 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -11,6 +11,7 @@ #include "../global/progresscounter.h" #include "../global/utils.h" #include "../map/ExitDirection.h" +#include "../map/RoomHandle.h" #include "../map/coordinate.h" #include "../map/exit.h" #include "../map/infomark.h" @@ -408,6 +409,48 @@ std::shared_ptr MapCanvas::getInfomarkSelection(const MouseSe return InfomarkSelection::alloc(m_data, lo, hi); } +RoomHandle MapCanvas::pickRoomAt(const Coordinate &coord) +{ + const auto rooms = m_data.findRoomHandles(coord); + + if (rooms.empty()) { + return RoomHandle{}; + } + + if (rooms.size() == 1) { + return rooms.front(); + } + + // Multiple rooms at this coordinate - show picker menu + QMenu menu(tr("Select Room"), this); + + for (const auto &room : rooms) { + const auto extId = room.getIdExternal(); + const auto name = room.getName().toQString(); + const QString label = QString("#%1: %2").arg(extId.asUint32()).arg(name); + + QAction *action = menu.addAction(label); + action->setData(QVariant::fromValue(room.getId().asUint32())); + } + + menu.addSeparator(); + menu.addAction(tr("Cancel")); + + QAction *selected = menu.exec(QCursor::pos()); + if (selected == nullptr || !selected->data().isValid()) { + return RoomHandle{}; + } + + const uint32_t selectedId = selected->data().toUInt(); + for (const auto &room : rooms) { + if (room.getId().asUint32() == selectedId) { + return room; + } + } + + return RoomHandle{}; +} + void MapCanvas::mousePressEvent(QMouseEvent *const event) { const bool hasLeftButton = (event->buttons() & Qt::LeftButton) != 0u; @@ -503,7 +546,7 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) const auto pos = glm::mix(near, far, t); assert(static_cast(std::lround(pos.z)) == z); const Coordinate c2 = MouseSel{Coordinate2f{pos.x, pos.y}, z}.getCoordinate(); - if (const auto r = m_data.findRoomHandle(c2)) { + for (const auto &r : m_data.findRoomHandles(c2)) { tmpSel.insert(r.getId()); } } @@ -526,7 +569,7 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) // Select rooms if (hasLeftButton && hasSel1()) { if (!hasCtrl) { - const auto pRoom = m_data.findRoomHandle(getSel1().getCoordinate()); + const auto pRoom = pickRoomAt(getSel1().getCoordinate()); if (pRoom.exists() && m_roomSelection != nullptr && m_roomSelection->contains(pRoom.getId())) { m_roomSelectionMove.emplace(RoomSelMove{}); @@ -546,8 +589,10 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) case CanvasMouseModeEnum::CREATE_CONNECTIONS: // Select connection if (hasLeftButton && hasSel1()) { - m_connectionSelection = ConnectionSelection::alloc(m_data, getSel1()); - if (!m_connectionSelection->isFirstValid()) { + const auto room = pickRoomAt(getSel1().getCoordinate()); + if (room.exists()) { + m_connectionSelection = ConnectionSelection::alloc(m_data, room, getSel1()); + } else { m_connectionSelection = nullptr; } emit sig_newConnectionSelection(nullptr); @@ -561,8 +606,13 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) case CanvasMouseModeEnum::SELECT_CONNECTIONS: if (hasLeftButton && hasSel1()) { - m_connectionSelection = ConnectionSelection::alloc(m_data, getSel1()); - if (!m_connectionSelection->isFirstValid()) { + const auto room = pickRoomAt(getSel1().getCoordinate()); + if (room.exists()) { + m_connectionSelection = ConnectionSelection::alloc(m_data, room, getSel1()); + } else { + m_connectionSelection = nullptr; + } + if (!m_connectionSelection || !m_connectionSelection->isFirstValid()) { m_connectionSelection = nullptr; } else { const auto &r1 = m_connectionSelection->getFirst().room; @@ -843,11 +893,20 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) } // Display a room info tooltip if there was no mouse movement if (hasSel1() && hasSel2() && getSel1().to_vec3() == getSel2().to_vec3()) { - if (const auto room = m_data.findRoomHandle(getSel1().getCoordinate())) { + const auto rooms = m_data.findRoomHandles(getSel1().getCoordinate()); + if (!rooms.empty()) { // Tooltip doesn't support ANSI, and there's no way to add formatted text. - auto message = mmqt::previewRoom(room, + QString message; + for (size_t i = 0; i < rooms.size(); ++i) { + if (i > 0) { + message += QString("\n\n--- Room %1 of %2 ---\n\n") + .arg(i + 1) + .arg(rooms.size()); + } + message += mmqt::previewRoom(rooms[i], mmqt::StripAnsiEnum::Yes, mmqt::PreviewStyleEnum::ForDisplay); + } QToolTip::showText(mapToGlobal(event->position().toPoint()), message, @@ -920,11 +979,16 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) m_mouseLeftPressed = false; if (m_connectionSelection == nullptr) { - m_connectionSelection = ConnectionSelection::alloc(m_data, getSel1()); + const auto room = pickRoomAt(getSel1().getCoordinate()); + if (room.exists()) { + m_connectionSelection = ConnectionSelection::alloc(m_data, room, getSel1()); + } + } + if (m_connectionSelection) { + m_connectionSelection->setSecond(getSel2()); } - m_connectionSelection->setSecond(getSel2()); - if (!m_connectionSelection->isValid()) { + if (!m_connectionSelection || !m_connectionSelection->isValid()) { m_connectionSelection = nullptr; } else { const auto first = m_connectionSelection->getFirst(); @@ -971,11 +1035,17 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) m_mouseLeftPressed = false; if (m_connectionSelection == nullptr && hasSel1()) { - m_connectionSelection = ConnectionSelection::alloc(m_data, getSel1()); + const auto room = pickRoomAt(getSel1().getCoordinate()); + if (room.exists()) { + m_connectionSelection = ConnectionSelection::alloc(m_data, room, getSel1()); + } + } + if (m_connectionSelection) { + m_connectionSelection->setSecond(getSel2()); } - m_connectionSelection->setSecond(getSel2()); - if (!m_connectionSelection->isValid() || !m_connectionSelection->isCompleteExisting()) { + if (!m_connectionSelection || !m_connectionSelection->isValid() + || !m_connectionSelection->isCompleteExisting()) { m_connectionSelection = nullptr; } slot_setConnectionSelection(m_connectionSelection); diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 2f10ea35e..c2e993310 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -38,6 +38,7 @@ class ConnectionSelection; class Coordinate; class InfomarkSelection; class MapData; +class RoomHandle; class Mmapper2Group; class PrespammedPath; class QMouseEvent; @@ -218,6 +219,11 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, void updateMultisampling(); NODISCARD std::shared_ptr getInfomarkSelection(const MouseSel &sel); + + /// Show a picker menu if multiple rooms exist at coordinate, returns selected room + /// Returns invalid handle if user cancels or no rooms at coordinate + NODISCARD RoomHandle pickRoomAt(const Coordinate &coord); + NODISCARD static glm::mat4 getViewProj_old(const glm::vec2 &scrollPos, const glm::ivec2 &size, float zoomScale, diff --git a/src/map/Map.cpp b/src/map/Map.cpp index de36f5b62..7b4b23fb8 100644 --- a/src/map/Map.cpp +++ b/src/map/Map.cpp @@ -131,6 +131,19 @@ RoomHandle Map::findRoomHandle(const Coordinate &coord) const return RoomHandle{}; } +std::vector Map::findRoomHandles(const Coordinate &coord) const +{ + std::vector result; + const auto rooms = getWorld().findRooms(coord); + result.reserve(rooms.size()); + for (const RoomId id : rooms) { + if (auto h = findRoomHandle(id)) { + result.push_back(std::move(h)); + } + } + return result; +} + RoomHandle Map::getRoomHandle(const RoomId id) const { if (auto h = findRoomHandle(id)) { diff --git a/src/map/Map.h b/src/map/Map.h index 7ee173d6e..d03bda92f 100644 --- a/src/map/Map.h +++ b/src/map/Map.h @@ -69,7 +69,10 @@ class NODISCARD Map final NODISCARD RoomHandle findRoomHandle(RoomId id) const; NODISCARD RoomHandle findRoomHandle(ExternalRoomId id) const; NODISCARD RoomHandle findRoomHandle(ServerRoomId id) const; + /// Find first room at coordinate (legacy, prefer findRoomHandles for new code) NODISCARD RoomHandle findRoomHandle(const Coordinate &coord) const; + /// Find all rooms at coordinate + NODISCARD std::vector findRoomHandles(const Coordinate &coord) const; public: // Semantics: "getRoomHandle()" functions throw if the handle is invalid; diff --git a/src/map/SpatialDb.cpp b/src/map/SpatialDb.cpp index 90df830e7..21e8b53c7 100644 --- a/src/map/SpatialDb.cpp +++ b/src/map/SpatialDb.cpp @@ -6,15 +6,6 @@ #include "../global/AnsiOstream.h" #include "../global/progresscounter.h" -const RoomId *SpatialDb::findUnique(const Coordinate &key) const -{ - m_cachedFindResult = m_index.findFirst(key); - if (m_cachedFindResult.has_value()) { - return &m_cachedFindResult.value(); - } - return nullptr; -} - TinyRoomIdSet SpatialDb::findRooms(const Coordinate &key) const { return m_index.findAt(key); diff --git a/src/map/SpatialDb.h b/src/map/SpatialDb.h index 6101c5bcd..0f9b1c14a 100644 --- a/src/map/SpatialDb.h +++ b/src/map/SpatialDb.h @@ -25,11 +25,7 @@ struct NODISCARD SpatialDb final NODISCARD std::optional getBounds() const { return m_index.getBounds(); } public: - /// Find first room at coordinate (legacy interface, returns nullptr if none) - /// @deprecated Use findRooms() for new code - NODISCARD const RoomId *findUnique(const Coordinate &key) const; - - /// Find all rooms at coordinate (new interface) + /// Find all rooms at coordinate NODISCARD TinyRoomIdSet findRooms(const Coordinate &key) const; /// Find first room at coordinate (new interface, cleaner than findUnique) @@ -68,8 +64,4 @@ struct NODISCARD SpatialDb final public: NODISCARD bool operator==(const SpatialDb &rhs) const { return m_index == rhs.m_index; } NODISCARD bool operator!=(const SpatialDb &rhs) const { return !(rhs == *this); } - -private: - /// Cached result for findUnique (to return pointer) - mutable std::optional m_cachedFindResult; }; diff --git a/src/map/World.cpp b/src/map/World.cpp index 21bf9eb63..29a343920 100644 --- a/src/map/World.cpp +++ b/src/map/World.cpp @@ -216,12 +216,19 @@ void World::requireValidRoom(const RoomId id) const } } +bool World::hasRoomAt(const Coordinate &coord) const +{ + return m_spatialDb.hasRoomAt(coord); +} + +TinyRoomIdSet World::findRooms(const Coordinate &coord) const +{ + return m_spatialDb.findRooms(coord); +} + std::optional World::findRoom(const Coordinate &coord) const { - if (const RoomId *const id = m_spatialDb.findUnique(coord)) { - return *id; - } - return std::nullopt; + return m_spatialDb.findFirst(coord); } ServerRoomId World::getServerId(const RoomId id) const @@ -1328,7 +1335,7 @@ const ImmUnorderedRoomIdSet *World::findAreaRoomSet(const RoomArea &area) const RoomId World::addRoom(const Coordinate &position) { - if (findRoom(position)) { + if (hasRoomAt(position)) { throw InvalidMapOperation("Position in use"); } @@ -1352,7 +1359,7 @@ RoomId World::addRoom(const Coordinate &position) initRoom(r); assert(hasRoom(id)); - assert(findRoom(position) == id); + assert(findRooms(position).contains(id)); const auto ext = convertToExternal(id); MMLOG() << "Added new room " << ext.value() << "."; return id; @@ -1364,7 +1371,7 @@ void World::undeleteRoom(const ExternalRoomId extid, const RawRoom &raw) throw InvalidMapOperation("Invalid room id"); } - if (findRoom(raw.position)) { + if (hasRoomAt(raw.position)) { throw InvalidMapOperation("Position in use"); } @@ -1401,7 +1408,7 @@ void World::undeleteRoom(const ExternalRoomId extid, const RawRoom &raw) initRoom(raw); assert(hasRoom(raw.id)); - assert(findRoom(raw.position) == raw.id); + assert(findRooms(raw.position).contains(raw.id)); const auto ext = convertToExternal(raw.id); if (ext != extid) { throw std::runtime_error("failed sanity check"); @@ -1413,7 +1420,7 @@ void World::addRoom2(const Coordinate &desiredPosition, const ParseEvent &event) { const auto position = std::invoke([this, desiredPosition]() -> Coordinate { return ::getNearestFree(desiredPosition, [this](const Coordinate &check) -> FindCoordEnum { - return findRoom(check).has_value() ? FindCoordEnum::InUse : FindCoordEnum::Available; + return hasRoomAt(check) ? FindCoordEnum::InUse : FindCoordEnum::Available; }); }); @@ -1899,7 +1906,7 @@ void World::apply(ProgressCounter & /*pc*/, const room_change_types::TryMoveClos } auto check = [this, z = desired.z](const Coordinate &suggested) -> FindCoordEnum { - if (suggested.z == z && !findRoom(suggested)) { + if (suggested.z == z && !hasRoomAt(suggested)) { return FindCoordEnum::Available; } else { return FindCoordEnum::InUse; diff --git a/src/map/World.h b/src/map/World.h index e35d178be..7b1f371c2 100644 --- a/src/map/World.h +++ b/src/map/World.h @@ -100,6 +100,11 @@ class NODISCARD World final void requireValidRoom(RoomId id) const; public: + /// Check if any room exists at coordinate + NODISCARD bool hasRoomAt(const Coordinate &coord) const; + /// Find all rooms at coordinate + NODISCARD TinyRoomIdSet findRooms(const Coordinate &coord) const; + /// Find first room at coordinate (legacy, prefer findRooms for new code) NODISCARD std::optional findRoom(const Coordinate &coord) const; NODISCARD const Coordinate &getPosition(RoomId id) const; diff --git a/src/mapfrontend/mapfrontend.cpp b/src/mapfrontend/mapfrontend.cpp index 46eb18121..720bc87ac 100644 --- a/src/mapfrontend/mapfrontend.cpp +++ b/src/mapfrontend/mapfrontend.cpp @@ -153,6 +153,11 @@ RoomHandle MapFrontend::findRoomHandle(const ServerRoomId id) const return getCurrentMap().findRoomHandle(id); } +std::vector MapFrontend::findRoomHandles(const Coordinate &coord) const +{ + return getCurrentMap().findRoomHandles(coord); +} + RoomHandle MapFrontend::getRoomHandle(const RoomId id) const { return getCurrentMap().getRoomHandle(id); @@ -165,10 +170,11 @@ const RawRoom &MapFrontend::getRawRoom(const RoomId id) const RoomIdSet MapFrontend::findAllRooms(const Coordinate &coord) const { - if (const auto room = findRoomHandle(coord)) { - return RoomIdSet{room.getId()}; + RoomIdSet result; + for (const auto &room : getCurrentMap().findRoomHandles(coord)) { + result.insert(room.getId()); } - return RoomIdSet{}; + return result; } RoomIdSet MapFrontend::findAllRooms(const SigParseEvent &event) const diff --git a/src/mapfrontend/mapfrontend.h b/src/mapfrontend/mapfrontend.h index 236b1a3c7..a46b2fd29 100644 --- a/src/mapfrontend/mapfrontend.h +++ b/src/mapfrontend/mapfrontend.h @@ -142,14 +142,17 @@ class NODISCARD_QOBJECT MapFrontend : public QObject, public RoomModificationTra NODISCARD bool tryMakePermanent(RoomId id); NODISCARD RoomHandle findRoomHandle(RoomId) const; + /// Find first room at coordinate (legacy, prefer findRoomHandles for new code) NODISCARD RoomHandle findRoomHandle(const Coordinate &) const; NODISCARD RoomHandle findRoomHandle(ExternalRoomId id) const; NODISCARD RoomHandle findRoomHandle(ServerRoomId id) const; + /// Find all rooms at coordinate + NODISCARD std::vector findRoomHandles(const Coordinate &) const; NODISCARD RoomHandle getRoomHandle(RoomId id) const; NODISCARD const RawRoom &getRawRoom(RoomId id) const; - // Will technically be 0 or 1 + /// Find all room IDs at coordinate (can be multiple if rooms overlap) NODISCARD RoomIdSet findAllRooms(const Coordinate &) const; NODISCARD RoomIdSet findAllRooms(const SigParseEvent &) const; // bounding box From 0c806de7f1f077bc94353ff9641056c22eb4e398 Mon Sep 17 00:00:00 2001 From: Dmitrijs Date: Sat, 24 Jan 2026 18:33:41 +0100 Subject: [PATCH 3/4] Initial effective scaling rendering with room scaling support. No connections, no special tooling. no sub spaces --- src/display/Characters.cpp | 39 ++++-- src/display/Characters.h | 11 +- src/display/Connections.cpp | 200 +++++++++++++++++++--------- src/display/Connections.h | 16 ++- src/display/Infomarks.cpp | 50 +++++-- src/display/MapCanvasData.h | 3 + src/display/MapCanvasRoomDrawer.cpp | 86 +++++++++--- src/display/RoomSelections.cpp | 26 +++- src/display/mapcanvas.cpp | 16 +++ src/display/mapcanvas.h | 5 +- src/display/mapcanvas_gl.cpp | 29 +++- src/map/ChangePrinter.cpp | 19 +++ src/map/ChangePrinter.h | 2 + src/map/ChangeTypes.h | 8 ++ src/map/RawRoom.h | 8 +- src/map/RawRooms.h | 7 + src/map/RoomHandle.cpp | 5 + src/map/RoomHandle.h | 1 + src/map/World.cpp | 13 ++ src/map/World.h | 1 + src/parser/AbstractParser-Room.cpp | 23 ++++ 21 files changed, 442 insertions(+), 126 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index cfe71483d..d34fd3336 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -49,7 +49,18 @@ bool CharacterBatch::isVisible(const Coordinate &c, float margin) const return m_mapScreen.isRoomVisible(c, margin); } -void CharacterBatch::drawCharacter(const Coordinate &c, const Color color, bool fill) +NODISCARD static float sanitizeRoomScale(const float scale) +{ + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + +void CharacterBatch::drawCharacter(const Coordinate &c, + const float roomScale, + const Color color, + const bool fill) { const Configuration::CanvasSettings &settings = getConfig().canvas; @@ -100,7 +111,7 @@ void CharacterBatch::drawCharacter(const Coordinate &c, const Color color, bool } const bool beacon = visible && !differentLayer && wantBeacons; - gl.drawBox(c, fill, beacon, isFar); + gl.drawBox(c, sanitizeRoomScale(roomScale), fill, beacon, isFar); } void CharacterBatch::CharFakeGL::drawPathSegment(const glm::vec3 &p1, @@ -227,10 +238,13 @@ void CharacterBatch::CharFakeGL::drawQuadCommon(const glm::vec2 &in_a, } void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, - bool fill, - bool beacon, + const float roomScale, + const bool fill, + const bool beacon, const bool isFar) { + bool canFill = fill; + bool canBeacon = beacon; const bool dontFillRotatedQuads = true; const bool shrinkRotatedQuads = false; // REVISIT: make this a user option? @@ -240,6 +254,9 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, glPushMatrix(); glTranslatef(coord.to_vec3()); + glTranslatef(glm::vec3{0.5f, 0.5f, 0.f}); + glScalef(roomScale, roomScale, 1.f); + glTranslatef(glm::vec3{-0.5f, -0.5f, 0.f}); if (numAlreadyInRoom != 0) { // NOTE: use of 45/PI here is NOT a botched conversion to radians; @@ -257,9 +274,9 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, glRotateZ(degrees); glTranslatef(-quadCenter); if (dontFillRotatedQuads) { - fill = false; // avoid highlighting the room multiple times + canFill = false; // avoid highlighting the room multiple times } - beacon = false; + canBeacon = false; } // d-c @@ -272,8 +289,8 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, if (isFar) { const auto options = QuadOptsEnum::OUTLINE - | (fill ? QuadOptsEnum::FILL : QuadOptsEnum::NONE) - | (beacon ? QuadOptsEnum::BEACON : QuadOptsEnum::NONE); + | (canFill ? QuadOptsEnum::FILL : QuadOptsEnum::NONE) + | (canBeacon ? QuadOptsEnum::BEACON : QuadOptsEnum::NONE); drawQuadCommon(a, b, c, d, options); } else { /* ignoring fill for now; that'll require a different icon */ @@ -289,7 +306,7 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, addTransformed(c); addTransformed(d); - if (beacon) { + if (canBeacon) { drawQuadCommon(a, b, c, d, QuadOptsEnum::BEACON); } } @@ -414,7 +431,7 @@ void MapCanvas::paintCharacters() // paint char current position const Color color{getConfig().groupManager.color}; - characterBatch.drawCharacter(pos, color); + characterBatch.drawCharacter(pos, room.getScaleFactor(), color); // paint prespam const auto prespam = m_data.getPath(id, m_prespammedPath.getQueue()); @@ -467,7 +484,7 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) const auto color = Color{character.getColor()}; const bool fill = !drawnRoomIds.contains(id); - batch.drawCharacter(pos, color, fill); + batch.drawCharacter(pos, r.getScaleFactor(), color, fill); drawnRoomIds.insert(id); } } diff --git a/src/display/Characters.h b/src/display/Characters.h index b23d70193..a839c2948 100644 --- a/src/display/Characters.h +++ b/src/display/Characters.h @@ -149,7 +149,11 @@ class NODISCARD CharacterBatch final m = glm::translate(m, v); } void drawArrow(bool fill, bool beacon); - void drawBox(const Coordinate &coord, bool fill, bool beacon, bool isFar); + void drawBox(const Coordinate &coord, + float roomScale, + bool fill, + bool beacon, + bool isFar); void addScreenSpaceArrow(const glm::vec3 &pos, float degrees, const Color color, bool fill); void drawPathSegment(const glm::vec3 &p1, const glm::vec3 &p2, const Color color); @@ -207,7 +211,10 @@ class NODISCARD CharacterBatch final NODISCARD bool isVisible(const Coordinate &c, float margin) const; public: - void drawCharacter(const Coordinate &coordinate, const Color color, bool fill = true); + void drawCharacter(const Coordinate &coordinate, + float roomScale, + const Color color, + bool fill = true); void drawPreSpammedPath(const Coordinate &coordinate, const std::vector &path, diff --git a/src/display/Connections.cpp b/src/display/Connections.cpp index 83f8916fc..8de835fa1 100644 --- a/src/display/Connections.cpp +++ b/src/display/Connections.cpp @@ -19,6 +19,7 @@ #include "mapcanvas.h" #include +#include #include #include #include @@ -33,6 +34,7 @@ static constexpr const float CONNECTION_LINE_WIDTH = 0.045f; static constexpr const float VALID_CONNECTION_POINT_SIZE = 6.f; static constexpr const float NEW_CONNECTION_POINT_SIZE = 8.f; +static constexpr const float ROOM_SCALE_MARGIN = 0.4f; static constexpr const float FAINT_CONNECTION_ALPHA = 0.1f; @@ -60,25 +62,35 @@ NODISCARD static bool isConnectionMode(const CanvasMouseModeEnum mode) return false; } -NODISCARD static glm::vec2 getConnectionOffsetRelative(const ExitDirEnum dir) +NODISCARD static float sanitizeRoomScale(const float scale) { + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + +NODISCARD static glm::vec2 getConnectionOffsetRelative(const ExitDirEnum dir, const float scale) +{ + const float roomScale = sanitizeRoomScale(scale); + switch (dir) { // NOTE: These are flipped north/south. case ExitDirEnum::NORTH: - return {0.f, +0.4f}; + return {0.f, +0.4f * roomScale}; case ExitDirEnum::SOUTH: - return {0.f, -0.4f}; + return {0.f, -0.4f * roomScale}; case ExitDirEnum::EAST: - return {0.4f, 0.f}; + return {0.4f * roomScale, 0.f}; case ExitDirEnum::WEST: - return {-0.4f, 0.f}; + return {-0.4f * roomScale, 0.f}; // NOTE: These are flipped north/south. case ExitDirEnum::UP: - return {0.25f, 0.25f}; + return {0.25f * roomScale, 0.25f * roomScale}; case ExitDirEnum::DOWN: - return {-0.25f, -0.25f}; + return {-0.25f * roomScale, -0.25f * roomScale}; case ExitDirEnum::UNKNOWN: return {0.f, 0.f}; @@ -92,14 +104,36 @@ NODISCARD static glm::vec2 getConnectionOffsetRelative(const ExitDirEnum dir) return {}; }; -NODISCARD static glm::vec3 getConnectionOffset(const ExitDirEnum dir) +NODISCARD static glm::vec3 getConnectionOffset(const ExitDirEnum dir, const float scale) { - return glm::vec3{getConnectionOffsetRelative(dir), 0.f} + glm::vec3{0.5f, 0.5f, 0.f}; + return glm::vec3{getConnectionOffsetRelative(dir, scale), 0.f} + glm::vec3{0.5f, 0.5f, 0.f}; +} + +NODISCARD static glm::vec3 getConnectionOffset(const RoomHandle &room, const ExitDirEnum dir) +{ + return getConnectionOffset(dir, room.getScaleFactor()); } NODISCARD static glm::vec3 getPosition(const ConnectionSelection::ConnectionDescriptor &cd) { - return cd.room.getPosition().to_vec3() + getConnectionOffset(cd.direction); + return cd.room.getPosition().to_vec3() + getConnectionOffset(cd.room, cd.direction); +} + +NODISCARD static glm::vec3 scalePointInRoom(const glm::vec3 &pos, + const glm::vec2 &origin, + const float scale) +{ + const glm::vec2 center = origin + glm::vec2{0.5f, 0.5f}; + const glm::vec2 xy = center + (glm::vec2{pos.x, pos.y} - center) * scale; + return glm::vec3{xy, pos.z}; +} + +NODISCARD static bool isWithinRoomBounds(const glm::vec3 &pos, + const glm::vec2 &origin, + const float margin) +{ + return pos.x >= origin.x - margin && pos.x <= origin.x + 1.f + margin + && pos.y >= origin.y - margin && pos.y <= origin.y + 1.f + margin; } NODISCARD static QString getDoorPostFix(const RoomHandle &room, const ExitDirEnum dir) @@ -415,6 +449,8 @@ void ConnectionDrawer::drawConnection(const RoomHandle &leftRoom, { const auto srcZ = static_cast(leftZ); const auto dstZ = static_cast(rightZ); + const float startScale = sanitizeRoomScale(leftRoom.getScaleFactor()); + const float endScale = sanitizeRoomScale(rightRoom.getScaleFactor()); drawConnectionLine(startDir, endDir, @@ -423,14 +459,18 @@ void ConnectionDrawer::drawConnection(const RoomHandle &leftRoom, static_cast(dX), static_cast(dY), srcZ, - dstZ); + dstZ, + startScale, + endScale); drawConnectionTriangles(startDir, endDir, oneway, static_cast(dX), static_cast(dY), srcZ, - dstZ); + dstZ, + startScale, + endScale); } gl.setOffset(0, 0, 0); @@ -443,13 +483,15 @@ void ConnectionDrawer::drawConnectionTriangles(const ExitDirEnum startDir, const float dX, const float dY, const float srcZ, - const float dstZ) + const float dstZ, + const float startScale, + const float endScale) { if (oneway) { - drawConnEndTri1Way(endDir, dX, dY, dstZ); + drawConnEndTri1Way(endDir, dX, dY, dstZ, endScale); } else { - drawConnStartTri(startDir, srcZ); - drawConnEndTri(endDir, dX, dY, dstZ); + drawConnStartTri(startDir, srcZ, startScale); + drawConnEndTri(endDir, dX, dY, dstZ, endScale); } } @@ -460,7 +502,9 @@ void ConnectionDrawer::drawConnectionLine(const ExitDirEnum startDir, const float dX, const float dY, const float srcZ, - const float dstZ) + const float dstZ, + const float startScale, + const float endScale) { std::vector points{}; ConnectionLineBuilder lb{points}; @@ -479,6 +523,16 @@ void ConnectionDrawer::drawConnectionLine(const ExitDirEnum startDir, return; } + const glm::vec2 startOrigin{0.f, 0.f}; + const glm::vec2 endOrigin{dX, dY}; + for (auto &point : points) { + if (isWithinRoomBounds(point, startOrigin, ROOM_SCALE_MARGIN)) { + point = scalePointInRoom(point, startOrigin, startScale); + } else if (isWithinRoomBounds(point, endOrigin, ROOM_SCALE_MARGIN)) { + point = scalePointInRoom(point, endOrigin, endScale); + } + } + drawLineStrip(points); } @@ -487,30 +541,34 @@ void ConnectionDrawer::drawLineStrip(const std::vector &points) getFakeGL().drawLineStrip(points); } -void ConnectionDrawer::drawConnStartTri(const ExitDirEnum startDir, const float srcZ) +void ConnectionDrawer::drawConnStartTri(const ExitDirEnum startDir, + const float srcZ, + const float roomScale) { auto &gl = getFakeGL(); + const glm::vec2 origin{0.f, 0.f}; + const float scale = sanitizeRoomScale(roomScale); switch (startDir) { case ExitDirEnum::NORTH: - gl.drawTriangle(glm::vec3{0.82f, 0.9f, srcZ}, - glm::vec3{0.68f, 0.9f, srcZ}, - glm::vec3{0.75f, 0.7f, srcZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{0.82f, 0.9f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.68f, 0.9f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.75f, 0.7f, srcZ}, origin, scale)); break; case ExitDirEnum::SOUTH: - gl.drawTriangle(glm::vec3{0.18f, 0.1f, srcZ}, - glm::vec3{0.32f, 0.1f, srcZ}, - glm::vec3{0.25f, 0.3f, srcZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{0.18f, 0.1f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.32f, 0.1f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.25f, 0.3f, srcZ}, origin, scale)); break; case ExitDirEnum::EAST: - gl.drawTriangle(glm::vec3{0.9f, 0.68f, srcZ}, - glm::vec3{0.9f, 0.82f, srcZ}, - glm::vec3{0.7f, 0.75f, srcZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{0.9f, 0.68f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.9f, 0.82f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.7f, 0.75f, srcZ}, origin, scale)); break; case ExitDirEnum::WEST: - gl.drawTriangle(glm::vec3{0.1f, 0.32f, srcZ}, - glm::vec3{0.1f, 0.18f, srcZ}, - glm::vec3{0.3f, 0.25f, srcZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{0.1f, 0.32f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.1f, 0.18f, srcZ}, origin, scale), + scalePointInRoom(glm::vec3{0.3f, 0.25f, srcZ}, origin, scale)); break; case ExitDirEnum::UP: @@ -518,7 +576,7 @@ void ConnectionDrawer::drawConnStartTri(const ExitDirEnum startDir, const float // Do not draw triangles for 2-way up/down break; case ExitDirEnum::UNKNOWN: - drawConnEndTriUpDownUnknown(0, 0, srcZ); + drawConnEndTriUpDownUnknown(0, 0, srcZ, scale); break; case ExitDirEnum::NONE: assert(false); @@ -529,30 +587,33 @@ void ConnectionDrawer::drawConnStartTri(const ExitDirEnum startDir, const float void ConnectionDrawer::drawConnEndTri(const ExitDirEnum endDir, const float dX, const float dY, - const float dstZ) + const float dstZ, + const float roomScale) { auto &gl = getFakeGL(); + const glm::vec2 origin{dX, dY}; + const float scale = sanitizeRoomScale(roomScale); switch (endDir) { case ExitDirEnum::NORTH: - gl.drawTriangle(glm::vec3{dX + 0.82f, dY + 0.9f, dstZ}, - glm::vec3{dX + 0.68f, dY + 0.9f, dstZ}, - glm::vec3{dX + 0.75f, dY + 0.7f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.82f, dY + 0.9f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.68f, dY + 0.9f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.75f, dY + 0.7f, dstZ}, origin, scale)); break; case ExitDirEnum::SOUTH: - gl.drawTriangle(glm::vec3{dX + 0.18f, dY + 0.1f, dstZ}, - glm::vec3{dX + 0.32f, dY + 0.1f, dstZ}, - glm::vec3{dX + 0.25f, dY + 0.3f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.18f, dY + 0.1f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.32f, dY + 0.1f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.25f, dY + 0.3f, dstZ}, origin, scale)); break; case ExitDirEnum::EAST: - gl.drawTriangle(glm::vec3{dX + 0.9f, dY + 0.68f, dstZ}, - glm::vec3{dX + 0.9f, dY + 0.82f, dstZ}, - glm::vec3{dX + 0.7f, dY + 0.75f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.9f, dY + 0.68f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.9f, dY + 0.82f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.7f, dY + 0.75f, dstZ}, origin, scale)); break; case ExitDirEnum::WEST: - gl.drawTriangle(glm::vec3{dX + 0.1f, dY + 0.32f, dstZ}, - glm::vec3{dX + 0.1f, dY + 0.18f, dstZ}, - glm::vec3{dX + 0.3f, dY + 0.25f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.1f, dY + 0.32f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.1f, dY + 0.18f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.3f, dY + 0.25f, dstZ}, origin, scale)); break; case ExitDirEnum::UP: @@ -561,7 +622,7 @@ void ConnectionDrawer::drawConnEndTri(const ExitDirEnum endDir, break; case ExitDirEnum::UNKNOWN: // NOTE: This is drawn for both 1-way and 2-way - drawConnEndTriUpDownUnknown(dX, dY, dstZ); + drawConnEndTriUpDownUnknown(dX, dY, dstZ, scale); break; case ExitDirEnum::NONE: assert(false); @@ -572,37 +633,40 @@ void ConnectionDrawer::drawConnEndTri(const ExitDirEnum endDir, void ConnectionDrawer::drawConnEndTri1Way(const ExitDirEnum endDir, const float dX, const float dY, - const float dstZ) + const float dstZ, + const float roomScale) { auto &gl = getFakeGL(); + const glm::vec2 origin{dX, dY}; + const float scale = sanitizeRoomScale(roomScale); switch (endDir) { case ExitDirEnum::NORTH: - gl.drawTriangle(glm::vec3{dX + 0.32f, dY + 0.9f, dstZ}, - glm::vec3{dX + 0.18f, dY + 0.9f, dstZ}, - glm::vec3{dX + 0.25f, dY + 0.7f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.32f, dY + 0.9f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.18f, dY + 0.9f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.25f, dY + 0.7f, dstZ}, origin, scale)); break; case ExitDirEnum::SOUTH: - gl.drawTriangle(glm::vec3{dX + 0.68f, dY + 0.1f, dstZ}, - glm::vec3{dX + 0.82f, dY + 0.1f, dstZ}, - glm::vec3{dX + 0.75f, dY + 0.3f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.68f, dY + 0.1f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.82f, dY + 0.1f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.75f, dY + 0.3f, dstZ}, origin, scale)); break; case ExitDirEnum::EAST: - gl.drawTriangle(glm::vec3{dX + 0.9f, dY + 0.18f, dstZ}, - glm::vec3{dX + 0.9f, dY + 0.32f, dstZ}, - glm::vec3{dX + 0.7f, dY + 0.25f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.9f, dY + 0.18f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.9f, dY + 0.32f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.7f, dY + 0.25f, dstZ}, origin, scale)); break; case ExitDirEnum::WEST: - gl.drawTriangle(glm::vec3{dX + 0.1f, dY + 0.82f, dstZ}, - glm::vec3{dX + 0.1f, dY + 0.68f, dstZ}, - glm::vec3{dX + 0.3f, dY + 0.75f, dstZ}); + gl.drawTriangle(scalePointInRoom(glm::vec3{dX + 0.1f, dY + 0.82f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.1f, dY + 0.68f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.3f, dY + 0.75f, dstZ}, origin, scale)); break; case ExitDirEnum::UP: case ExitDirEnum::DOWN: case ExitDirEnum::UNKNOWN: // NOTE: This is drawn for both 1-way and 2-way - drawConnEndTriUpDownUnknown(dX, dY, dstZ); + drawConnEndTriUpDownUnknown(dX, dY, dstZ, scale); break; case ExitDirEnum::NONE: assert(false); @@ -610,11 +674,17 @@ void ConnectionDrawer::drawConnEndTri1Way(const ExitDirEnum endDir, } } -void ConnectionDrawer::drawConnEndTriUpDownUnknown(float dX, float dY, float dstZ) +void ConnectionDrawer::drawConnEndTriUpDownUnknown(float dX, + float dY, + float dstZ, + const float roomScale) { - getFakeGL().drawTriangle(glm::vec3{dX + 0.5f, dY + 0.5f, dstZ}, - glm::vec3{dX + 0.55f, dY + 0.3f, dstZ}, - glm::vec3{dX + 0.7f, dY + 0.45f, dstZ}); + const glm::vec2 origin{dX, dY}; + const float scale = sanitizeRoomScale(roomScale); + getFakeGL().drawTriangle( + scalePointInRoom(glm::vec3{dX + 0.5f, dY + 0.5f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.55f, dY + 0.3f, dstZ}, origin, scale), + scalePointInRoom(glm::vec3{dX + 0.7f, dY + 0.45f, dstZ}, origin, scale)); } ConnectionMeshes ConnectionDrawerBuffers::getMeshes(OpenGL &gl) const @@ -678,7 +748,7 @@ void MapCanvas::paintNearbyConnectionPoints() } } - points.emplace_back(Colors::cyan, roomCoord.to_vec3() + getConnectionOffset(dir)); + points.emplace_back(Colors::cyan, roomCoord.to_vec3() + getConnectionOffset(room, dir)); }; const auto addPoints = [this, isSelection, &addPoint](const std::optional &sel, @@ -714,7 +784,7 @@ void MapCanvas::paintNearbyConnectionPoints() : m_connectionSelection->getSecond(); const Coordinate &c = valid.room.getPosition(); const glm::vec3 &pos = c.to_vec3(); - points.emplace_back(Colors::cyan, pos + getConnectionOffset(valid.direction)); + points.emplace_back(Colors::cyan, pos + getConnectionOffset(valid.room, valid.direction)); addPoints(MouseSel{Coordinate2f{pos.x, pos.y}, c.z}, valid); addPoints(m_sel1, valid); diff --git a/src/display/Connections.h b/src/display/Connections.h index 211b29864..05bce4385 100644 --- a/src/display/Connections.h +++ b/src/display/Connections.h @@ -191,13 +191,13 @@ struct NODISCARD ConnectionDrawer final bool oneway, bool inExitFlags = true); - void drawConnEndTriUpDownUnknown(float dX, float dY, float dstZ); + void drawConnEndTriUpDownUnknown(float dX, float dY, float dstZ, float roomScale); - void drawConnStartTri(ExitDirEnum startDir, float srcZ); + void drawConnStartTri(ExitDirEnum startDir, float srcZ, float roomScale); - void drawConnEndTri(ExitDirEnum endDir, float dX, float dY, float dstZ); + void drawConnEndTri(ExitDirEnum endDir, float dX, float dY, float dstZ, float roomScale); - void drawConnEndTri1Way(ExitDirEnum endDir, float dX, float dY, float dstZ); + void drawConnEndTri1Way(ExitDirEnum endDir, float dX, float dY, float dstZ, float roomScale); void drawConnectionLine(ExitDirEnum startDir, ExitDirEnum endDir, @@ -206,7 +206,9 @@ struct NODISCARD ConnectionDrawer final float dX, float dY, float srcZ, - float dstZ); + float dstZ, + float startScale, + float endScale); void drawConnectionTriangles(ExitDirEnum startDir, ExitDirEnum endDir, @@ -214,7 +216,9 @@ struct NODISCARD ConnectionDrawer final float dX, float dY, float srcZ, - float dstZ); + float dstZ, + float startScale, + float endScale); }; using BatchedConnections = std::unordered_map; diff --git a/src/display/Infomarks.cpp b/src/display/Infomarks.cpp index bcfade72a..67afbee99 100644 --- a/src/display/Infomarks.cpp +++ b/src/display/Infomarks.cpp @@ -20,6 +20,7 @@ #include "mapcanvas.h" #include +#include #include #include #include @@ -93,6 +94,30 @@ NODISCARD static FontFormatFlags getFontFormatFlags(const InfomarkClassEnum info return {}; } +NODISCARD static float sanitizeRoomScale(const float scale) +{ + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + +NODISCARD static glm::vec2 scaleInfomarkPoint(const MapData &data, + const glm::vec2 &pos, + const int layer) +{ + const int roomX = static_cast(std::floor(pos.x)); + const int roomY = static_cast(std::floor(pos.y)); + const Coordinate roomCoord{roomX, roomY, layer}; + if (const auto room = data.findRoomHandle(roomCoord)) { + const float scale = sanitizeRoomScale(room.getScaleFactor()); + const glm::vec2 center{static_cast(roomX) + 0.5f, + static_cast(roomY) + 0.5f}; + return center + (pos - center) * scale; + } + return pos; +} + BatchedInfomarksMeshes MapCanvas::getInfomarksMeshes() { BatchedInfomarksMeshes result; @@ -238,12 +263,14 @@ void MapCanvas::drawInfomark(InfomarksBatch &batch, } const Coordinate &pos2 = marker.getPosition2(); - const float x1 = static_cast(pos1.x) / INFOMARK_SCALE + offset.x; - const float y1 = static_cast(pos1.y) / INFOMARK_SCALE + offset.y; - const float x2 = static_cast(pos2.x) / INFOMARK_SCALE + offset.x; - const float y2 = static_cast(pos2.y) / INFOMARK_SCALE + offset.y; - const float dx = x2 - x1; - const float dy = y2 - y1; + const glm::vec2 pos1_raw{static_cast(pos1.x) / INFOMARK_SCALE + offset.x, + static_cast(pos1.y) / INFOMARK_SCALE + offset.y}; + const glm::vec2 pos2_raw{static_cast(pos2.x) / INFOMARK_SCALE + offset.x, + static_cast(pos2.y) / INFOMARK_SCALE + offset.y}; + const glm::vec2 pos1_scaled = scaleInfomarkPoint(m_data, pos1_raw, layer); + const glm::vec2 pos2_scaled = scaleInfomarkPoint(m_data, pos2_raw, layer); + const float dx = pos2_scaled.x - pos1_scaled.x; + const float dy = pos2_scaled.y - pos1_scaled.y; const auto infoMarkType = marker.getType(); const auto infoMarkClass = marker.getClass(); @@ -252,7 +279,7 @@ void MapCanvas::drawInfomark(InfomarksBatch &batch, const Color infoMarkColor = getInfomarkColor(infoMarkType, infoMarkClass).withAlpha(0.55f); const auto fontFormatFlag = getFontFormatFlags(infoMarkClass); - const glm::vec3 pos{x1, y1, static_cast(layer)}; + const glm::vec3 pos{pos1_scaled, static_cast(layer)}; batch.setOffset(pos); const Color bgColor = (overrideColor) ? overrideColor.value() : infoMarkColor; @@ -335,12 +362,13 @@ void MapCanvas::paintSelectedInfomarks() // draw selection points if (m_canvasMouseMode == CanvasMouseModeEnum::SELECT_INFOMARKS) { - const auto drawPoint = [&batch](const Coordinate &c, const Color color) { + const auto drawPoint = [this, &batch](const Coordinate &c, const Color color) { batch.setColor(color); batch.setOffset(glm::vec3{0}); - const glm::vec3 point{static_cast(c.to_vec3()) - / static_cast(INFOMARK_SCALE), - c.z}; + const glm::vec2 rawPoint + = static_cast(c.to_vec3()) / static_cast(INFOMARK_SCALE); + const glm::vec2 scaledPoint = scaleInfomarkPoint(m_data, rawPoint, c.z); + const glm::vec3 point{scaledPoint, static_cast(c.z)}; batch.drawPoint(point); }; diff --git a/src/display/MapCanvasData.h b/src/display/MapCanvasData.h index 886c625bf..868480616 100644 --- a/src/display/MapCanvasData.h +++ b/src/display/MapCanvasData.h @@ -107,6 +107,8 @@ struct NODISCARD MapCanvasViewport glm::vec2 m_scroll{0.f}; ScaleFactor m_scaleFactor; int m_currentLayer = 0; + float m_effectiveScale = 1.f; // Current effective scale (smoothly interpolated) + float m_targetEffectiveScale = 1.f; // Target effective scale based on current room public: explicit MapCanvasViewport(QWidget &sizeWidget) @@ -122,6 +124,7 @@ struct NODISCARD MapCanvasViewport return Viewport{glm::ivec2{r.x(), r.y()}, glm::ivec2{r.width(), r.height()}}; } NODISCARD float getTotalScaleFactor() const { return m_scaleFactor.getTotal(); } + NODISCARD float getEffectiveScale() const { return m_effectiveScale; } public: NODISCARD std::optional project(const glm::vec3 &) const; diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index b82bb6569..c591cc92a 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -33,6 +33,7 @@ #include "mapcanvas.h" // hack, since we're now definining some of its symbols #include +#include #include #include #include @@ -404,14 +405,36 @@ static void visitRooms(const RoomVector &rooms, } } +NODISCARD static float sanitizeRoomScale(const float scale) +{ + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + +NODISCARD static glm::vec3 scaledRoomVertex(const Coordinate &pos, + const float scale, + const float x, + const float y) +{ + const auto center = pos.to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; + const auto offset = glm::vec3{x - 0.5f, y - 0.5f, 0.f} * scale; + return center + offset; +} + struct NODISCARD RoomTex { Coordinate coord; MMTexArrayPosition pos; + float scale = 1.f; - explicit RoomTex(const Coordinate input_coord, const MMTexArrayPosition &input_pos) + explicit RoomTex(const Coordinate input_coord, + const MMTexArrayPosition &input_pos, + const float input_scale) : coord{input_coord} , pos{input_pos} + , scale{sanitizeRoomScale(input_scale)} { if (input_pos.array == INVALID_MM_TEXTURE_ID) throw std::invalid_argument("input_pos"); @@ -430,8 +453,9 @@ struct NODISCARD ColoredRoomTex : public RoomTex explicit ColoredRoomTex(const Coordinate input_coord, const MMTexArrayPosition input_pos, - const NamedColorEnum input_color) - : RoomTex{input_coord, input_pos} + const NamedColorEnum input_color, + const float input_scale) + : RoomTex{input_coord, input_pos, input_scale} , colorId{input_color} {} }; @@ -564,10 +588,11 @@ NODISCARD static LayerMeshesIntermediate::FnVec createSortedTexturedMeshes( for (size_t i = beg; i < end; ++i) { const RoomTex &thisVert = textures[i]; const auto &pos = thisVert.coord; - const auto v0 = pos.to_vec3(); + const float scale = thisVert.scale; const auto z = thisVert.pos.position; -#define EMIT(x, y) verts.emplace_back(glm::vec3((x), (y), z), v0 + glm::vec3((x), (y), 0)) +#define EMIT(x, y) \ + verts.emplace_back(glm::vec3((x), (y), z), ::scaledRoomVertex(pos, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -628,11 +653,12 @@ NODISCARD static LayerMeshesIntermediate::FnVec createSortedColoredTexturedMeshe for (size_t i = beg; i < end; ++i) { const ColoredRoomTex &thisVert = textures[i]; const auto &pos = thisVert.coord; - const auto v0 = pos.to_vec3(); + const float scale = thisVert.scale; const auto color = XNamedColor{thisVert.colorId}.getColor(); const auto z = thisVert.pos.position; -#define EMIT(x, y) verts.emplace_back(color, glm::vec3((x), (y), z), v0 + glm::vec3((x), (y), 0)) +#define EMIT(x, y) \ + verts.emplace_back(color, glm::vec3((x), (y), z), ::scaledRoomVertex(pos, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -738,10 +764,11 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks } const auto pos = room.getPosition(); - m_data.roomTerrains.emplace_back(pos, terrain); + const float scale = sanitizeRoomScale(room.getScaleFactor()); + m_data.roomTerrains.emplace_back(pos, terrain, scale); - const auto v0 = pos.to_vec3(); -#define EMIT(x, y) m_data.roomLayerBoostQuads.emplace_back(v0 + glm::vec3((x), (y), 0)) +#define EMIT(x, y) \ + m_data.roomLayerBoostQuads.emplace_back(::scaledRoomVertex(pos, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -752,21 +779,26 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks void virt_visitTrailTexture(const RoomHandle &room, const MMTexArrayPosition &trail) final { if (trail.array != INVALID_MM_TEXTURE_ID) { - m_data.roomTrails.emplace_back(room.getPosition(), trail); + m_data.roomTrails.emplace_back(room.getPosition(), + trail, + sanitizeRoomScale(room.getScaleFactor())); } } void virt_visitOverlayTexture(const RoomHandle &room, const MMTexArrayPosition &overlay) final { if (overlay.array != INVALID_MM_TEXTURE_ID) { - m_data.roomOverlays.emplace_back(room.getPosition(), overlay); + m_data.roomOverlays.emplace_back(room.getPosition(), + overlay, + sanitizeRoomScale(room.getScaleFactor())); } } void virt_visitNamedColorTint(const RoomHandle &room, const RoomTintEnum tint) final { - const auto v0 = room.getPosition().to_vec3(); -#define EMIT(x, y) m_data.roomTints[tint].emplace_back(v0 + glm::vec3((x), (y), 0)) + const auto pos = room.getPosition(); + const float scale = sanitizeRoomScale(room.getScaleFactor()); +#define EMIT(x, y) m_data.roomTints[tint].emplace_back(::scaledRoomVertex(pos, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -788,16 +820,25 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks // Note: We could use two door textures (NESW and UD), and then just rotate the // texture coordinates, but doing that would require a different code path. const MMTexArrayPosition &tex = m_textures.door[dir]; - m_data.doors.emplace_back(room.getPosition(), tex, color); + m_data.doors.emplace_back(room.getPosition(), + tex, + color, + sanitizeRoomScale(room.getScaleFactor())); } else { if (isNESW(dir)) { if (wallType == WallTypeEnum::SOLID) { const MMTexArrayPosition &tex = m_textures.wall[dir]; - m_data.solidWallLines.emplace_back(room.getPosition(), tex, color); + m_data.solidWallLines.emplace_back(room.getPosition(), + tex, + color, + sanitizeRoomScale(room.getScaleFactor())); } else { const MMTexArrayPosition &tex = m_textures.dotted_wall[dir]; - m_data.dottedWallLines.emplace_back(room.getPosition(), tex, color); + m_data.dottedWallLines.emplace_back(room.getPosition(), + tex, + color, + sanitizeRoomScale(room.getScaleFactor())); } } else { const bool isUp = dir == ExitDirEnum::UP; @@ -808,7 +849,8 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks : (isUp ? m_textures.exit_up : m_textures.exit_down); - m_data.roomUpDownExits.emplace_back(room.getPosition(), tex, color); + m_data.roomUpDownExits.emplace_back( + room.getPosition(), tex, color, sanitizeRoomScale(room.getScaleFactor())); } } } @@ -819,10 +861,14 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks { switch (type) { case StreamTypeEnum::OutFlow: - m_data.streamOuts.emplace_back(room.getPosition(), m_textures.stream_out[dir]); + m_data.streamOuts.emplace_back(room.getPosition(), + m_textures.stream_out[dir], + sanitizeRoomScale(room.getScaleFactor())); return; case StreamTypeEnum::InFlow: - m_data.streamIns.emplace_back(room.getPosition(), m_textures.stream_in[dir]); + m_data.streamIns.emplace_back(room.getPosition(), + m_textures.stream_in[dir], + sanitizeRoomScale(room.getScaleFactor())); return; default: break; diff --git a/src/display/RoomSelections.cpp b/src/display/RoomSelections.cpp index 5e89f93f8..be98e9058 100644 --- a/src/display/RoomSelections.cpp +++ b/src/display/RoomSelections.cpp @@ -12,6 +12,7 @@ #include "Textures.h" #include "mapcanvas.h" +#include #include #include #include @@ -114,12 +115,21 @@ class NODISCARD RoomSelFakeGL final } }; +NODISCARD static float sanitizeRoomScale(const float scale) +{ + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) { const Coordinate &roomPos = room.getPosition(); - const int x = roomPos.x; - const int y = roomPos.y; - const int z = roomPos.z; + const float x = static_cast(roomPos.x); + const float y = static_cast(roomPos.y); + const float z = static_cast(roomPos.z); + const float roomScale = sanitizeRoomScale(room.getScaleFactor()); // This fake GL uses resetMatrix() before this function. gl.resetMatrix(); @@ -150,7 +160,9 @@ void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) gl.drawColoredQuad(RoomSelFakeGL::SelTypeEnum::Distant); } else { // Room is close - gl.glTranslatef(x, y, z); + gl.glTranslatef(x + 0.5f, y + 0.5f, z); + gl.glScalef(roomScale, roomScale, 1.f); + gl.glTranslatef(-0.5f, -0.5f, 0.f); gl.drawColoredQuad(RoomSelFakeGL::SelTypeEnum::Near); gl.resetMatrix(); } @@ -158,7 +170,11 @@ void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) if (isMoving) { gl.resetMatrix(); const auto &relativeOffset = m_roomSelectionMove->pos; - gl.glTranslatef(x + relativeOffset.x, y + relativeOffset.y, z); + gl.glTranslatef(x + static_cast(relativeOffset.x) + 0.5f, + y + static_cast(relativeOffset.y) + 0.5f, + z); + gl.glScalef(roomScale, roomScale, 1.f); + gl.glTranslatef(-0.5f, -0.5f, 0.f); gl.drawColoredQuad(m_roomSelectionMove->wrongPlace ? RoomSelFakeGL::SelTypeEnum::MoveBad : RoomSelFakeGL::SelTypeEnum::MoveGood); } diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index 1234239d2..b4c99ca5d 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -1122,9 +1122,25 @@ void MapCanvas::onMovement() const Coordinate &pos = m_data.tryGetPosition().value_or(Coordinate{}); m_currentLayer = pos.z; emit sig_onCenter(pos.to_vec2() + glm::vec2{0.5f, 0.5f}); + updateEffectiveScale(); update(); } +void MapCanvas::updateEffectiveScale() +{ + if (const auto &room = m_data.getCurrentRoom()) { + updateEffectiveScaleForRoom(room); + } +} + +void MapCanvas::updateEffectiveScaleForRoom(const RoomHandle &room) +{ + const float roomScale = room.getScaleFactor(); + // When entering a room with scale < 1.0, we need to zoom out (increase effective scale) + // so that the room appears at normal size but surroundings appear larger + m_targetEffectiveScale = (roomScale > 0.f) ? (1.f / roomScale) : 1.f; +} + void MapCanvas::slot_dataLoaded() { onMovement(); diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index c2e993310..5d6a3a980 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -188,6 +188,8 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, private: void onMovement(); + void updateEffectiveScale(); + void updateEffectiveScaleForRoom(const RoomHandle &room); private: void reportGLVersion(); @@ -231,7 +233,8 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, NODISCARD static glm::mat4 getViewProj(const glm::vec2 &scrollPos, const glm::ivec2 &size, float zoomScale, - int currentLayer); + int currentLayer, + float tiltZoomScale); void setMvp(const glm::mat4 &viewProj); void setViewportAndMvp(int width, int height); diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index 460321f03..b4e856346 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -374,7 +374,8 @@ NODISCARD static float getPitchDegrees(const float zoomScale) glm::mat4 MapCanvas::getViewProj(const glm::vec2 &scrollPos, const glm::ivec2 &size, const float zoomScale, - const int currentLayer) + const int currentLayer, + const float tiltZoomScale) { static_assert(GLM_CONFIG_CLIP_CONTROL == GLM_CLIP_CONTROL_RH_NO); @@ -385,7 +386,7 @@ glm::mat4 MapCanvas::getViewProj(const glm::vec2 &scrollPos, const auto &advanced = getConfig().canvas.advanced; const float fovDegrees = advanced.fov.getFloat(); - const auto pitchRadians = glm::radians(getPitchDegrees(zoomScale)); + const auto pitchRadians = glm::radians(getPitchDegrees(tiltZoomScale)); const auto yawRadians = glm::radians(advanced.horizontalAngle.getFloat()); const auto layerHeight = advanced.layerHeight.getFloat(); @@ -472,9 +473,15 @@ void MapCanvas::setViewportAndMvp(int width, int height) assert(size.x == width); assert(size.y == height); + // Apply effective scale: when in a room with scale < 1, we increase effectiveScale + // to make surrounding rooms appear larger (zoomed in view) const float zoomScale = getTotalScaleFactor(); - const auto viewProj = (!want3D) ? getViewProj_old(m_scroll, size, zoomScale, m_currentLayer) - : getViewProj(m_scroll, size, zoomScale, m_currentLayer); + const float effectiveZoom = zoomScale * m_effectiveScale; + + const auto viewProj = + (!want3D) + ? getViewProj_old(m_scroll, size, effectiveZoom, m_currentLayer) + : getViewProj(m_scroll, size, effectiveZoom, m_currentLayer, zoomScale); setMvp(viewProj); } @@ -636,6 +643,20 @@ void MapCanvas::finishPendingMapBatches() void MapCanvas::actuallyPaintGL() { // DECL_TIMER(t, __FUNCTION__); + + // Smoothly interpolate effective scale toward target + { + constexpr float EFFECTIVE_SCALE_LERP_ALPHA = 0.2f; + const float delta = m_targetEffectiveScale - m_effectiveScale; + if (std::abs(delta) > 0.001f) { + m_effectiveScale += delta * EFFECTIVE_SCALE_LERP_ALPHA; + // Continue animating if we're still interpolating + setAnimating(true); + } else { + m_effectiveScale = m_targetEffectiveScale; + } + } + setViewportAndMvp(width(), height()); auto &gl = getOpenGL(); diff --git a/src/map/ChangePrinter.cpp b/src/map/ChangePrinter.cpp index 3aa541894..819babbfd 100644 --- a/src/map/ChangePrinter.cpp +++ b/src/map/ChangePrinter.cpp @@ -151,6 +151,16 @@ void ChangePrinter::print(const bool val) m_os.writeWithColor(const_color, val ? "true" : "false"); } +void ChangePrinter::print(const int value) +{ + m_os.writeWithColor(const_color, value); +} + +void ChangePrinter::print(const float value) +{ + m_os.writeWithColor(const_color, value); +} + void ChangePrinter::print(const Coordinate &coord) { auto &os = m_os; @@ -776,6 +786,15 @@ void ChangePrinter::virt_accept(const SetServerId &change) } } +void ChangePrinter::virt_accept(const SetScaleFactor &change) +{ + BEGIN_STRUCT_HELPER("SetScaleFactor") + { + HELPER_ADD_MEMBER(room); + HELPER_ADD_MEMBER(scale); + } +} + void ChangePrinter::virt_accept(const MakePermanent &change) { BEGIN_STRUCT_HELPER("MakePermanent") diff --git a/src/map/ChangePrinter.h b/src/map/ChangePrinter.h index f06cb0de4..1a2758799 100644 --- a/src/map/ChangePrinter.h +++ b/src/map/ChangePrinter.h @@ -43,8 +43,10 @@ struct NODISCARD ChangePrinter final : public AbstractChangeVisitor void print(ExternalRoomId room); private: + void print(int value); void print(ServerRoomId serverId); void print(const Coordinate &coord); + void print(float value); private: void print(const DoorName &name); diff --git a/src/map/ChangeTypes.h b/src/map/ChangeTypes.h index 784b7fe3f..b0c8d1e9e 100644 --- a/src/map/ChangeTypes.h +++ b/src/map/ChangeTypes.h @@ -106,6 +106,12 @@ struct NODISCARD SetServerId final ServerRoomId server_id = INVALID_SERVER_ROOMID; }; +struct NODISCARD SetScaleFactor final +{ + RoomId room = INVALID_ROOMID; + float scale = 1.0f; +}; + struct NODISCARD MoveRelative final { RoomId room = INVALID_ROOMID; @@ -304,6 +310,8 @@ struct NODISCARD ConnectToNeighborsArgs final SEP() \ X(room_change_types::RemoveRoom) \ SEP() \ + X(room_change_types::SetScaleFactor) \ + SEP() \ X(room_change_types::SetServerId) \ SEP() \ X(room_change_types::TryMoveCloseTo) \ diff --git a/src/map/RawRoom.h b/src/map/RawRoom.h index dd59627b7..97bd0f6f6 100644 --- a/src/map/RawRoom.h +++ b/src/map/RawRoom.h @@ -28,6 +28,7 @@ struct NODISCARD TaggedRawRoom final : public RoomFieldsGettersposition = c; } +public: + NODISCARD float getScaleFactor() const { return scaleFactor; } + void setScaleFactor(const float scale) { this->scaleFactor = scale; } + public: NODISCARD RoomFields &getRoomFields() { return fields; } NODISCARD const RoomFields &getRoomFields() const { return fields; } @@ -55,7 +60,8 @@ struct NODISCARD TaggedRawRoom final : public RoomFieldsGetters NODISCARD ServerRoomId getServerId() const; NODISCARD const Coordinate &getPosition() const; NODISCARD bool isTemporary() const; + NODISCARD float getScaleFactor() const; public: NODISCARD ExitDirFlags computeExitDirections() const; diff --git a/src/map/World.cpp b/src/map/World.cpp index 29a343920..d58f99d97 100644 --- a/src/map/World.cpp +++ b/src/map/World.cpp @@ -848,6 +848,12 @@ void World::setServerId(const RoomId id, const ServerRoomId serverId) m_serverIds.set(serverId, id); } +void World::setScaleFactor(const RoomId id, const float scale) +{ + requireValidRoom(id); + m_rooms.setScaleFactor(id, scale); +} + void World::setPosition(const RoomId id, const Coordinate &coord) { requireValidRoom(id); @@ -1660,6 +1666,12 @@ void World::apply(ProgressCounter & /*pc*/, const room_change_types::SetServerId setServerId(change.room, change.server_id); } +void World::apply(ProgressCounter & /*pc*/, const room_change_types::SetScaleFactor &change) +{ + // + setScaleFactor(change.room, change.scale); +} + void World::apply(ProgressCounter & /*pc*/, const room_change_types::MoveRelative &change) { // @@ -2549,6 +2561,7 @@ NODISCARD bool hasMeshDifference(const RoomFields &a, const RoomFields &b) NODISCARD bool hasMeshDifference(const RawRoom &a, const RawRoom &b) { return a.position != b.position // + || a.scaleFactor != b.scaleFactor // || hasMeshDifference(a.fields, b.fields) // || hasMeshDifference(a.exits, b.exits); // } diff --git a/src/map/World.h b/src/map/World.h index 7b1f371c2..13cbf03c0 100644 --- a/src/map/World.h +++ b/src/map/World.h @@ -238,6 +238,7 @@ class NODISCARD World final void moveRelative(const RoomIdSet &rooms, const Coordinate &offset); void setPosition(RoomId id, const Coordinate &coord); void setServerId(RoomId id, ServerRoomId serverId); + void setScaleFactor(RoomId id, float scale); private: void removeFromWorld(RoomId id, bool removeLinks); diff --git a/src/parser/AbstractParser-Room.cpp b/src/parser/AbstractParser-Room.cpp index 425198ebc..52fa345c5 100644 --- a/src/parser/AbstractParser-Room.cpp +++ b/src/parser/AbstractParser-Room.cpp @@ -776,6 +776,23 @@ class NODISCARD AbstractParser::ParseRoomHelper final } } + void onSetScale(User &user, const Vector &v) const + { + const auto roomId = getRoomId(); + auto &os = user.getOstream(); + + if constexpr (IS_DEBUG_BUILD) { + const auto &name = v[1].getString(); + assert(name == "scale"); + } + + const float scale = v[2].getFloat(); + m_self.applySingleChange(Change{room_change_types::SetScaleFactor{roomId, scale}}); + + os << "Scale factor set to " << scale << ".\n"; + send_ok(os); + } + void onNoexit(User &user, const Vector &v) const { const auto roomId = getRoomId(); @@ -1094,6 +1111,9 @@ class NODISCARD AbstractParser::ParseRoomHelper final auto setServerId = processHiddenParam([this](User &u, const Vector &argv) { onSetServerId(u, argv); }, "set server id"); + auto setScale = processHiddenParam([this](User &u, + const Vector &argv) { onSetScale(u, argv); }, + "set scale factor"); auto noexit = processHiddenParam([this](User &u, const Vector &argv) { onNoexit(u, argv); }, "remove an exit (or -1 for all exits)"); @@ -1128,6 +1148,9 @@ class NODISCARD AbstractParser::ParseRoomHelper final auto setSyntax = buildSyntax(abb("set"), buildSyntax(abb("name"), TokenMatcher::alloc(), setRoomName), + buildSyntax(abb("scale"), + TokenMatcher::alloc_copy(ArgFloat::withMinMax(0.01f, 100.f)), + setScale), buildSyntax(abb("server_id"), TokenMatcher::alloc(), setServerId)); auto makeExitSyn = [this, &makeConn](WaysEnum ways, std::string name, std::string desc) { From 690faa29f683f39f3e6050aee868197f3a27372a Mon Sep 17 00:00:00 2001 From: Dmitrijs Date: Sat, 24 Jan 2026 23:42:38 +0100 Subject: [PATCH 4/4] Experimental implementation of localspaces concept --- src/CMakeLists.txt | 3 + src/display/Characters.cpp | 74 ++++--- src/display/Characters.h | 14 +- src/display/Connections.cpp | 94 ++++++-- src/display/Connections.h | 13 ++ src/display/Infomarks.cpp | 34 +-- src/display/MapCanvasRoomDrawer.cpp | 103 +++++---- src/display/RoomRenderTransform.cpp | 61 ++++++ src/display/RoomRenderTransform.h | 28 +++ src/display/RoomSelections.cpp | 44 ++-- src/display/mapcanvas.cpp | 68 +++++- src/display/mapcanvas.h | 3 +- src/map/ChangePrinter.cpp | 35 +++ src/map/ChangePrinter.h | 2 + src/map/ChangeTypes.h | 31 ++- src/map/World.cpp | 291 ++++++++++++++++++++++++- src/map/World.h | 56 +++++ src/map/localspace.h | 45 ++++ src/parser/AbstractParser-Commands.cpp | 123 +++++++++++ src/parser/AbstractParser-Commands.h | 1 + src/parser/abstractparser.h | 1 + src/syntax/SyntaxArgs.cpp | 4 +- 22 files changed, 975 insertions(+), 153 deletions(-) create mode 100644 src/display/RoomRenderTransform.cpp create mode 100644 src/display/RoomRenderTransform.h create mode 100644 src/map/localspace.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80f72b617..8dde0a5f1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,6 +61,8 @@ set(mmapper_SRCS display/MapCanvasRoomDrawer.h display/RoadIndex.cpp display/RoadIndex.h + display/RoomRenderTransform.cpp + display/RoomRenderTransform.h display/RoomSelections.cpp display/Textures.cpp display/Textures.h @@ -281,6 +283,7 @@ set(mmapper_SRCS map/InvalidMapOperation.h map/Map.cpp map/Map.h + map/localspace.h map/MapConsistencyError.cpp map/MapConsistencyError.h map/ParseTree.cpp diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index d34fd3336..140e68efe 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -6,6 +6,7 @@ #include "../configuration/configuration.h" #include "../group/CGroupChar.h" #include "../group/mmapper2group.h" +#include "../map/Map.h" #include "../map/roomid.h" #include "../mapdata/mapdata.h" #include "../mapdata/roomselection.h" @@ -44,35 +45,27 @@ DistantObjectTransform DistantObjectTransform::construct(const glm::vec3 &pos, return DistantObjectTransform{hint, degrees}; } -bool CharacterBatch::isVisible(const Coordinate &c, float margin) const +bool CharacterBatch::isVisible(const glm::vec3 &pos, const float margin) const { - return m_mapScreen.isRoomVisible(c, margin); + return glm::distance(m_mapScreen.getProxyLocation(pos, margin), pos) < 0.001f; } -NODISCARD static float sanitizeRoomScale(const float scale) -{ - if (!std::isfinite(scale) || scale <= 0.f) { - return 1.f; - } - return scale; -} - -void CharacterBatch::drawCharacter(const Coordinate &c, - const float roomScale, - const Color color, - const bool fill) +void CharacterBatch::drawCharacter(const RoomHandle &room, const Color color, const bool fill) { const Configuration::CanvasSettings &settings = getConfig().canvas; - const glm::vec3 roomCenter = c.to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; - const int layerDifference = c.z - m_currentLayer; + const auto transform = getRoomRenderTransform(room); + const glm::vec3 &roomCenter = transform.roomCenter; + const glm::vec3 &renderCenter = transform.renderCenter; + const Coordinate &coord = room.getPosition(); + const int layerDifference = coord.z - m_currentLayer; auto &gl = getOpenGL(); gl.setColor(color); // REVISIT: The margin probably needs to be modified for high-dpi. const float marginPixels = MapScreen::DEFAULT_MARGIN_PIXELS; - const bool visible = isVisible(c, marginPixels / 2.f); + const bool visible = isVisible(renderCenter, marginPixels / 2.f); const bool isFar = m_scale <= settings.charBeaconScaleCutoff; const bool wantBeacons = settings.drawCharBeacons && isFar; if (!visible) { @@ -80,7 +73,7 @@ void CharacterBatch::drawCharacter(const Coordinate &c, auto opt = utils::getEnvBool("MMAPPER_SCREEN_SPACE_ARROW"); return opt ? opt.value() : true; }); - const auto dot = DistantObjectTransform::construct(roomCenter, m_mapScreen, marginPixels); + const auto dot = DistantObjectTransform::construct(renderCenter, m_mapScreen, marginPixels); // Player is distant if (useScreenSpacePlayerArrow) { gl.addScreenSpaceArrow(dot.offset, dot.rotationDegrees, color, fill); @@ -97,8 +90,9 @@ void CharacterBatch::drawCharacter(const Coordinate &c, const bool differentLayer = layerDifference != 0; if (differentLayer) { - const glm::vec3 centerOnCurrentLayer{static_cast(roomCenter), - static_cast(m_currentLayer)}; + const glm::vec3 centerOnCurrentLayer = applyLocalspaceTransform( + transform, + glm::vec3{roomCenter.x, roomCenter.y, static_cast(m_currentLayer)}); // Draw any arrow on the current layer pointing in either up or down // (this may not make sense graphically in an angled 3D view). gl.glPushMatrix(); @@ -111,7 +105,7 @@ void CharacterBatch::drawCharacter(const Coordinate &c, } const bool beacon = visible && !differentLayer && wantBeacons; - gl.drawBox(c, sanitizeRoomScale(roomScale), fill, beacon, isFar); + gl.drawBox(coord, transform, fill, beacon, isFar); } void CharacterBatch::CharFakeGL::drawPathSegment(const glm::vec3 &p1, @@ -121,7 +115,8 @@ void CharacterBatch::CharFakeGL::drawPathSegment(const glm::vec3 &p1, mmgl::generateLineQuadsSafe(m_pathLineQuads, p1, p2, PATH_LINE_WIDTH, color); } -void CharacterBatch::drawPreSpammedPath(const Coordinate &c1, +void CharacterBatch::drawPreSpammedPath(const Map &map, + const Coordinate &c1, const std::vector &path, const Color color) { @@ -130,13 +125,19 @@ void CharacterBatch::drawPreSpammedPath(const Coordinate &c1, } using TranslatedVerts = std::vector; - const auto verts = std::invoke([&c1, &path]() -> TranslatedVerts { + const auto verts = std::invoke([&c1, &path, &map]() -> TranslatedVerts { TranslatedVerts translated; translated.reserve(path.size() + 1); - const auto add = [&translated](const Coordinate &c) -> void { + const auto add = [&translated, &map](const Coordinate &c) -> void { static const glm::vec3 PATH_OFFSET{0.5f, 0.5f, 0.f}; - translated.push_back(c.to_vec3() + PATH_OFFSET); + const glm::vec3 pos = c.to_vec3() + PATH_OFFSET; + if (const auto room = map.findRoomHandle(c)) { + const auto transform = getRoomRenderTransform(room); + translated.push_back(applyLocalspaceTransform(transform, pos)); + } else { + translated.push_back(pos); + } }; add(c1); @@ -238,7 +239,7 @@ void CharacterBatch::CharFakeGL::drawQuadCommon(const glm::vec2 &in_a, } void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, - const float roomScale, + const RoomRenderTransform &transform, const bool fill, const bool beacon, const bool isFar) @@ -253,9 +254,9 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, glPushMatrix(); - glTranslatef(coord.to_vec3()); - glTranslatef(glm::vec3{0.5f, 0.5f, 0.f}); - glScalef(roomScale, roomScale, 1.f); + glTranslatef(transform.renderCenter); + const float combinedScale = getCombinedRoomScale(transform); + glScalef(combinedScale, combinedScale, 1.f); glTranslatef(glm::vec3{-0.5f, -0.5f, 0.f}); if (numAlreadyInRoom != 0) { @@ -423,19 +424,21 @@ void MapCanvas::paintCharacters() if (const std::optional opt_pos = m_data.getCurrentRoomId()) { const auto &id = opt_pos.value(); if (const auto room = m_data.findRoomHandle(id)) { - const auto &pos = room.getPosition(); // draw the characters before the current position - characterBatch.incrementCount(pos); + characterBatch.incrementCount(room.getPosition()); drawGroupCharacters(characterBatch); - characterBatch.resetCount(pos); + characterBatch.resetCount(room.getPosition()); // paint char current position const Color color{getConfig().groupManager.color}; - characterBatch.drawCharacter(pos, room.getScaleFactor(), color); + characterBatch.drawCharacter(room, color); // paint prespam const auto prespam = m_data.getPath(id, m_prespammedPath.getQueue()); - characterBatch.drawPreSpammedPath(pos, prespam, color); + characterBatch.drawPreSpammedPath(m_data.getCurrentMap(), + room.getPosition(), + prespam, + color); return; } else { // this can happen if the "current room" is deleted @@ -480,11 +483,10 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) } const RoomId id = r.getId(); - const auto &pos = r.getPosition(); const auto color = Color{character.getColor()}; const bool fill = !drawnRoomIds.contains(id); - batch.drawCharacter(pos, r.getScaleFactor(), color, fill); + batch.drawCharacter(r, color, fill); drawnRoomIds.insert(id); } } diff --git a/src/display/Characters.h b/src/display/Characters.h index a839c2948..291e0e4e7 100644 --- a/src/display/Characters.h +++ b/src/display/Characters.h @@ -5,6 +5,7 @@ #include "../global/RuleOf5.h" #include "../global/utils.h" #include "../map/coordinate.h" +#include "RoomRenderTransform.h" #include "../opengl/Font.h" #include "../opengl/OpenGLTypes.h" @@ -21,6 +22,7 @@ #include class MapScreen; +class Map; class OpenGL; struct MapCanvasTextures; @@ -150,7 +152,7 @@ class NODISCARD CharacterBatch final } void drawArrow(bool fill, bool beacon); void drawBox(const Coordinate &coord, - float roomScale, + const RoomRenderTransform &transform, bool fill, bool beacon, bool isFar); @@ -208,15 +210,13 @@ class NODISCARD CharacterBatch final void resetCount(const Coordinate &c) { getOpenGL().clear(c); } - NODISCARD bool isVisible(const Coordinate &c, float margin) const; + NODISCARD bool isVisible(const glm::vec3 &pos, float margin) const; public: - void drawCharacter(const Coordinate &coordinate, - float roomScale, - const Color color, - bool fill = true); + void drawCharacter(const RoomHandle &room, const Color color, bool fill = true); - void drawPreSpammedPath(const Coordinate &coordinate, + void drawPreSpammedPath(const Map &map, + const Coordinate &coordinate, const std::vector &path, const Color color); diff --git a/src/display/Connections.cpp b/src/display/Connections.cpp index 8de835fa1..f6ede1389 100644 --- a/src/display/Connections.cpp +++ b/src/display/Connections.cpp @@ -15,6 +15,7 @@ #include "../opengl/OpenGLTypes.h" #include "ConnectionLineBuilder.h" #include "MapCanvasData.h" +#include "RoomRenderTransform.h" #include "connectionselection.h" #include "mapcanvas.h" @@ -70,6 +71,19 @@ NODISCARD static float sanitizeRoomScale(const float scale) return scale; } +NODISCARD static bool isRoomWithinBounds(const RoomHandle &room, const OptBounds &bounds) +{ + if (!bounds.isRestricted()) { + return true; + } + + const auto &bb = bounds.getBounds(); + const auto transform = getRoomRenderTransform(room); + const auto ¢er = transform.renderCenter; + return center.x >= bb.min.x && center.x <= bb.max.x + 1.f && center.y >= bb.min.y + && center.y <= bb.max.y + 1.f && center.z >= bb.min.z && center.z <= bb.max.z + 1.f; +} + NODISCARD static glm::vec2 getConnectionOffsetRelative(const ExitDirEnum dir, const float scale) { const float roomScale = sanitizeRoomScale(scale); @@ -116,7 +130,10 @@ NODISCARD static glm::vec3 getConnectionOffset(const RoomHandle &room, const Exi NODISCARD static glm::vec3 getPosition(const ConnectionSelection::ConnectionDescriptor &cd) { - return cd.room.getPosition().to_vec3() + getConnectionOffset(cd.room, cd.direction); + const auto transform = getRoomRenderTransform(cd.room); + const glm::vec3 worldPos + = cd.room.getPosition().to_vec3() + getConnectionOffset(cd.room, cd.direction); + return applyLocalspaceTransform(transform, worldPos); } NODISCARD static glm::vec3 scalePointInRoom(const glm::vec3 &pos, @@ -181,6 +198,8 @@ void ConnectionDrawer::drawRoomDoorName(const RoomHandle &sourceRoom, const Coordinate &sourcePos = sourceRoom.getPosition(); const Coordinate &targetPos = targetRoom.getPosition(); + const auto sourceTransform = getRoomRenderTransform(sourceRoom); + const auto targetTransform = getRoomRenderTransform(targetRoom); if (sourcePos.z != m_currentLayer && targetPos.z != m_currentLayer) { return; @@ -255,8 +274,17 @@ void ConnectionDrawer::drawRoomDoorName(const RoomHandle &sourceRoom, }); static const auto bg = Colors::black.withAlpha(0.4f); - const glm::vec3 pos{xy, m_currentLayer}; - m_roomNameBatch.emplace_back(GLText{pos, + const glm::vec3 pos{xy, static_cast(m_currentLayer)}; + const auto &transform = std::invoke([&]() -> const RoomRenderTransform & { + if (sourceTransform.localspaceId == targetTransform.localspaceId) { + return sourceTransform; + } + const float distSource = glm::distance(xy, sourcePos.to_vec2()); + const float distTarget = glm::distance(xy, targetPos.to_vec2()); + return (distSource <= distTarget) ? sourceTransform : targetTransform; + }); + const glm::vec3 renderPos = applyLocalspaceTransform(transform, pos); + m_roomNameBatch.emplace_back(GLText{renderPos, mmqt::toStdStringLatin1(name), // GL font is latin1 Colors::white, bg, @@ -270,7 +298,7 @@ void ConnectionDrawer::drawRoomConnectionsAndDoors(const RoomHandle &room) // Ooops, this is wrong since we may reject a connection that would be visible // if we looked at the other side. const auto room_pos = room.getPosition(); - const bool sourceWithinBounds = m_bounds.contains(room_pos); + const bool sourceWithinBounds = isRoomWithinBounds(room, m_bounds); const auto sourceId = room.getId(); @@ -294,7 +322,7 @@ void ConnectionDrawer::drawRoomConnectionsAndDoors(const RoomHandle &room) continue; } const auto &target_coord = targetRoom.getPosition(); - const bool targetOutsideBounds = !m_bounds.contains(target_coord); + const bool targetOutsideBounds = !isRoomWithinBounds(targetRoom, m_bounds); // Two way means that the target room directly connects back to source room const ExitDirEnum targetDir = opposite(sourceDir); @@ -350,7 +378,7 @@ void ConnectionDrawer::drawRoomConnectionsAndDoors(const RoomHandle &room) // Only draw the connection if the target room is within the bounds const Coordinate &target_coord = targetRoom.getPosition(); - if (!m_bounds.contains(target_coord)) { + if (!isRoomWithinBounds(targetRoom, m_bounds)) { continue; } @@ -438,7 +466,38 @@ void ConnectionDrawer::drawConnection(const RoomHandle &leftRoom, auto &gl = getFakeGL(); - gl.setOffset(static_cast(leftX), static_cast(leftY), 0.f); + const auto leftTransform = getRoomRenderTransform(leftRoom); + const auto rightTransform = getRoomRenderTransform(rightRoom); + const glm::vec3 leftOrigin = leftPos.to_vec3(); + const glm::vec2 startOrigin{0.f, 0.f}; + const glm::vec2 endOrigin{static_cast(dX), static_cast(dY)}; + const bool sameLocalspace = leftTransform.localspaceId == rightTransform.localspaceId; + + gl.setOffset(0.f, 0.f, 0.f); + gl.setPointTransform([leftTransform, + rightTransform, + leftOrigin, + startOrigin, + endOrigin, + sameLocalspace](const glm::vec3 &point) -> glm::vec3 { + const glm::vec3 worldPoint = point + leftOrigin; + if (sameLocalspace) { + return applyLocalspaceTransform(leftTransform, worldPoint); + } + if (isWithinRoomBounds(point, startOrigin, ROOM_SCALE_MARGIN)) { + return applyLocalspaceTransform(leftTransform, worldPoint); + } + if (isWithinRoomBounds(point, endOrigin, ROOM_SCALE_MARGIN)) { + return applyLocalspaceTransform(rightTransform, worldPoint); + } + const glm::vec2 worldXY{worldPoint.x, worldPoint.y}; + const glm::vec2 leftXY{leftTransform.roomCenter.x, leftTransform.roomCenter.y}; + const glm::vec2 rightXY{rightTransform.roomCenter.x, rightTransform.roomCenter.y}; + const float distLeft = glm::distance(worldXY, leftXY); + const float distRight = glm::distance(worldXY, rightXY); + const auto &transform = (distLeft <= distRight) ? leftTransform : rightTransform; + return applyLocalspaceTransform(transform, worldPoint); + }); if (inExitFlags) { gl.setNormal(); @@ -473,6 +532,7 @@ void ConnectionDrawer::drawConnection(const RoomHandle &leftRoom, endScale); } + gl.clearPointTransform(); gl.setOffset(0, 0, 0); gl.setNormal(); } @@ -748,7 +808,9 @@ void MapCanvas::paintNearbyConnectionPoints() } } - points.emplace_back(Colors::cyan, roomCoord.to_vec3() + getConnectionOffset(room, dir)); + const auto transform = getRoomRenderTransform(room); + const glm::vec3 worldPoint = roomCoord.to_vec3() + getConnectionOffset(room, dir); + points.emplace_back(Colors::cyan, applyLocalspaceTransform(transform, worldPoint)); }; const auto addPoints = [this, isSelection, &addPoint](const std::optional &sel, @@ -783,8 +845,10 @@ void MapCanvas::paintNearbyConnectionPoints() const CD valid = m_connectionSelection->isFirstValid() ? m_connectionSelection->getFirst() : m_connectionSelection->getSecond(); const Coordinate &c = valid.room.getPosition(); - const glm::vec3 &pos = c.to_vec3(); - points.emplace_back(Colors::cyan, pos + getConnectionOffset(valid.room, valid.direction)); + const glm::vec3 pos = c.to_vec3(); + const auto transform = getRoomRenderTransform(valid.room); + const glm::vec3 worldPos = pos + getConnectionOffset(valid.room, valid.direction); + points.emplace_back(Colors::cyan, applyLocalspaceTransform(transform, worldPos)); addPoints(MouseSel{Coordinate2f{pos.x, pos.y}, c.z}, valid); addPoints(m_sel1, valid); @@ -858,14 +922,16 @@ void ConnectionDrawer::ConnectionFakeGL::drawTriangle(const glm::vec3 &a, const auto &color = isNormal() ? getCanvasNamedColorOptions().connectionNormalColor : Colors::red; auto &verts = deref(m_currentBuffer).triVerts; - verts.emplace_back(color, a + m_offset); - verts.emplace_back(color, b + m_offset); - verts.emplace_back(color, c + m_offset); + verts.emplace_back(color, applyTransform(a) + m_offset); + verts.emplace_back(color, applyTransform(b) + m_offset); + verts.emplace_back(color, applyTransform(c) + m_offset); } void ConnectionDrawer::ConnectionFakeGL::drawLineStrip(const std::vector &points) { - const auto transform = [this](const glm::vec3 &vert) { return vert + m_offset; }; + const auto transform = [this](const glm::vec3 &vert) { + return applyTransform(vert) + m_offset; + }; const float extension = CONNECTION_LINE_WIDTH * 0.5f; // Helper lambda to generate a quad between two points with a specific color. diff --git a/src/display/Connections.h b/src/display/Connections.h index 05bce4385..2f30b4171 100644 --- a/src/display/Connections.h +++ b/src/display/Connections.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -122,10 +123,14 @@ struct NODISCARD ConnectionDrawer final private: struct NODISCARD ConnectionFakeGL final { + private: + using PointTransform = std::function; + private: ConnectionDrawerBuffers &m_buffers; ConnectionDrawerColorBuffer *m_currentBuffer = nullptr; glm::vec3 m_offset{0.f}; + PointTransform m_pointTransform; public: explicit ConnectionFakeGL(ConnectionDrawerBuffers &buffers) @@ -141,6 +146,14 @@ struct NODISCARD ConnectionDrawer final void setRed() { m_currentBuffer = &m_buffers.red; } void setNormal() { m_currentBuffer = &m_buffers.normal; } NODISCARD bool isNormal() const { return m_currentBuffer == &m_buffers.normal; } + void setPointTransform(PointTransform transform) { m_pointTransform = std::move(transform); } + void clearPointTransform() { m_pointTransform = nullptr; } + + private: + NODISCARD glm::vec3 applyTransform(const glm::vec3 &point) const + { + return m_pointTransform ? m_pointTransform(point) : point; + } public: void drawTriangle(const glm::vec3 &a, const glm::vec3 &b, const glm::vec3 &c); diff --git a/src/display/Infomarks.cpp b/src/display/Infomarks.cpp index 67afbee99..ab258d66e 100644 --- a/src/display/Infomarks.cpp +++ b/src/display/Infomarks.cpp @@ -16,6 +16,7 @@ #include "InfomarkSelection.h" #include "MapCanvasData.h" #include "MapCanvasRoomDrawer.h" +#include "RoomRenderTransform.h" #include "connectionselection.h" #include "mapcanvas.h" @@ -94,15 +95,7 @@ NODISCARD static FontFormatFlags getFontFormatFlags(const InfomarkClassEnum info return {}; } -NODISCARD static float sanitizeRoomScale(const float scale) -{ - if (!std::isfinite(scale) || scale <= 0.f) { - return 1.f; - } - return scale; -} - -NODISCARD static glm::vec2 scaleInfomarkPoint(const MapData &data, +NODISCARD static glm::vec3 scaleInfomarkPoint(const MapData &data, const glm::vec2 &pos, const int layer) { @@ -110,12 +103,11 @@ NODISCARD static glm::vec2 scaleInfomarkPoint(const MapData &data, const int roomY = static_cast(std::floor(pos.y)); const Coordinate roomCoord{roomX, roomY, layer}; if (const auto room = data.findRoomHandle(roomCoord)) { - const float scale = sanitizeRoomScale(room.getScaleFactor()); - const glm::vec2 center{static_cast(roomX) + 0.5f, - static_cast(roomY) + 0.5f}; - return center + (pos - center) * scale; + const auto transform = getRoomRenderTransform(room); + return applyRoomGeometryTransform(transform, + glm::vec3{pos, static_cast(layer)}); } - return pos; + return glm::vec3{pos, static_cast(layer)}; } BatchedInfomarksMeshes MapCanvas::getInfomarksMeshes() @@ -267,8 +259,8 @@ void MapCanvas::drawInfomark(InfomarksBatch &batch, static_cast(pos1.y) / INFOMARK_SCALE + offset.y}; const glm::vec2 pos2_raw{static_cast(pos2.x) / INFOMARK_SCALE + offset.x, static_cast(pos2.y) / INFOMARK_SCALE + offset.y}; - const glm::vec2 pos1_scaled = scaleInfomarkPoint(m_data, pos1_raw, layer); - const glm::vec2 pos2_scaled = scaleInfomarkPoint(m_data, pos2_raw, layer); + const glm::vec3 pos1_scaled = scaleInfomarkPoint(m_data, pos1_raw, layer); + const glm::vec3 pos2_scaled = scaleInfomarkPoint(m_data, pos2_raw, layer); const float dx = pos2_scaled.x - pos1_scaled.x; const float dy = pos2_scaled.y - pos1_scaled.y; @@ -279,8 +271,7 @@ void MapCanvas::drawInfomark(InfomarksBatch &batch, const Color infoMarkColor = getInfomarkColor(infoMarkType, infoMarkClass).withAlpha(0.55f); const auto fontFormatFlag = getFontFormatFlags(infoMarkClass); - const glm::vec3 pos{pos1_scaled, static_cast(layer)}; - batch.setOffset(pos); + batch.setOffset(pos1_scaled); const Color bgColor = (overrideColor) ? overrideColor.value() : infoMarkColor; batch.setColor(bgColor); @@ -289,7 +280,7 @@ void MapCanvas::drawInfomark(InfomarksBatch &batch, case InfomarkTypeEnum::TEXT: { const auto utf8 = marker.getText().getStdStringViewUtf8(); const auto latin1_to_render = charset::conversion::utf8ToLatin1(utf8); // GL font is latin1 - batch.renderText(pos, + batch.renderText(pos1_scaled, latin1_to_render, textColor(bgColor), bgColor, @@ -367,9 +358,8 @@ void MapCanvas::paintSelectedInfomarks() batch.setOffset(glm::vec3{0}); const glm::vec2 rawPoint = static_cast(c.to_vec3()) / static_cast(INFOMARK_SCALE); - const glm::vec2 scaledPoint = scaleInfomarkPoint(m_data, rawPoint, c.z); - const glm::vec3 point{scaledPoint, static_cast(c.z)}; - batch.drawPoint(point); + const glm::vec3 scaledPoint = scaleInfomarkPoint(m_data, rawPoint, c.z); + batch.drawPoint(scaledPoint); }; const auto drawSelectionPoints = [this, &drawPoint](const InfomarkHandle &marker) { diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index c591cc92a..e6eebf301 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -30,6 +30,7 @@ #include "ConnectionLineBuilder.h" #include "MapCanvasData.h" #include "RoadIndex.h" +#include "RoomRenderTransform.h" #include "mapcanvas.h" // hack, since we're now definining some of its symbols #include @@ -405,36 +406,27 @@ static void visitRooms(const RoomVector &rooms, } } -NODISCARD static float sanitizeRoomScale(const float scale) -{ - if (!std::isfinite(scale) || scale <= 0.f) { - return 1.f; - } - return scale; -} - -NODISCARD static glm::vec3 scaledRoomVertex(const Coordinate &pos, +NODISCARD static glm::vec3 scaledRoomVertex(const glm::vec3 ¢er, const float scale, const float x, const float y) { - const auto center = pos.to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; const auto offset = glm::vec3{x - 0.5f, y - 0.5f, 0.f} * scale; return center + offset; } struct NODISCARD RoomTex { - Coordinate coord; + glm::vec3 center{}; MMTexArrayPosition pos; float scale = 1.f; - explicit RoomTex(const Coordinate input_coord, + explicit RoomTex(const glm::vec3 input_center, const MMTexArrayPosition &input_pos, const float input_scale) - : coord{input_coord} + : center{input_center} , pos{input_pos} - , scale{sanitizeRoomScale(input_scale)} + , scale{input_scale} { if (input_pos.array == INVALID_MM_TEXTURE_ID) throw std::invalid_argument("input_pos"); @@ -451,11 +443,11 @@ struct NODISCARD ColoredRoomTex : public RoomTex { NamedColorEnum colorId = NamedColorEnum::DEFAULT; - explicit ColoredRoomTex(const Coordinate input_coord, + explicit ColoredRoomTex(const glm::vec3 input_center, const MMTexArrayPosition input_pos, const NamedColorEnum input_color, const float input_scale) - : RoomTex{input_coord, input_pos, input_scale} + : RoomTex{input_center, input_pos, input_scale} , colorId{input_color} {} }; @@ -587,12 +579,12 @@ NODISCARD static LayerMeshesIntermediate::FnVec createSortedTexturedMeshes( // A-B for (size_t i = beg; i < end; ++i) { const RoomTex &thisVert = textures[i]; - const auto &pos = thisVert.coord; + const auto ¢er = thisVert.center; const float scale = thisVert.scale; const auto z = thisVert.pos.position; #define EMIT(x, y) \ - verts.emplace_back(glm::vec3((x), (y), z), ::scaledRoomVertex(pos, scale, (x), (y))) + verts.emplace_back(glm::vec3((x), (y), z), ::scaledRoomVertex(center, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -652,13 +644,13 @@ NODISCARD static LayerMeshesIntermediate::FnVec createSortedColoredTexturedMeshe // A-B for (size_t i = beg; i < end; ++i) { const ColoredRoomTex &thisVert = textures[i]; - const auto &pos = thisVert.coord; + const auto ¢er = thisVert.center; const float scale = thisVert.scale; const auto color = XNamedColor{thisVert.colorId}.getColor(); const auto z = thisVert.pos.position; #define EMIT(x, y) \ - verts.emplace_back(color, glm::vec3((x), (y), z), ::scaledRoomVertex(pos, scale, (x), (y))) + verts.emplace_back(color, glm::vec3((x), (y), z), ::scaledRoomVertex(center, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -754,7 +746,16 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks private: NODISCARD bool virt_acceptRoom(const RoomHandle &room) const final { - return m_bounds.contains(room.getPosition()); + if (!m_bounds.isRestricted()) { + return true; + } + + const auto &bounds = m_bounds.getBounds(); + const auto transform = getRoomRenderTransform(room); + const auto ¢er = transform.renderCenter; + return center.x >= bounds.min.x && center.x <= bounds.max.x + 1.f + && center.y >= bounds.min.y && center.y <= bounds.max.y + 1.f + && center.z >= bounds.min.z && center.z <= bounds.max.z + 1.f; } void virt_visitTerrainTexture(const RoomHandle &room, const MMTexArrayPosition &terrain) final @@ -763,12 +764,12 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks return; } - const auto pos = room.getPosition(); - const float scale = sanitizeRoomScale(room.getScaleFactor()); - m_data.roomTerrains.emplace_back(pos, terrain, scale); + const auto transform = getRoomRenderTransform(room); + const float scale = getCombinedRoomScale(transform); + m_data.roomTerrains.emplace_back(transform.renderCenter, terrain, scale); #define EMIT(x, y) \ - m_data.roomLayerBoostQuads.emplace_back(::scaledRoomVertex(pos, scale, (x), (y))) + m_data.roomLayerBoostQuads.emplace_back(::scaledRoomVertex(transform.renderCenter, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -779,26 +780,29 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks void virt_visitTrailTexture(const RoomHandle &room, const MMTexArrayPosition &trail) final { if (trail.array != INVALID_MM_TEXTURE_ID) { - m_data.roomTrails.emplace_back(room.getPosition(), + const auto transform = getRoomRenderTransform(room); + m_data.roomTrails.emplace_back(transform.renderCenter, trail, - sanitizeRoomScale(room.getScaleFactor())); + getCombinedRoomScale(transform)); } } void virt_visitOverlayTexture(const RoomHandle &room, const MMTexArrayPosition &overlay) final { if (overlay.array != INVALID_MM_TEXTURE_ID) { - m_data.roomOverlays.emplace_back(room.getPosition(), + const auto transform = getRoomRenderTransform(room); + m_data.roomOverlays.emplace_back(transform.renderCenter, overlay, - sanitizeRoomScale(room.getScaleFactor())); + getCombinedRoomScale(transform)); } } void virt_visitNamedColorTint(const RoomHandle &room, const RoomTintEnum tint) final { - const auto pos = room.getPosition(); - const float scale = sanitizeRoomScale(room.getScaleFactor()); -#define EMIT(x, y) m_data.roomTints[tint].emplace_back(::scaledRoomVertex(pos, scale, (x), (y))) + const auto transform = getRoomRenderTransform(room); + const float scale = getCombinedRoomScale(transform); +#define EMIT(x, y) \ + m_data.roomTints[tint].emplace_back(::scaledRoomVertex(transform.renderCenter, scale, (x), (y))) EMIT(0, 0); EMIT(1, 0); EMIT(1, 1); @@ -820,27 +824,30 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks // Note: We could use two door textures (NESW and UD), and then just rotate the // texture coordinates, but doing that would require a different code path. const MMTexArrayPosition &tex = m_textures.door[dir]; - m_data.doors.emplace_back(room.getPosition(), + const auto transform = getRoomRenderTransform(room); + m_data.doors.emplace_back(transform.renderCenter, tex, color, - sanitizeRoomScale(room.getScaleFactor())); + getCombinedRoomScale(transform)); } else { if (isNESW(dir)) { + const auto transform = getRoomRenderTransform(room); if (wallType == WallTypeEnum::SOLID) { const MMTexArrayPosition &tex = m_textures.wall[dir]; - m_data.solidWallLines.emplace_back(room.getPosition(), + m_data.solidWallLines.emplace_back(transform.renderCenter, tex, color, - sanitizeRoomScale(room.getScaleFactor())); + getCombinedRoomScale(transform)); } else { const MMTexArrayPosition &tex = m_textures.dotted_wall[dir]; - m_data.dottedWallLines.emplace_back(room.getPosition(), + m_data.dottedWallLines.emplace_back(transform.renderCenter, tex, color, - sanitizeRoomScale(room.getScaleFactor())); + getCombinedRoomScale(transform)); } } else { + const auto transform = getRoomRenderTransform(room); const bool isUp = dir == ExitDirEnum::UP; assert(isUp || dir == ExitDirEnum::DOWN); @@ -850,7 +857,7 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks : m_textures.exit_down); m_data.roomUpDownExits.emplace_back( - room.getPosition(), tex, color, sanitizeRoomScale(room.getScaleFactor())); + transform.renderCenter, tex, color, getCombinedRoomScale(transform)); } } } @@ -861,14 +868,20 @@ class NODISCARD LayerBatchBuilder final : public IRoomVisitorCallbacks { switch (type) { case StreamTypeEnum::OutFlow: - m_data.streamOuts.emplace_back(room.getPosition(), - m_textures.stream_out[dir], - sanitizeRoomScale(room.getScaleFactor())); + { + const auto transform = getRoomRenderTransform(room); + m_data.streamOuts.emplace_back(transform.renderCenter, + m_textures.stream_out[dir], + getCombinedRoomScale(transform)); + } return; case StreamTypeEnum::InFlow: - m_data.streamIns.emplace_back(room.getPosition(), - m_textures.stream_in[dir], - sanitizeRoomScale(room.getScaleFactor())); + { + const auto transform = getRoomRenderTransform(room); + m_data.streamIns.emplace_back(transform.renderCenter, + m_textures.stream_in[dir], + getCombinedRoomScale(transform)); + } return; default: break; diff --git a/src/display/RoomRenderTransform.cpp b/src/display/RoomRenderTransform.cpp new file mode 100644 index 000000000..0c0b980ef --- /dev/null +++ b/src/display/RoomRenderTransform.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "RoomRenderTransform.h" + +#include "../map/World.h" + +#include + +NODISCARD static float sanitizeRoomScale(const float scale) +{ + if (!std::isfinite(scale) || scale <= 0.f) { + return 1.f; + } + return scale; +} + +RoomRenderTransform getRoomRenderTransform(const RoomHandle &room) +{ + RoomRenderTransform transform; + const auto &pos = room.getPosition(); + transform.roomCenter = pos.to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; + transform.roomScale = sanitizeRoomScale(room.getScaleFactor()); + + const Map map = room.getMap(); + const auto &world = map.getWorld(); + transform.localspaceId = world.getRoomLocalSpace(room.getId()); + + if (transform.localspaceId) { + if (const auto renderData = world.getLocalSpaceRenderData(*transform.localspaceId)) { + transform.localspaceScale = renderData->portalScale; + transform.localspaceOrigin = glm::vec3{renderData->portalX, + renderData->portalY, + renderData->portalZ} + - glm::vec3{renderData->localCx, + renderData->localCy, + renderData->localCz} + * transform.localspaceScale; + } + } + + transform.renderCenter = applyLocalspaceTransform(transform, transform.roomCenter); + return transform; +} + +glm::vec3 applyLocalspaceTransform(const RoomRenderTransform &transform, const glm::vec3 &pos) +{ + return transform.localspaceOrigin + pos * transform.localspaceScale; +} + +glm::vec3 applyRoomGeometryTransform(const RoomRenderTransform &transform, const glm::vec3 &pos) +{ + const glm::vec3 scaledPos + = transform.roomCenter + (pos - transform.roomCenter) * transform.roomScale; + return applyLocalspaceTransform(transform, scaledPos); +} + +float getCombinedRoomScale(const RoomRenderTransform &transform) +{ + return transform.roomScale * transform.localspaceScale; +} diff --git a/src/display/RoomRenderTransform.h b/src/display/RoomRenderTransform.h new file mode 100644 index 000000000..37b0d02e6 --- /dev/null +++ b/src/display/RoomRenderTransform.h @@ -0,0 +1,28 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "../global/macros.h" +#include "../map/RoomHandle.h" +#include "../map/localspace.h" + +#include + +#include + +struct NODISCARD RoomRenderTransform final +{ + glm::vec3 roomCenter{}; + glm::vec3 renderCenter{}; + glm::vec3 localspaceOrigin{}; + float roomScale = 1.f; + float localspaceScale = 1.f; + std::optional localspaceId; +}; + +NODISCARD RoomRenderTransform getRoomRenderTransform(const RoomHandle &room); +NODISCARD glm::vec3 applyLocalspaceTransform(const RoomRenderTransform &transform, + const glm::vec3 &pos); +NODISCARD glm::vec3 applyRoomGeometryTransform(const RoomRenderTransform &transform, + const glm::vec3 &pos); +NODISCARD float getCombinedRoomScale(const RoomRenderTransform &transform); diff --git a/src/display/RoomSelections.cpp b/src/display/RoomSelections.cpp index be98e9058..e0619d256 100644 --- a/src/display/RoomSelections.cpp +++ b/src/display/RoomSelections.cpp @@ -2,6 +2,7 @@ // Copyright (C) 2019 The MMapper Authors #include "../global/EnumIndexedArray.h" +#include "../map/RoomHandle.h" #include "../map/coordinate.h" #include "../map/room.h" #include "../mapdata/roomselection.h" @@ -9,6 +10,7 @@ #include "../opengl/OpenGLTypes.h" #include "Characters.h" #include "MapCanvasData.h" +#include "RoomRenderTransform.h" #include "Textures.h" #include "mapcanvas.h" @@ -123,31 +125,33 @@ NODISCARD static float sanitizeRoomScale(const float scale) return scale; } -void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) +void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RoomHandle &room) { - const Coordinate &roomPos = room.getPosition(); - const float x = static_cast(roomPos.x); - const float y = static_cast(roomPos.y); - const float z = static_cast(roomPos.z); - const float roomScale = sanitizeRoomScale(room.getScaleFactor()); + const auto transform = getRoomRenderTransform(room); + const glm::vec3 &renderCenter = transform.renderCenter; + const glm::vec3 &roomCenter = transform.roomCenter; + const float roomScale = sanitizeRoomScale(transform.roomScale); + const float combinedScale = roomScale * transform.localspaceScale; // This fake GL uses resetMatrix() before this function. gl.resetMatrix(); const float marginPixels = MapScreen::DEFAULT_MARGIN_PIXELS; const bool isMoving = hasRoomSelectionMove(); + const bool isVisible = glm::distance(m_mapScreen.getProxyLocation(renderCenter, marginPixels / 2.f), + renderCenter) + < 0.001f; - if (!isMoving && !m_mapScreen.isRoomVisible(roomPos, marginPixels / 2.f)) { - const glm::vec3 roomCenter = roomPos.to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; - const auto dot = DistantObjectTransform::construct(roomCenter, m_mapScreen, marginPixels); + if (!isMoving && !isVisible) { + const auto dot = DistantObjectTransform::construct(renderCenter, m_mapScreen, marginPixels); gl.glTranslatef(dot.offset.x, dot.offset.y, dot.offset.z); gl.glRotatef(dot.rotationDegrees, 0.f, 0.f, 1.f); const glm::vec2 iconCenter{0.5f, 0.5f}; gl.glTranslatef(-iconCenter.x, -iconCenter.y, 0.f); // Scale based upon distance - const auto scaleFactor = std::invoke([this, roomCenter]() -> float { - const auto delta = roomCenter - m_mapScreen.getCenter(); + const auto scaleFactor = std::invoke([this, &renderCenter]() -> float { + const auto delta = renderCenter - m_mapScreen.getCenter(); const auto distance = std::sqrt((delta.y * delta.y) + (delta.x * delta.x)); // If we're too far away just scale down to 50% at most if (distance >= static_cast(BASESIZE)) { @@ -160,8 +164,8 @@ void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) gl.drawColoredQuad(RoomSelFakeGL::SelTypeEnum::Distant); } else { // Room is close - gl.glTranslatef(x + 0.5f, y + 0.5f, z); - gl.glScalef(roomScale, roomScale, 1.f); + gl.glTranslatef(renderCenter.x, renderCenter.y, renderCenter.z); + gl.glScalef(combinedScale, combinedScale, 1.f); gl.glTranslatef(-0.5f, -0.5f, 0.f); gl.drawColoredQuad(RoomSelFakeGL::SelTypeEnum::Near); gl.resetMatrix(); @@ -170,10 +174,14 @@ void MapCanvas::paintSelectedRoom(RoomSelFakeGL &gl, const RawRoom &room) if (isMoving) { gl.resetMatrix(); const auto &relativeOffset = m_roomSelectionMove->pos; - gl.glTranslatef(x + static_cast(relativeOffset.x) + 0.5f, - y + static_cast(relativeOffset.y) + 0.5f, - z); - gl.glScalef(roomScale, roomScale, 1.f); + const glm::vec3 movedCenter + = applyLocalspaceTransform(transform, + roomCenter + + glm::vec3{static_cast(relativeOffset.x), + static_cast(relativeOffset.y), + 0.f}); + gl.glTranslatef(movedCenter.x, movedCenter.y, movedCenter.z); + gl.glScalef(combinedScale, combinedScale, 1.f); gl.glTranslatef(-0.5f, -0.5f, 0.f); gl.drawColoredQuad(m_roomSelectionMove->wrongPlace ? RoomSelFakeGL::SelTypeEnum::MoveBad : RoomSelFakeGL::SelTypeEnum::MoveGood); @@ -191,7 +199,7 @@ void MapCanvas::paintSelectedRooms() for (const RoomId id : deref(m_roomSelection)) { if (const auto room = m_data.findRoomHandle(id)) { gl.resetMatrix(); - paintSelectedRoom(gl, room.getRaw()); + paintSelectedRoom(gl, room); } } diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index b4c99ca5d..63cc6ef78 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -12,6 +12,7 @@ #include "../global/utils.h" #include "../map/ExitDirection.h" #include "../map/RoomHandle.h" +#include "../map/World.h" #include "../map/coordinate.h" #include "../map/exit.h" #include "../map/infomark.h" @@ -22,12 +23,15 @@ #include "InfomarkSelection.h" #include "MapCanvasData.h" #include "MapCanvasRoomDrawer.h" +#include "RoomRenderTransform.h" #include "connectionselection.h" +#include #include #include #include #include +#include #include #include #include @@ -1119,9 +1123,15 @@ void MapCanvas::slot_zoomReset() void MapCanvas::onMovement() { - const Coordinate &pos = m_data.tryGetPosition().value_or(Coordinate{}); - m_currentLayer = pos.z; - emit sig_onCenter(pos.to_vec2() + glm::vec2{0.5f, 0.5f}); + if (const auto room = m_data.getCurrentRoom()) { + const auto transform = getRoomRenderTransform(room); + m_currentLayer = room.getPosition().z; + emit sig_onCenter(glm::vec2{transform.renderCenter.x, transform.renderCenter.y}); + } else { + const Coordinate &pos = m_data.tryGetPosition().value_or(Coordinate{}); + m_currentLayer = pos.z; + emit sig_onCenter(pos.to_vec2() + glm::vec2{0.5f, 0.5f}); + } updateEffectiveScale(); update(); } @@ -1136,9 +1146,55 @@ void MapCanvas::updateEffectiveScale() void MapCanvas::updateEffectiveScaleForRoom(const RoomHandle &room) { const float roomScale = room.getScaleFactor(); - // When entering a room with scale < 1.0, we need to zoom out (increase effective scale) - // so that the room appears at normal size but surroundings appear larger - m_targetEffectiveScale = (roomScale > 0.f) ? (1.f / roomScale) : 1.f; + float target = (roomScale > 0.f) ? (1.f / roomScale) : 1.f; + + const auto &world = room.getMap().getWorld(); + const auto localData = world.getLocalSpaceRenderDataForRoom(room.getId()); + if (localData) { + const float portalScale = localData->portalScale; + if (portalScale > 0.f) { + target *= 1.f / portalScale; + } + } else { + static constexpr float maxZoom = 3.0f; + static constexpr float outerRadius = 3.0f; + static constexpr float minStrength = 1.0f / 3.0f; + static constexpr float maxStrength = 0.5f; + if (outerRadius > 0.f) { + const auto spaces = world.getLocalSpaceRenderDataList(); + const glm::vec3 pos = room.getPosition().to_vec3() + glm::vec3{0.5f, 0.5f, 0.f}; + float bestDist = std::numeric_limits::max(); + float bestPortalScale = 0.f; + for (const auto &space : spaces) { + const float portalScale = space.portalScale; + if (portalScale <= 0.f) { + continue; + } + const float dx = pos.x - space.portalX; + const float dy = pos.y - space.portalY; + const float dist = std::sqrt(dx * dx + dy * dy); + if (dist < bestDist) { + bestDist = dist; + bestPortalScale = portalScale; + } + } + + if (bestPortalScale > 0.f) { + const float zoomIn = std::max(1.f, std::min(maxZoom, 1.f / bestPortalScale)); + float strength = 0.f; + if (bestDist <= outerRadius) { + const float t = std::clamp((outerRadius - bestDist) / outerRadius, 0.f, 1.f); + const float smooth = t * t * (3.f - 2.f * t); + strength = minStrength + (maxStrength - minStrength) * smooth; + } + if (strength > 0.f) { + target *= std::exp(std::log(zoomIn) * strength); + } + } + } + } + + m_targetEffectiveScale = target; } void MapCanvas::slot_dataLoaded() diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 5d6a3a980..8cfff1e30 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -39,6 +39,7 @@ class Coordinate; class InfomarkSelection; class MapData; class RoomHandle; +class RoomHandle; class Mmapper2Group; class PrespammedPath; class QMouseEvent; @@ -257,7 +258,7 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, void paintSelectionArea(); void paintNewInfomarkSelection(); void paintSelectedRooms(); - void paintSelectedRoom(RoomSelFakeGL &, const RawRoom &room); + void paintSelectedRoom(RoomSelFakeGL &, const RoomHandle &room); void paintSelectedConnection(); void paintNearbyConnectionPoints(); void paintSelectedInfomarks(); diff --git a/src/map/ChangePrinter.cpp b/src/map/ChangePrinter.cpp index 819babbfd..137aeb26a 100644 --- a/src/map/ChangePrinter.cpp +++ b/src/map/ChangePrinter.cpp @@ -161,6 +161,11 @@ void ChangePrinter::print(const float value) m_os.writeWithColor(const_color, value); } +void ChangePrinter::print(const std::string_view value) +{ + print_string_color_quoted(m_os, value); +} + void ChangePrinter::print(const Coordinate &coord) { auto &os = m_os; @@ -748,6 +753,36 @@ void ChangePrinter::virt_accept(const GenerateBaseMap &) m_os << "GenerateBaseMap{}"; } +void ChangePrinter::virt_accept(const CreateLocalSpace &change) +{ + BEGIN_STRUCT_HELPER("CreateLocalSpace") + { + HELPER_ADD_MEMBER(name); + } +} + +void ChangePrinter::virt_accept(const SetLocalSpacePortal &change) +{ + BEGIN_STRUCT_HELPER("SetLocalSpacePortal") + { + HELPER_ADD_MEMBER(name); + HELPER_ADD_MEMBER(x); + HELPER_ADD_MEMBER(y); + HELPER_ADD_MEMBER(z); + HELPER_ADD_MEMBER(w); + HELPER_ADD_MEMBER(h); + } +} + +void ChangePrinter::virt_accept(const AddRoomToLocalSpace &change) +{ + BEGIN_STRUCT_HELPER("AddRoomToLocalSpace") + { + HELPER_ADD_MEMBER(name); + HELPER_ADD_MEMBER(room); + } +} + void ChangePrinter::virt_accept(const AddPermanentRoom &change) { BEGIN_STRUCT_HELPER("AddPermanentRoom") diff --git a/src/map/ChangePrinter.h b/src/map/ChangePrinter.h index 1a2758799..a5528d515 100644 --- a/src/map/ChangePrinter.h +++ b/src/map/ChangePrinter.h @@ -16,6 +16,7 @@ #include #include +#include struct NODISCARD ChangePrinter final : public AbstractChangeVisitor { @@ -47,6 +48,7 @@ struct NODISCARD ChangePrinter final : public AbstractChangeVisitor void print(ServerRoomId serverId); void print(const Coordinate &coord); void print(float value); + void print(std::string_view value); private: void print(const DoorName &name); diff --git a/src/map/ChangeTypes.h b/src/map/ChangeTypes.h index b0c8d1e9e..d1f6130e1 100644 --- a/src/map/ChangeTypes.h +++ b/src/map/ChangeTypes.h @@ -10,9 +10,11 @@ #include "Map.h" #include "RoomHandle.h" #include "infomark.h" +#include "localspace.h" #include "mmapper2room.h" #include "roomid.h" +#include #include #define XFOREACH_ChangeTypeEnum(X) \ @@ -53,6 +55,27 @@ struct NODISCARD RemoveAllDoorNames final struct NODISCARD GenerateBaseMap final {}; +struct NODISCARD CreateLocalSpace final +{ + std::string name; +}; + +struct NODISCARD SetLocalSpacePortal final +{ + std::string name; + float x = 0.f; + float y = 0.f; + float z = 0.f; + float w = 0.f; + float h = 0.f; +}; + +struct NODISCARD AddRoomToLocalSpace final +{ + std::string name; + RoomId room = INVALID_ROOMID; +}; + }; // namespace world_change_types namespace room_change_types { @@ -291,7 +314,13 @@ struct NODISCARD ConnectToNeighborsArgs final SEP() \ X(world_change_types::RemoveAllDoorNames) \ SEP() \ - X(world_change_types::GenerateBaseMap) + X(world_change_types::GenerateBaseMap) \ + SEP() \ + X(world_change_types::CreateLocalSpace) \ + SEP() \ + X(world_change_types::SetLocalSpacePortal) \ + SEP() \ + X(world_change_types::AddRoomToLocalSpace) #define XFOREACH_ROOM_CHANGE_TYPES(X, SEP) \ X(room_change_types::AddPermanentRoom) \ diff --git a/src/map/World.cpp b/src/map/World.cpp index d58f99d97..5cf3d14b8 100644 --- a/src/map/World.cpp +++ b/src/map/World.cpp @@ -15,6 +15,9 @@ #include "sanitizer.h" #include "utils.h" +#include +#include +#include #include #include #include @@ -165,6 +168,9 @@ World World::copy() const result.m_parseTree = m_parseTree; result.m_areaInfos = m_areaInfos; result.m_infomarks = m_infomarks; + result.m_localSpaces = m_localSpaces; + result.m_roomLocalSpaces = m_roomLocalSpaces; + result.m_nextLocalSpaceId = m_nextLocalSpaceId; result.m_checkedConsistency = false; return result; @@ -179,7 +185,10 @@ bool World::operator==(const World &rhs) const && m_serverIds == rhs.m_serverIds // && m_parseTree == rhs.m_parseTree // && m_areaInfos == rhs.m_areaInfos // - && m_infomarks == rhs.m_infomarks; + && m_infomarks == rhs.m_infomarks // + && m_localSpaces == rhs.m_localSpaces + && m_roomLocalSpaces == rhs.m_roomLocalSpaces + && m_nextLocalSpaceId == rhs.m_nextLocalSpaceId; } const AreaInfo &World::getArea(const RoomArea &area) const @@ -199,6 +208,248 @@ const RawRoom *World::getRoom(const RoomId id) const return std::addressof(ref); } +std::optional World::findLocalSpaceId(const std::string_view name) const +{ + for (const auto &space : m_localSpaces) { + if (space.name == name) { + return space.id; + } + } + return std::nullopt; +} + +std::optional World::getRoomLocalSpace(const RoomId id) const +{ + if (const auto it = m_roomLocalSpaces.find(id); it != m_roomLocalSpaces.end()) { + return it->second; + } + return std::nullopt; +} + +std::optional World::getLocalSpaceRenderData(const LocalSpaceId id) const +{ + const LocalSpace *space = findLocalSpace(id); + if (space == nullptr) { + return std::nullopt; + } + + updateLocalSpaceBounds(*space); + if (!space->hasPortal || !space->hasBounds) { + return std::nullopt; + } + + const float portalScale = computePortalScale(*space); + if (portalScale <= 0.f) { + return std::nullopt; + } + + LocalSpaceRenderData data; + data.portalScale = portalScale; + data.portalX = space->portalX + 0.5f; + data.portalY = space->portalY + 0.5f; + data.portalZ = space->portalZ; + data.localCx = (space->minX + space->maxX + 1.f) * 0.5f; + data.localCy = (space->minY + space->maxY + 1.f) * 0.5f; + data.localCz = (space->minZ + space->maxZ) * 0.5f; + return data; +} + +std::vector World::getLocalSpaceRenderDataList() const +{ + std::vector result; + result.reserve(m_localSpaces.size()); + + for (const auto &space : m_localSpaces) { + updateLocalSpaceBounds(space); + if (!space.hasPortal || !space.hasBounds) { + continue; + } + + const float portalScale = computePortalScale(space); + if (portalScale <= 0.f) { + continue; + } + + LocalSpaceRenderData data; + data.portalScale = portalScale; + data.portalX = space.portalX + 0.5f; + data.portalY = space.portalY + 0.5f; + data.portalZ = space.portalZ; + data.localCx = (space.minX + space.maxX + 1.f) * 0.5f; + data.localCy = (space.minY + space.maxY + 1.f) * 0.5f; + data.localCz = (space.minZ + space.maxZ) * 0.5f; + result.emplace_back(data); + } + + return result; +} + +std::optional World::getLocalSpaceRenderDataForRoom(const RoomId id) const +{ + if (const auto optSpaceId = getRoomLocalSpace(id)) { + return getLocalSpaceRenderData(*optSpaceId); + } + return std::nullopt; +} + +LocalSpaceId World::createLocalSpace(std::string name) +{ + if (const auto existing = findLocalSpaceId(name)) { + return *existing; + } + + LocalSpace space; + space.id = m_nextLocalSpaceId; + space.name = std::move(name); + m_nextLocalSpaceId = LocalSpaceId{m_nextLocalSpaceId.asUint32() + 1}; + m_localSpaces.emplace_back(std::move(space)); + return m_localSpaces.back().id; +} + +bool World::setLocalSpacePortal(const LocalSpaceId id, + const float x, + const float y, + const float z, + const float w, + const float h) +{ + LocalSpace *space = findLocalSpace(id); + if (space == nullptr) { + return false; + } + + space->portalX = x; + space->portalY = y; + space->portalZ = z; + space->portalW = w; + space->portalH = h; + space->hasPortal = true; + return true; +} + +bool World::addRoomToLocalSpace(const LocalSpaceId id, const RoomId room) +{ + requireValidRoom(room); + LocalSpace *space = findLocalSpace(id); + if (space == nullptr) { + return false; + } + + removeRoomFromLocalSpace(room); + space->rooms.insert(room); + m_roomLocalSpaces[room] = id; + space->boundsDirty = true; + return true; +} + +void World::removeRoomFromLocalSpace(const RoomId room) +{ + if (const auto it = m_roomLocalSpaces.find(room); it != m_roomLocalSpaces.end()) { + const LocalSpaceId id = it->second; + m_roomLocalSpaces.erase(it); + if (LocalSpace *space = findLocalSpace(id)) { + space->rooms.erase(room); + space->boundsDirty = true; + } + } +} + +void World::markLocalSpaceBoundsDirty(const LocalSpaceId id) +{ + if (LocalSpace *space = findLocalSpace(id)) { + space->boundsDirty = true; + } +} + +void World::markAllLocalSpaceBoundsDirty() +{ + for (auto &space : m_localSpaces) { + space.boundsDirty = true; + } +} + +World::LocalSpace *World::findLocalSpace(const LocalSpaceId id) +{ + for (auto &space : m_localSpaces) { + if (space.id == id) { + return &space; + } + } + return nullptr; +} + +const World::LocalSpace *World::findLocalSpace(const LocalSpaceId id) const +{ + for (const auto &space : m_localSpaces) { + if (space.id == id) { + return &space; + } + } + return nullptr; +} + +void World::updateLocalSpaceBounds(const LocalSpace &space) const +{ + if (!space.boundsDirty) { + return; + } + + space.boundsDirty = false; + space.hasBounds = false; + + for (const RoomId id : space.rooms) { + const RawRoom *room = getRoom(id); + if (room == nullptr) { + continue; + } + + const auto &pos = room->getPosition(); + const float x = static_cast(pos.x); + const float y = static_cast(pos.y); + const float z = static_cast(pos.z); + + if (!space.hasBounds) { + space.minX = space.maxX = x; + space.minY = space.maxY = y; + space.minZ = space.maxZ = z; + space.hasBounds = true; + continue; + } + + space.minX = std::min(space.minX, x); + space.maxX = std::max(space.maxX, x); + space.minY = std::min(space.minY, y); + space.maxY = std::max(space.maxY, y); + space.minZ = std::min(space.minZ, z); + space.maxZ = std::max(space.maxZ, z); + } +} + +float World::computePortalScale(const LocalSpace &space) const +{ + if (!space.hasPortal || !space.hasBounds) { + return 0.f; + } + + const float localW = space.maxX - space.minX + 1.f; + const float localH = space.maxY - space.minY + 1.f; + const bool hasW = localW > 0.f; + const bool hasH = localH > 0.f; + const bool hasPortalW = space.portalW > 0.f; + const bool hasPortalH = space.portalH > 0.f; + + if (hasW && hasH && hasPortalW && hasPortalH) { + return std::min(space.portalW / localW, space.portalH / localH); + } + if (hasW && hasPortalW) { + return space.portalW / localW; + } + if (hasH && hasPortalH) { + return space.portalH / localH; + } + return 0.f; +} + bool World::hasRoom(const RoomId id) const { if (id == INVALID_ROOMID) { @@ -865,6 +1116,10 @@ void World::setPosition(const RoomId id, const Coordinate &coord) const Coordinate &ref = m_rooms.getPosition(id); m_spatialDb.move(id, ref, coord); m_rooms.setPosition(id, coord); + + if (const auto it = m_roomLocalSpaces.find(id); it != m_roomLocalSpaces.end()) { + markLocalSpaceBoundsDirty(it->second); + } } bool World::wouldAllowRelativeMove(const RoomIdSet &rooms, const Coordinate &offset) const @@ -946,6 +1201,8 @@ void World::removeFromWorld(const RoomId id, const bool removeLinks) throw InvalidMapOperation("Invalid RoomId"); } + removeRoomFromLocalSpace(id); + const auto coord = getPosition(id); const auto server_id = getServerId(id); const auto area = getRoomArea(id); @@ -1501,6 +1758,34 @@ void World::apply(ProgressCounter &pc, const world_change_types::RemoveAllDoorNa << ((numRemoved == 1) ? "" : "s") << "."; } +void World::apply(ProgressCounter &, const world_change_types::CreateLocalSpace &change) +{ + static_cast(createLocalSpace(change.name)); +} + +void World::apply(ProgressCounter &, const world_change_types::SetLocalSpacePortal &change) +{ + const auto id = findLocalSpaceId(change.name); + if (!id) { + throw InvalidMapOperation("Unknown localspace name"); + } + if (!setLocalSpacePortal(*id, change.x, change.y, change.z, change.w, change.h)) { + throw InvalidMapOperation("Unable to set localspace portal"); + } +} + +void World::apply(ProgressCounter &, const world_change_types::AddRoomToLocalSpace &change) +{ + requireValidRoom(change.room); + const auto id = findLocalSpaceId(change.name); + if (!id) { + throw InvalidMapOperation("Unknown localspace name"); + } + if (!addRoomToLocalSpace(*id, change.room)) { + throw InvalidMapOperation("Unable to add room to localspace"); + } +} + void World::apply(ProgressCounter &, const exit_change_types::NukeExit &change) { nukeExit(change.room, change.dir, change.ways); @@ -2573,6 +2858,10 @@ NODISCARD bool hasMeshDifference(const World &a, const World &b) { DECL_TIMER(t, "hasMeshDifference (parallel)"); + if (a.m_localSpaces != b.m_localSpaces || a.m_roomLocalSpaces != b.m_roomLocalSpaces) { + return true; + } + struct NODISCARD ThreadLocal final { bool result = false; diff --git a/src/map/World.h b/src/map/World.h index 13cbf03c0..fefbfcabe 100644 --- a/src/map/World.h +++ b/src/map/World.h @@ -7,6 +7,7 @@ #include "Changes.h" #include "ExitFields.h" #include "InvalidMapOperation.h" +#include "localspace.h" #include "ParseTree.h" #include "RawRooms.h" #include "Remapping.h" @@ -18,7 +19,9 @@ #include #include #include +#include #include +#include #include class RawRooms; @@ -43,6 +46,36 @@ NODISCARD bool hasMeshDifference(const World &a, const World &b); class NODISCARD World final { private: + struct NODISCARD LocalSpace final + { + LocalSpaceId id = INVALID_LOCALSPACE_ID; + std::string name; + float portalX = 0.f; + float portalY = 0.f; + float portalZ = 0.f; + float portalW = 0.f; + float portalH = 0.f; + bool hasPortal = false; + RoomIdSet rooms; + + mutable bool boundsDirty = true; + mutable bool hasBounds = false; + mutable float minX = 0.f; + mutable float maxX = 0.f; + mutable float minY = 0.f; + mutable float maxY = 0.f; + mutable float minZ = 0.f; + mutable float maxZ = 0.f; + + NODISCARD bool operator==(const LocalSpace &rhs) const + { + return id == rhs.id && name == rhs.name && portalX == rhs.portalX + && portalY == rhs.portalY && portalZ == rhs.portalZ && portalW == rhs.portalW + && portalH == rhs.portalH && hasPortal == rhs.hasPortal && rooms == rhs.rooms; + } + NODISCARD bool operator!=(const LocalSpace &rhs) const { return !(*this == rhs); } + }; + Remapping m_remapping; RawRooms m_rooms; /// This must be updated any time a room's position changes. @@ -51,6 +84,9 @@ class NODISCARD World final ParseTree m_parseTree; AreaInfoMap m_areaInfos; InfomarkDb m_infomarks; + std::vector m_localSpaces; + std::unordered_map m_roomLocalSpaces; + LocalSpaceId m_nextLocalSpaceId{1}; bool m_checkedConsistency = false; public: @@ -78,6 +114,14 @@ class NODISCARD World final public: NODISCARD const InfomarkDb &getInfomarkDb() const { return m_infomarks; } +public: + NODISCARD std::optional findLocalSpaceId(std::string_view name) const; + NODISCARD std::optional getRoomLocalSpace(RoomId id) const; + NODISCARD std::optional getLocalSpaceRenderData(LocalSpaceId id) const; + NODISCARD std::vector getLocalSpaceRenderDataList() const; + NODISCARD std::optional + getLocalSpaceRenderDataForRoom(RoomId id) const; + public: NODISCARD const RawRoom *getRoom(RoomId id) const; @@ -185,6 +229,18 @@ class NODISCARD World final NODISCARD static WorldComparisonStats getComparisonStats(const World &base, const World &modified); +private: + LocalSpaceId createLocalSpace(std::string name); + bool setLocalSpacePortal(LocalSpaceId id, float x, float y, float z, float w, float h); + bool addRoomToLocalSpace(LocalSpaceId id, RoomId room); + void removeRoomFromLocalSpace(RoomId room); + void markLocalSpaceBoundsDirty(LocalSpaceId id); + void markAllLocalSpaceBoundsDirty(); + LocalSpace *findLocalSpace(LocalSpaceId id); + const LocalSpace *findLocalSpace(LocalSpaceId id) const; + void updateLocalSpaceBounds(const LocalSpace &space) const; + NODISCARD float computePortalScale(const LocalSpace &space) const; + private: void insertParse(RoomId id, ParseKeyFlags parseKeys); diff --git a/src/map/localspace.h b/src/map/localspace.h new file mode 100644 index 000000000..37f32b3a6 --- /dev/null +++ b/src/map/localspace.h @@ -0,0 +1,45 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "../global/TaggedInt.h" +#include "../global/hash.h" +#include "../global/macros.h" + +#include + +namespace tags { +struct NODISCARD LocalSpaceIdTag final +{}; +} // namespace tags + +struct NODISCARD LocalSpaceId final : public TaggedInt +{ + using TaggedInt::TaggedInt; + constexpr LocalSpaceId() + : LocalSpaceId{0} + {} + NODISCARD constexpr uint32_t asUint32() const { return value(); } +}; + +static constexpr LocalSpaceId INVALID_LOCALSPACE_ID{0}; + +template<> +struct std::hash +{ + std::size_t operator()(const LocalSpaceId id) const noexcept + { + return numeric_hash(id.asUint32()); + } +}; + +struct NODISCARD LocalSpaceRenderData final +{ + float portalScale = 0.f; + float portalX = 0.f; + float portalY = 0.f; + float portalZ = 0.f; + float localCx = 0.f; + float localCy = 0.f; + float localCz = 0.f; +}; diff --git a/src/parser/AbstractParser-Commands.cpp b/src/parser/AbstractParser-Commands.cpp index d1b724cb3..562db4c1b 100644 --- a/src/parser/AbstractParser-Commands.cpp +++ b/src/parser/AbstractParser-Commands.cpp @@ -14,6 +14,7 @@ #include "../map/CommandId.h" #include "../map/DoorFlags.h" #include "../map/ExitFlags.h" +#include "../map/World.h" #include "../map/enums.h" #include "../map/infomark.h" #include "../mapdata/mapdata.h" @@ -49,6 +50,7 @@ const Abbrev cmdDoorHelp{"doorhelp", 5}; const Abbrev cmdGenerateBaseMap{"generate-base-map"}; const Abbrev cmdGroup{"group", 2}; const Abbrev cmdHelp{"help", 2}; +const Abbrev cmdLocalspace{"_localspace"}; const Abbrev cmdMap{"map"}; const Abbrev cmdMark{"mark", 3}; const Abbrev cmdRemoveDoorNames{"remove-secret-door-names"}; @@ -587,6 +589,120 @@ void AbstractParser::parseDirections(StringView view) } } +void AbstractParser::parseLocalspace(StringView view) +{ + using namespace syntax; + static const auto abb = syntax::abbrevToken; + + auto createLocalspace = Accept( + [this](User &user, const Pair *const args) { + auto &os = user.getOstream(); + const auto v = getAnyVectorReversed(args); + + if constexpr (IS_DEBUG_BUILD) { + const auto &name = v[0].getString(); + assert(name == "create"); + } + + const std::string spaceName = v[1].getString(); + if (spaceName.empty()) { + os << "Localspace name is required.\n"; + return; + } + + if (!m_mapData.applySingleChange(Change{world_change_types::CreateLocalSpace{spaceName}})) { + os << "Failed to create localspace.\n"; + return; + } + send_ok(os); + }, + "create localspace"); + + auto setPortal = Accept( + [this](User &user, const Pair *const args) { + auto &os = user.getOstream(); + const auto v = getAnyVectorReversed(args); + + if constexpr (IS_DEBUG_BUILD) { + const auto &name = v[0].getString(); + assert(name == "portal"); + } + + const std::string spaceName = v[1].getString(); + if (spaceName.empty()) { + os << "Localspace name is required.\n"; + return; + } + + const auto &world = m_mapData.getCurrentMap().getWorld(); + if (!world.findLocalSpaceId(spaceName)) { + os << "Unknown localspace name.\n"; + return; + } + + const float x = v[2].getFloat(); + const float y = v[3].getFloat(); + const float z = v[4].getFloat(); + const float w = v[5].getFloat(); + const float h = v[6].getFloat(); + + if (!m_mapData.applySingleChange(Change{world_change_types::SetLocalSpacePortal{ + spaceName, x, y, z, w, h}})) { + os << "Failed to set portal.\n"; + return; + } + send_ok(os); + }, + "set localspace portal"); + + auto addRoom = Accept( + [this](User &user, const Pair *const args) { + auto &os = user.getOstream(); + const auto v = getAnyVectorReversed(args); + + if constexpr (IS_DEBUG_BUILD) { + const auto &name = v[0].getString(); + assert(name == "addroom"); + } + + const std::string spaceName = v[1].getString(); + if (spaceName.empty()) { + os << "Localspace name is required.\n"; + return; + } + + const auto &world = m_mapData.getCurrentMap().getWorld(); + if (!world.findLocalSpaceId(spaceName)) { + os << "Unknown localspace name.\n"; + return; + } + + const RoomId roomId = getOtherRoom(v[2].getInt()); + if (!m_mapData.applySingleChange( + Change{world_change_types::AddRoomToLocalSpace{spaceName, roomId}})) { + os << "Failed to add room to localspace.\n"; + return; + } + send_ok(os); + }, + "add room to localspace"); + + auto localspaceSyntax = buildSyntax( + buildSyntax(abb("create"), TokenMatcher::alloc(), createLocalspace), + buildSyntax(abb("portal"), + TokenMatcher::alloc(), + TokenMatcher::alloc(), + TokenMatcher::alloc(), + TokenMatcher::alloc(), + TokenMatcher::alloc(), + TokenMatcher::alloc(), + setPortal), + buildSyntax(abb("addroom"), TokenMatcher::alloc(), TokenMatcher::alloc(), + addRoom)); + + eval(cmdLocalspace.getCommand(), localspaceSyntax, view); +} + class NODISCARD ArgHelpCommand final : public syntax::IArgument { private: @@ -1011,6 +1127,13 @@ void AbstractParser::initSpecialCommandMap() return true; }, makeSimpleHelp("Print the changes changes since the last save")); + add( + cmdLocalspace, + [this](const std::vector & /*s*/, StringView rest) { + parseLocalspace(rest); + return true; + }, + makeSimpleHelp("Create localspaces and assign portals/rooms.")); add( cmdSearch, [this](const std::vector & /*s*/, StringView rest) { diff --git a/src/parser/AbstractParser-Commands.h b/src/parser/AbstractParser-Commands.h index dbc958158..03b9bc011 100644 --- a/src/parser/AbstractParser-Commands.h +++ b/src/parser/AbstractParser-Commands.h @@ -18,6 +18,7 @@ extern const Abbrev cmdDirections; extern const Abbrev cmdDoorHelp; extern const Abbrev cmdGroupTell; extern const Abbrev cmdHelp; +extern const Abbrev cmdLocalspace; extern const Abbrev cmdMap; extern const Abbrev cmdSearch; extern const Abbrev cmdSet; diff --git a/src/parser/abstractparser.h b/src/parser/abstractparser.h index 60c61167e..a4eb23d75 100644 --- a/src/parser/abstractparser.h +++ b/src/parser/abstractparser.h @@ -407,6 +407,7 @@ class NODISCARD_QOBJECT AbstractParser final : public ParserCommon void parseHelp(StringView words); void parseMark(StringView input); void parseRoom(StringView input); + void parseLocalspace(StringView input); void parseGroup(StringView input); void parseTimer(StringView input); diff --git a/src/syntax/SyntaxArgs.cpp b/src/syntax/SyntaxArgs.cpp index 88d090f15..2a682e0c8 100644 --- a/src/syntax/SyntaxArgs.cpp +++ b/src/syntax/SyntaxArgs.cpp @@ -352,7 +352,7 @@ MatchResult ArgFloat::virt_match(const ParserInput &input, IMatchErrorLogger *lo const std::string &firstWord = input.front(); using Limits = std::numeric_limits; - const float minVal = arg.min.value_or(Limits::min()); + const float minVal = arg.min.value_or(Limits::lowest()); const float maxVal = arg.max.value_or(Limits::max()); char *end = nullptr; @@ -506,7 +506,7 @@ std::ostream &ArgRest::virt_to_stream(std::ostream &os) const MatchResult ArgString::virt_match(const ParserInput &input, IMatchErrorLogger * /*logger*/) const { - if (input.length() != 1) { + if (input.empty()) { return MatchResult::failure(input); }