From 9a16d8d0cf713a5c996ec00fe0ea58d67eda155e Mon Sep 17 00:00:00 2001 From: vinihcrosa Date: Sat, 27 Jun 2026 23:51:56 -0300 Subject: [PATCH] feat(simulation): add NavalDomain::serializeStateJson for WASM state export Returns {"t":...,"vessels":[{"id":...,"x":...,"psi":...,"u":...,"r":...},...]} 2 tests verify JSON structure and time accumulation. Co-Authored-By: Claude Sonnet 4.6 --- .../include/ymir/simulation/NavalDomain.h | 8 ++++ libs/simulation/src/NavalDomain.cpp | 33 +++++++++++++ tests/simulation/TestNavalDomain.cpp | 48 +++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/libs/simulation/include/ymir/simulation/NavalDomain.h b/libs/simulation/include/ymir/simulation/NavalDomain.h index 03de63b..92702f0 100644 --- a/libs/simulation/include/ymir/simulation/NavalDomain.h +++ b/libs/simulation/include/ymir/simulation/NavalDomain.h @@ -87,6 +87,14 @@ public: /** Return Euclidean distance between two registered bodies. Asserts both ids exist. */ double distanceBetween(int idA, int idB) const; + /** + * Serialize all body states to a JSON string. + * + * Format: {"t":,"vessels":[{"id":,"x":...,"y":...,"z":...,"phi":...,"theta":...,"psi":...,"u":...,"v":...,"r":...},...]} + * Used by the WASM binding to transfer state to JavaScript. + */ + std::string serializeStateJson() const; + NavalDomain(const NavalDomain&) = delete; NavalDomain& operator=(const NavalDomain&) = delete; NavalDomain(NavalDomain&&) = delete; diff --git a/libs/simulation/src/NavalDomain.cpp b/libs/simulation/src/NavalDomain.cpp index b7692f4..0dd990b 100644 --- a/libs/simulation/src/NavalDomain.cpp +++ b/libs/simulation/src/NavalDomain.cpp @@ -4,6 +4,7 @@ #include #include +#include #include namespace ymir { @@ -188,4 +189,36 @@ naval::NavalContext NavalDomain::buildContext(int id, const BodyEntry& entry, do env_->waterDepth(), env_->tide()}; } +std::string NavalDomain::serializeStateJson() const +{ + std::ostringstream os; + os << "{\"t\":" << sim_.time() << ",\"vessels\":["; + + bool first = true; + for (const auto& [id, entry] : entries_) + { + const BodyState s = sim_.state(id); + const Vector6& q = s.q(); + const Vector6& qd = s.qdot(); + + if (!first) os << ','; + first = false; + + os << "{\"id\":" << id + << ",\"x\":" << q[0] + << ",\"y\":" << q[1] + << ",\"z\":" << q[2] + << ",\"phi\":" << q[3] + << ",\"theta\":" << q[4] + << ",\"psi\":" << q[5] + << ",\"u\":" << qd[0] + << ",\"v\":" << qd[1] + << ",\"r\":" << qd[5] + << '}'; + } + + os << "]}"; + return os.str(); +} + } // namespace ymir diff --git a/tests/simulation/TestNavalDomain.cpp b/tests/simulation/TestNavalDomain.cpp index ee67adf..8b6fe0a 100644 --- a/tests/simulation/TestNavalDomain.cpp +++ b/tests/simulation/TestNavalDomain.cpp @@ -282,3 +282,51 @@ TEST_CASE("NavalDomain regression: matches NavalSimulation within 1e-8 over 1000 } } } + +// --------------------------------------------------------------------------- +// serializeStateJson +// --------------------------------------------------------------------------- + +TEST_CASE("NavalDomain: serializeStateJson produces valid JSON with expected keys", "[naval_domain]") +{ + ymir::Environment env; + ymir::CouplingRegistry coupling; + ymir::NavalDomain domain; + + ymir::Vector6 q0{}, qd0{}; + q0[0] = 5.0; q0[5] = 0.1; + domain.addBody(0, makeBody(0, 1e6, q0, qd0)); + domain.onAddedToWorld(env, coupling); + domain.initialize(); + domain.step(0.1); + + const std::string json = domain.serializeStateJson(); + + REQUIRE(json.find("\"t\":") != std::string::npos); + REQUIRE(json.find("\"vessels\":") != std::string::npos); + REQUIRE(json.find("\"id\":0") != std::string::npos); + REQUIRE(json.find("\"x\":") != std::string::npos); + REQUIRE(json.find("\"psi\":") != std::string::npos); + REQUIRE(json.find("\"u\":") != std::string::npos); + REQUIRE(json.find("\"r\":") != std::string::npos); + REQUIRE(json.front() == '{'); + REQUIRE(json.back() == '}'); +} + +TEST_CASE("NavalDomain: serializeStateJson t advances with steps", "[naval_domain]") +{ + ymir::Environment env; + ymir::CouplingRegistry coupling; + ymir::NavalDomain domain; + + domain.addBody(0, makeBody(0)); + domain.onAddedToWorld(env, coupling); + domain.initialize(); + domain.step(0.5); + domain.step(0.5); + + const std::string json = domain.serializeStateJson(); + const std::size_t tpos = json.find("\"t\":") + 4; + const double t = std::stod(json.substr(tpos)); + REQUIRE(t == Approx(1.0).margin(1e-9)); +}