feat: add vec3 type to nhollman json / fix: scene serialisation stuff

This commit is contained in:
2026-10-18 15:20:01 +02:00
parent 7223e9ce07
commit 94fa2bb4b2
18 changed files with 670 additions and 289 deletions
+138
View File
@@ -23,6 +23,7 @@
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h>
#include <destrum/Serialization/GlmJson.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <destrum/Physics/SimplePhysicsWorld.h>
#include <destrum/Physics/JoltPhysicsWorld.h>
@@ -1341,6 +1342,137 @@ namespace {
SceneManager::GetInstance().Destroy();
}
std::string readFile(const std::filesystem::path& path)
{
std::ifstream file(path, std::ios::binary);
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
void testSceneSaveRejectsNonFinite()
{
Scene& scene = SceneManager::GetInstance().CreateScene("non-finite");
GameObject* object = scene.CreateGameObject("object");
scene.CommitPendingAdditions();
const auto path = std::filesystem::temp_directory_path() / "destrum_non_finite_scene.json";
check(SceneSerializer::Save(scene, path), "a finite scene must save");
const std::string goodContents = readFile(path);
object->GetTransform().SetLocalPosition({std::nanf(""), 0.0f, 0.0f});
check(!SceneSerializer::Save(scene, path), "a scene with NaN values must not save");
check(readFile(path) == goodContents, "a failed save must leave the previous file untouched");
check(!std::filesystem::exists(std::filesystem::path{path} += ".tmp"),
"a failed save must not leave its temp file behind");
std::filesystem::remove(path);
SceneManager::GetInstance().Destroy();
}
void testSceneValidationErrors()
{
const auto path = std::filesystem::temp_directory_path() / "destrum_invalid_scene.json";
std::ofstream{path}
<< R"({"version":2,"name":"bad","objects":[{"id":7,"name":"Lamp","transform":{"position":[0,0,0],"rotation":[0,0,0,1],"scale":[1,1,1]},"parent":0,"components":[{"type":"NotAComponent","enabled":true,"data":{}}]}]})";
const auto result = SceneSerializer::LoadSceneFile(path);
check(!result.success, "a scene with an unknown component must not load");
check(result.errorMessage.find("\"Lamp\"") != std::string::npos &&
result.errorMessage.find("NotAComponent") != std::string::npos &&
result.errorMessage.find("unknown component type") != std::string::npos,
"scene validation errors must name the object, component and problem");
std::ofstream{path}
<< R"({"version":2,"objects":[{"id":7,"name":"Lamp","components":[{"type":"Spinner","data":["message","hi"]}]}]})";
const auto arrayData = SceneSerializer::LoadSceneFile(path);
check(!arrayData.success && arrayData.errorMessage.find("must be an object") != std::string::npos,
"component data that is not an object must be rejected with a reason");
std::filesystem::remove(path);
}
void testSceneLoadRestoresBindings()
{
Scene& scene = SceneManager::GetInstance().CreateScene("bindings");
int registered = 0;
int unregistered = 0;
scene.SetRegisterBindings([&registered] { ++registered; });
scene.SetUnregisterBindings([&unregistered] { ++unregistered; });
scene.Load();
const int registeredBefore = registered;
const int unregisteredBefore = unregistered;
scene.CreateGameObject("object");
scene.CommitPendingAdditions();
const auto path = std::filesystem::temp_directory_path() / "destrum_bindings_scene.json";
check(SceneSerializer::Save(scene, path), "the bindings scene must save");
check(SceneSerializer::Load(scene, path), "the bindings scene must load");
std::filesystem::remove(path);
check(registered - registeredBefore == 1 && unregistered - unregisteredBefore == 1, "loading a scene in place must register its bindings again");
scene.SetRegisterBindings(nullptr);
scene.SetUnregisterBindings(nullptr);
SceneManager::GetInstance().Destroy();
}
void testSceneLoadDropsMissingCamera()
{
Scene& scene = SceneManager::GetInstance().CreateScene("missing camera");
const auto path = std::filesystem::temp_directory_path() / "destrum_missing_camera_scene.json";
std::ofstream{path}
<< R"({"version":2,"currentCamera":4242,"objects":[{"id":11,"name":"object"}]})";
check(SceneSerializer::Load(scene, path), "a scene with a dangling camera id must still load");
check(scene.GetCurrentCamera() == nullptr, "a dangling current camera id must be ignored");
std::filesystem::remove(path);
SceneManager::GetInstance().Destroy();
}
void testGlmJson()
{
const nlohmann::json vec = glm::vec3{1.0f, 2.5f, -3.0f};
check(vec == nlohmann::json::array({1.0f, 2.5f, -3.0f}), "vec3 must serialize as a 3 number array");
check(vec.get<glm::vec3>() == glm::vec3{1.0f, 2.5f, -3.0f}, "vec3 must round-trip through json");
const glm::quat rotation = glm::angleAxis(0.5f, glm::vec3{0.0f, 1.0f, 0.0f});
const nlohmann::json quat = rotation;
check(quat.size() == 4 && close(quat.at(3).get<float>(), rotation.w),
"quaternions must serialize as [x, y, z, w]");
check(quat.get<glm::quat>() == rotation, "quaternions must round-trip through json");
bool rejected = false;
try {
(void)nlohmann::json::array({1.0f, 2.0f}).get<glm::vec3>();
} catch (const std::exception&) {
rejected = true;
}
check(rejected, "a vec3 must reject arrays of the wrong size");
const nlohmann::json data = {{"axis", nlohmann::json::array({0, 1, 0})}};
check(data.value("axis", glm::vec3{0.0f}) == glm::vec3{0.0f, 1.0f, 0.0f} &&
data.value("missing", glm::vec3{7.0f}) == glm::vec3{7.0f},
"json value() must work with glm defaults");
}
void testPrimitiveMeshKeys()
{
const glm::vec3 size{0.35f, 0.12f, 3.0f};
const std::string key = PrimitiveMeshes::BoxKey(size);
check(PrimitiveMeshes::IsPrimitiveKey(key), "box keys must be primitive keys");
check(PrimitiveMeshes::MakeBox(size).name == key, "boxes must be named by their key");
const auto rebuilt = PrimitiveMeshes::MakeFromKey(key + "#3");
check(rebuilt.has_value() && rebuilt->maxPos == size * 0.5f,
"a box must rebuild exactly from its key, ignoring the duplicate suffix");
check(PrimitiveMeshes::StripDuplicateSuffix("BulbMaterial#12") == "BulbMaterial" &&
PrimitiveMeshes::StripDuplicateSuffix("game://a.glb#mesh") == "game://a.glb#mesh",
"only numeric duplicate suffixes must be stripped");
check(!PrimitiveMeshes::MakeFromKey("prim://box?size=1,2").has_value() &&
!PrimitiveMeshes::MakeFromKey("prim://box?size=1,-2,3").has_value() &&
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
"invalid primitive keys must not build a mesh");
}
}
int main()
@@ -1380,6 +1512,12 @@ int main()
testKinematicBodiesMoveWithVelocity();
testRenderer2DShapes();
testFontAtlasAndLayout();
testSceneSaveRejectsNonFinite();
testSceneValidationErrors();
testSceneLoadRestoresBindings();
testSceneLoadDropsMissingCamera();
testGlmJson();
testPrimitiveMeshKeys();
std::cout << "destrum tests passed\n";
return EXIT_SUCCESS;
}