mirror of
https://github.com/orange-cpp/omath.git
synced 2026-04-19 10:23:27 +00:00
improved serialization
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
#include "omath/linear_algebra/vector3.hpp"
|
#include "omath/linear_algebra/vector3.hpp"
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace omath::pathfinding
|
namespace omath::pathfinding
|
||||||
@@ -28,9 +29,9 @@ namespace omath::pathfinding
|
|||||||
[[nodiscard]]
|
[[nodiscard]]
|
||||||
bool empty() const;
|
bool empty() const;
|
||||||
|
|
||||||
[[nodiscard]] std::vector<uint8_t> serialize() const noexcept;
|
[[nodiscard]] std::string serialize() const noexcept;
|
||||||
|
|
||||||
void deserialize(const std::vector<uint8_t>& raw) noexcept;
|
void deserialize(const std::string& raw);
|
||||||
|
|
||||||
std::unordered_map<Vector3<float>, std::vector<Vector3<float>>> m_vertex_map;
|
std::unordered_map<Vector3<float>, std::vector<Vector3<float>>> m_vertex_map;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
//
|
//
|
||||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <sstream>
|
||||||
#include <limits>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
namespace omath::pathfinding
|
namespace omath::pathfinding
|
||||||
{
|
{
|
||||||
@@ -30,76 +29,37 @@ namespace omath::pathfinding
|
|||||||
return m_vertex_map.empty();
|
return m_vertex_map.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> NavigationMesh::serialize() const noexcept
|
std::string NavigationMesh::serialize() const noexcept
|
||||||
{
|
{
|
||||||
std::vector<std::uint8_t> raw;
|
std::ostringstream oss;
|
||||||
|
|
||||||
// Pre-calculate total size for better performance
|
|
||||||
std::size_t total_size = 0;
|
|
||||||
for (const auto& [vertex, neighbors] : m_vertex_map)
|
for (const auto& [vertex, neighbors] : m_vertex_map)
|
||||||
{
|
{
|
||||||
total_size += sizeof(vertex) + sizeof(std::uint16_t) + sizeof(Vector3<float>) * neighbors.size();
|
oss << vertex.x << ' ' << vertex.y << ' ' << vertex.z
|
||||||
|
<< ' ' << neighbors.size() << '\n';
|
||||||
|
for (const auto& n : neighbors)
|
||||||
|
oss << n.x << ' ' << n.y << ' ' << n.z << '\n';
|
||||||
}
|
}
|
||||||
raw.reserve(total_size);
|
return oss.str();
|
||||||
|
|
||||||
auto dump_to_vector = [&raw]<typename T>(const T& t)
|
|
||||||
{
|
|
||||||
const auto* byte_ptr = reinterpret_cast<const std::uint8_t*>(&t);
|
|
||||||
raw.insert(raw.end(), byte_ptr, byte_ptr + sizeof(T));
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& [vertex, neighbors] : m_vertex_map)
|
|
||||||
{
|
|
||||||
// Clamp neighbors count to fit in uint16_t (prevents silent data corruption)
|
|
||||||
// NOTE: If neighbors.size() > 65535, only the first 65535 neighbors will be serialized.
|
|
||||||
// This is a limitation of the current serialization format using uint16_t for count.
|
|
||||||
const auto clamped_count =
|
|
||||||
std::min<std::size_t>(neighbors.size(), std::numeric_limits<std::uint16_t>::max());
|
|
||||||
const auto neighbors_count = static_cast<std::uint16_t>(clamped_count);
|
|
||||||
|
|
||||||
dump_to_vector(vertex);
|
|
||||||
dump_to_vector(neighbors_count);
|
|
||||||
|
|
||||||
// Only serialize up to the clamped count
|
|
||||||
for (std::size_t i = 0; i < clamped_count; ++i)
|
|
||||||
dump_to_vector(neighbors[i]);
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NavigationMesh::deserialize(const std::vector<uint8_t>& raw) noexcept
|
void NavigationMesh::deserialize(const std::string& raw)
|
||||||
{
|
{
|
||||||
auto load_from_vector = [](const std::vector<uint8_t>& vec, std::size_t& offset, auto& value)
|
|
||||||
{
|
|
||||||
if (offset + sizeof(value) > vec.size())
|
|
||||||
throw std::runtime_error("Deserialize: Invalid input data size.");
|
|
||||||
|
|
||||||
std::copy_n(vec.data() + offset, sizeof(value), reinterpret_cast<uint8_t*>(&value));
|
|
||||||
offset += sizeof(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
m_vertex_map.clear();
|
m_vertex_map.clear();
|
||||||
|
std::istringstream iss(raw);
|
||||||
|
|
||||||
std::size_t offset = 0;
|
Vector3<float> vertex;
|
||||||
|
std::size_t neighbors_count;
|
||||||
while (offset < raw.size())
|
while (iss >> vertex.x >> vertex.y >> vertex.z >> neighbors_count)
|
||||||
{
|
{
|
||||||
Vector3<float> vertex;
|
|
||||||
load_from_vector(raw, offset, vertex);
|
|
||||||
|
|
||||||
std::uint16_t neighbors_count;
|
|
||||||
load_from_vector(raw, offset, neighbors_count);
|
|
||||||
|
|
||||||
std::vector<Vector3<float>> neighbors;
|
std::vector<Vector3<float>> neighbors;
|
||||||
neighbors.reserve(neighbors_count);
|
neighbors.reserve(neighbors_count);
|
||||||
|
|
||||||
for (std::size_t i = 0; i < neighbors_count; ++i)
|
for (std::size_t i = 0; i < neighbors_count; ++i)
|
||||||
{
|
{
|
||||||
Vector3<float> neighbor;
|
Vector3<float> n;
|
||||||
load_from_vector(raw, offset, neighbor);
|
if (!(iss >> n.x >> n.y >> n.z))
|
||||||
neighbors.push_back(neighbor);
|
throw std::runtime_error("Deserialize: Unexpected end of data.");
|
||||||
|
neighbors.push_back(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_vertex_map.emplace(vertex, std::move(neighbors));
|
m_vertex_map.emplace(vertex, std::move(neighbors));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,18 @@ using namespace omath::pathfinding;
|
|||||||
TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
|
TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
|
||||||
{
|
{
|
||||||
NavigationMesh nav;
|
NavigationMesh nav;
|
||||||
Vector3<float> a{0.f,0.f,0.f};
|
Vector3<float> a{0.f, 0.f, 0.f};
|
||||||
Vector3<float> b{1.f,0.f,0.f};
|
Vector3<float> b{1.f, 0.f, 0.f};
|
||||||
Vector3<float> c{0.f,1.f,0.f};
|
Vector3<float> c{0.f, 1.f, 0.f};
|
||||||
|
|
||||||
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{b,c});
|
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{b, c});
|
||||||
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{a});
|
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{a});
|
||||||
nav.m_vertex_map.emplace(c, std::vector<Vector3<float>>{a});
|
nav.m_vertex_map.emplace(c, std::vector<Vector3<float>>{a});
|
||||||
|
|
||||||
auto data = nav.serialize();
|
std::string data = nav.serialize();
|
||||||
NavigationMesh nav2;
|
NavigationMesh nav2;
|
||||||
EXPECT_NO_THROW(nav2.deserialize(data));
|
EXPECT_NO_THROW(nav2.deserialize(data));
|
||||||
|
|
||||||
// verify neighbors preserved
|
|
||||||
EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size());
|
EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size());
|
||||||
EXPECT_EQ(nav2.get_neighbors(a).size(), 2u);
|
EXPECT_EQ(nav2.get_neighbors(a).size(), 2u);
|
||||||
}
|
}
|
||||||
@@ -27,7 +26,117 @@ TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
|
|||||||
TEST(NavigationMeshTests, GetClosestVertexWhenEmpty)
|
TEST(NavigationMeshTests, GetClosestVertexWhenEmpty)
|
||||||
{
|
{
|
||||||
const NavigationMesh nav;
|
const NavigationMesh nav;
|
||||||
constexpr Vector3<float> p{5.f,5.f,5.f};
|
constexpr Vector3<float> p{5.f, 5.f, 5.f};
|
||||||
const auto res = nav.get_closest_vertex(p);
|
const auto res = nav.get_closest_vertex(p);
|
||||||
EXPECT_FALSE(res.has_value());
|
EXPECT_FALSE(res.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, SerializeEmptyMesh)
|
||||||
|
{
|
||||||
|
const NavigationMesh nav;
|
||||||
|
const std::string data = nav.serialize();
|
||||||
|
EXPECT_TRUE(data.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, DeserializeEmptyString)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
EXPECT_NO_THROW(nav.deserialize(""));
|
||||||
|
EXPECT_TRUE(nav.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, SerializeProducesHumanReadableText)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
nav.m_vertex_map.emplace(Vector3<float>{1.f, 2.f, 3.f}, std::vector<Vector3<float>>{{4.f, 5.f, 6.f}});
|
||||||
|
|
||||||
|
const std::string data = nav.serialize();
|
||||||
|
|
||||||
|
// Must contain the vertex and neighbor coords as plain text
|
||||||
|
EXPECT_NE(data.find("1"), std::string::npos);
|
||||||
|
EXPECT_NE(data.find("2"), std::string::npos);
|
||||||
|
EXPECT_NE(data.find("3"), std::string::npos);
|
||||||
|
EXPECT_NE(data.find("4"), std::string::npos);
|
||||||
|
EXPECT_NE(data.find("5"), std::string::npos);
|
||||||
|
EXPECT_NE(data.find("6"), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, DeserializeRestoresNeighborValues)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
const Vector3<float> v{1.f, 2.f, 3.f};
|
||||||
|
const Vector3<float> n1{4.f, 5.f, 6.f};
|
||||||
|
const Vector3<float> n2{7.f, 8.f, 9.f};
|
||||||
|
nav.m_vertex_map.emplace(v, std::vector<Vector3<float>>{n1, n2});
|
||||||
|
|
||||||
|
NavigationMesh nav2;
|
||||||
|
nav2.deserialize(nav.serialize());
|
||||||
|
|
||||||
|
ASSERT_EQ(nav2.m_vertex_map.count(v), 1u);
|
||||||
|
const auto& neighbors = nav2.get_neighbors(v);
|
||||||
|
ASSERT_EQ(neighbors.size(), 2u);
|
||||||
|
EXPECT_EQ(neighbors[0], n1);
|
||||||
|
EXPECT_EQ(neighbors[1], n2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, DeserializeOverwritesPreviousData)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
const Vector3<float> v{1.f, 0.f, 0.f};
|
||||||
|
nav.m_vertex_map.emplace(v, std::vector<Vector3<float>>{});
|
||||||
|
|
||||||
|
// Load a different mesh into the same object
|
||||||
|
NavigationMesh other;
|
||||||
|
const Vector3<float> a{10.f, 20.f, 30.f};
|
||||||
|
other.m_vertex_map.emplace(a, std::vector<Vector3<float>>{});
|
||||||
|
|
||||||
|
nav.deserialize(other.serialize());
|
||||||
|
|
||||||
|
EXPECT_EQ(nav.m_vertex_map.size(), 1u);
|
||||||
|
EXPECT_EQ(nav.m_vertex_map.count(v), 0u);
|
||||||
|
EXPECT_EQ(nav.m_vertex_map.count(a), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, RoundTripNegativeAndFractionalCoords)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
const Vector3<float> v{-1.5f, 0.25f, -3.75f};
|
||||||
|
const Vector3<float> n{100.f, -200.f, 0.001f};
|
||||||
|
nav.m_vertex_map.emplace(v, std::vector<Vector3<float>>{n});
|
||||||
|
|
||||||
|
NavigationMesh nav2;
|
||||||
|
nav2.deserialize(nav.serialize());
|
||||||
|
|
||||||
|
ASSERT_EQ(nav2.m_vertex_map.count(v), 1u);
|
||||||
|
const auto& neighbors = nav2.get_neighbors(v);
|
||||||
|
ASSERT_EQ(neighbors.size(), 1u);
|
||||||
|
EXPECT_NEAR(neighbors[0].x, n.x, 1e-3f);
|
||||||
|
EXPECT_NEAR(neighbors[0].y, n.y, 1e-3f);
|
||||||
|
EXPECT_NEAR(neighbors[0].z, n.z, 1e-3f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, GetClosestVertexReturnsNearest)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
const Vector3<float> a{0.f, 0.f, 0.f};
|
||||||
|
const Vector3<float> b{10.f, 0.f, 0.f};
|
||||||
|
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{});
|
||||||
|
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{});
|
||||||
|
|
||||||
|
const auto res = nav.get_closest_vertex({1.f, 0.f, 0.f});
|
||||||
|
ASSERT_TRUE(res.has_value());
|
||||||
|
EXPECT_EQ(res.value(), a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavigationMeshTests, VertexWithNoNeighborsRoundTrip)
|
||||||
|
{
|
||||||
|
NavigationMesh nav;
|
||||||
|
const Vector3<float> v{5.f, 5.f, 5.f};
|
||||||
|
nav.m_vertex_map.emplace(v, std::vector<Vector3<float>>{});
|
||||||
|
|
||||||
|
NavigationMesh nav2;
|
||||||
|
nav2.deserialize(nav.serialize());
|
||||||
|
|
||||||
|
ASSERT_EQ(nav2.m_vertex_map.count(v), 1u);
|
||||||
|
EXPECT_TRUE(nav2.get_neighbors(v).empty());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user