Files
DarkflameServer/tests/dCommonTests/LxfmlTests.cpp
Aaron Kimbrell 17513a8390 fix: splitting and normalizing models moves every bone and rigid, in file order
Lxfml::NormalizePosition moved only the first bone of each part, and never
the rigid systems, so after a save a flexible part's other bones and every
Rigid kept their world positions while the rest of the model was moved to
its own origin. Now every Bone and every Rigid moves by the same amount, and
the box that places the model holds every bone. Split maps every bone of a
part too, so a rigid system holding only a later bone keeps its brick.

Also:
- a model with no readable bone is kept as it is at the origin instead of
  getting a center 10000 up
- the math is done in doubles and numbers are written as the shortest text
  that reads back as the same float (-0.4, not -0.400002; 12.1678, not
  12.1677)
- bricks and rigid systems come out in the file's order, so the same model
  always splits into the same bytes
- parts over 5 MB are normalized like any other (the input is capped at
  10 MB), and the loop's safety limit no longer drops every later model

The pivot is still snapped to the 0.8 grid (the bricks don't move in the
world; the model's position stays on the grid), which can put it half a
stud from the middle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00

537 lines
23 KiB
C++

#include "gtest/gtest.h"
#include "Lxfml.h"
#include "TinyXmlUtils.h"
#include "dCommonDependencies.h"
#include <fstream>
#include <sstream>
#include <unordered_set>
#include <filesystem>
#include <functional>
#include <map>
using namespace TinyXmlUtils;
static std::string ReadFile(const std::string& filename) {
std::ifstream in(filename, std::ios::in | std::ios::binary);
if (!in.is_open()) {
return "";
}
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
std::string SerializeElement(tinyxml2::XMLElement* elem) {
tinyxml2::XMLPrinter p;
elem->Accept(&p);
return std::string(p.CStr());
};
// A rigid system by what it holds (its rigids' refIDs and boneRefs): splitting moves its transformations
std::string RigidSystemKey(tinyxml2::XMLElement* rigidSystem) {
std::string key;
for (auto* rigid = rigidSystem->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
key += std::string(rigid->Attribute("refID") ? rigid->Attribute("refID") : "") + ":" + (rigid->Attribute("boneRefs") ? rigid->Attribute("boneRefs") : "") + ";";
}
for (auto* joint = rigidSystem->FirstChildElement("Joint"); joint; joint = joint->NextSiblingElement("Joint")) key += SerializeElement(joint);
return key;
}
// Helper function to test splitting functionality
static void TestSplitUsesAllBricksAndNoDuplicatesHelper(const std::string& filename) {
// Read the LXFML file
std::string data = ReadFile(filename);
ASSERT_FALSE(data.empty()) << "Failed to read " << filename << " from build directory";
std::cout << "\n=== Testing LXFML splitting for: " << filename << " ===" << std::endl;
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0) << "Split results should not be empty for " << filename;
std::cout << "Split produced " << results.size() << " output(s)" << std::endl;
// parse original to count bricks
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS) << "Failed to parse " << filename;
DocumentReader reader(doc);
auto lxfml = reader["LXFML"];
ASSERT_TRUE(lxfml) << "No LXFML element found in " << filename;
std::unordered_set<std::string> originalRigidSet;
if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
originalRigidSet.insert(RigidSystemKey(rs));
}
}
std::unordered_set<std::string> originalGroupSet;
if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) {
for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
// collect this group and nested groups
std::function<void(tinyxml2::XMLElement*)> collectGroups = [&](tinyxml2::XMLElement* grp) {
originalGroupSet.insert(SerializeElement(grp));
for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child);
};
collectGroups(g);
}
}
}
std::unordered_set<std::string> originalBricks;
for (const auto& brick : lxfml["Bricks"]) {
const auto* ref = brick.Attribute("refID");
if (ref) originalBricks.insert(ref);
}
ASSERT_GT(originalBricks.size(), 0);
// Collect bricks across all results and ensure no duplicates and all used
std::unordered_set<std::string> usedBricks;
// Track used rigid systems and groups (serialized strings)
std::unordered_set<std::string> usedRigidSet;
std::unordered_set<std::string> usedGroupSet;
std::cout << "Original file contains " << originalBricks.size() << " bricks: ";
for (const auto& brick : originalBricks) {
std::cout << brick << " ";
}
std::cout << std::endl;
int splitIndex = 0;
std::filesystem::path baseFilename = std::filesystem::path(filename).stem();
for (const auto& res : results) {
splitIndex++;
std::cout << "\n--- Split " << splitIndex << " ---" << std::endl;
tinyxml2::XMLDocument outDoc;
ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS);
DocumentReader outReader(outDoc);
auto outLxfml = outReader["LXFML"];
ASSERT_TRUE(outLxfml);
// collect rigid systems in this output
if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
auto s = RigidSystemKey(rs);
// no duplicate allowed across outputs
ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits";
usedRigidSet.insert(s);
}
}
// collect groups in this output
if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) {
for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
std::function<void(tinyxml2::XMLElement*)> collectGroupsOut = [&](tinyxml2::XMLElement* grp) {
auto s = SerializeElement(grp);
ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits";
usedGroupSet.insert(s);
for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child);
};
collectGroupsOut(g);
}
}
}
// Collect and display bricks in this split
std::vector<std::string> splitBricks;
for (const auto& brick : outLxfml["Bricks"]) {
const auto* ref = brick.Attribute("refID");
if (ref) {
// no duplicate allowed
ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref;
usedBricks.insert(ref);
splitBricks.push_back(ref);
}
}
std::cout << "Contains " << splitBricks.size() << " bricks: ";
for (const auto& brick : splitBricks) {
std::cout << brick << " ";
}
std::cout << std::endl;
// Count rigid systems and groups
int rigidCount = 0;
if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidCount++;
}
}
int groupCount = 0;
if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) {
for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
groupCount++;
}
}
}
std::cout << "Contains " << rigidCount << " rigid systems and " << groupCount << " groups" << std::endl;
}
// Every original brick must be used in one of the outputs
for (const auto& bref : originalBricks) {
ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref << " in " << filename;
}
// And usedBricks should not contain anything outside original
for (const auto& ub : usedBricks) {
ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub << " in " << filename;
}
// Ensure all original rigid systems and groups were used exactly once
ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch in " << filename;
for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits in " << filename;
ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch in " << filename;
for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits in " << filename;
}
TEST(LxfmlTests, SplitGroupIssueFile) {
// Specific test for the group issue file
TestSplitUsesAllBricksAndNoDuplicatesHelper("group_issue.lxfml");
}
TEST(LxfmlTests, SplitTestFile) {
// Specific test for the larger test file
TestSplitUsesAllBricksAndNoDuplicatesHelper("test.lxfml");
}
TEST(LxfmlTests, SplitComplexGroupingFile) {
// Test for the complex grouping file - should produce only one split
// because all groups are connected via rigid systems
std::string data = ReadFile("complex_grouping.lxfml");
ASSERT_FALSE(data.empty()) << "Failed to read complex_grouping.lxfml from build directory";
std::cout << "\n=== Testing complex grouping file ===" << std::endl;
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0) << "Split results should not be empty";
// The complex grouping file should produce exactly ONE split
// because all groups share bricks through rigid systems
if (results.size() != 1) {
FAIL() << "Complex grouping file produced " << results.size()
<< " splits instead of 1 (all groups should be merged)";
}
std::cout << "✓ Correctly produced 1 merged split" << std::endl;
// Verify the split contains all the expected elements
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(results[0].lxfml.c_str()), tinyxml2::XML_SUCCESS);
auto* lxfml = doc.FirstChildElement("LXFML");
ASSERT_NE(lxfml, nullptr);
// Count bricks
int brickCount = 0;
if (auto* bricks = lxfml->FirstChildElement("Bricks")) {
for (auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
brickCount++;
}
}
std::cout << "Contains " << brickCount << " bricks" << std::endl;
// Count rigid systems
int rigidCount = 0;
if (auto* rigidSystems = lxfml->FirstChildElement("RigidSystems")) {
for (auto* rs = rigidSystems->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidCount++;
}
}
std::cout << "Contains " << rigidCount << " rigid systems" << std::endl;
EXPECT_GT(rigidCount, 0) << "Should contain rigid systems";
// Count groups
int groupCount = 0;
if (auto* groupSystems = lxfml->FirstChildElement("GroupSystems")) {
for (auto* gs = groupSystems->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
groupCount++;
}
}
}
std::cout << "Contains " << groupCount << " groups" << std::endl;
EXPECT_GT(groupCount, 1) << "Should contain multiple groups (all merged into one split)";
}
// Tests for invalid input handling - now working with the improved Split function
TEST(LxfmlTests, InvalidLxfmlHandling) {
// Test LXFML with invalid transformation matrices
std::string invalidTransformData = ReadFile("invalid_transform.lxfml");
ASSERT_FALSE(invalidTransformData.empty()) << "Failed to read invalid_transform.lxfml from build directory";
// The Split function should handle invalid transformation matrices gracefully
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(invalidTransformData);
}) << "Split should not crash on invalid transformation matrices";
// Function should handle invalid transforms gracefully, possibly returning empty or partial results
// The exact behavior depends on how the function handles invalid numeric parsing
}
TEST(LxfmlTests, EmptyLxfmlHandling) {
// Test with completely empty input
std::string emptyData = "";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(emptyData);
}) << "Split should not crash on empty input";
EXPECT_EQ(results.size(), 0) << "Empty input should return empty results";
}
TEST(LxfmlTests, EmptyTransformHandling) {
// Test LXFML with empty transformation matrix
std::string testData = ReadFile("empty_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read empty_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on empty transformation matrix";
// The function should handle empty transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, TooFewValuesTransformHandling) {
// Test LXFML with too few transformation values (needs 12, has fewer)
std::string testData = ReadFile("too_few_values.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read too_few_values.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on transformation matrix with too few values";
// The function should handle incomplete transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, NonNumericTransformHandling) {
// Test LXFML with non-numeric transformation values
std::string testData = ReadFile("non_numeric_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read non_numeric_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on non-numeric transformation values";
// The function should handle non-numeric transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, MixedInvalidTransformHandling) {
// Test LXFML with mixed valid/invalid transformation values within a matrix
std::string testData = ReadFile("mixed_invalid_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read mixed_invalid_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on mixed valid/invalid transformation values";
// The function should handle mixed valid/invalid transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, NoBricksHandling) {
// Test LXFML with no Bricks section (should return empty gracefully)
std::string testData = ReadFile("no_bricks.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read no_bricks.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on LXFML with no Bricks section";
// Should return empty results gracefully when no bricks are present
EXPECT_EQ(results.size(), 0) << "LXFML with no bricks should return empty results";
}
TEST(LxfmlTests, MixedValidInvalidTransformsHandling) {
// Test LXFML with mix of valid and invalid transformation data
std::string mixedValidData = ReadFile("mixed_valid_invalid.lxfml");
ASSERT_FALSE(mixedValidData.empty()) << "Failed to read mixed_valid_invalid.lxfml from build directory";
// The Split function should handle mixed valid/invalid transforms gracefully
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(mixedValidData);
}) << "Split should not crash on mixed valid/invalid transforms";
// Should process valid bricks and handle invalid ones gracefully
if (results.size() > 0) {
EXPECT_NO_FATAL_FAILURE({
for (size_t i = 0; i < results.size(); ++i) {
// Each result should have valid LXFML structure
tinyxml2::XMLDocument doc;
auto parseResult = doc.Parse(results[i].lxfml.c_str());
EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS)
<< "Result " << i << " should produce valid XML";
if (parseResult == tinyxml2::XML_SUCCESS) {
auto* lxfml = doc.FirstChildElement("LXFML");
EXPECT_NE(lxfml, nullptr) << "Result " << i << " should have LXFML root element";
}
}
}) << "Mixed valid/invalid transform processing should not cause fatal errors";
}
}
TEST(LxfmlTests, DeepCloneDepthProtection) {
// Test that deep cloning has protection against excessive nesting
std::string deeplyNestedLxfml = ReadFile("deeply_nested.lxfml");
ASSERT_FALSE(deeplyNestedLxfml.empty()) << "Failed to read deeply_nested.lxfml from build directory";
// The Split function should handle deeply nested structures without hanging
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(deeplyNestedLxfml);
}) << "Split should not hang or crash on deeply nested XML structures";
// Should still produce valid output despite depth limitations
EXPECT_GT(results.size(), 0) << "Should produce at least one result even with deep nesting";
if (results.size() > 0) {
// Verify the result is still valid XML
tinyxml2::XMLDocument doc;
auto parseResult = doc.Parse(results[0].lxfml.c_str());
EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS) << "Result should still be valid XML";
if (parseResult == tinyxml2::XML_SUCCESS) {
auto* lxfml = doc.FirstChildElement("LXFML");
EXPECT_NE(lxfml, nullptr) << "Result should have LXFML root element";
// Verify that bricks are still included despite group nesting issues
auto* bricks = lxfml->FirstChildElement("Bricks");
EXPECT_NE(bricks, nullptr) << "Bricks element should be present";
if (bricks) {
auto* brick = bricks->FirstChildElement("Brick");
EXPECT_NE(brick, nullptr) << "At least one brick should be present";
}
}
}
}
TEST(LxfmlTests, ReadContentsListsBricksAndTheirBox) {
const std::string lxfml = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Bricks>
<Brick refID="0" designID="3710"><Part refID="0" designID="3710" materials="21"><Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,-1.2,0,0.4"/></Part></Brick>
<Brick refID="1" designID="3001;A"><Part refID="1" designID="3001" materials="1"><Bone refID="1" transformation="1,0,0,0,1,0,0,0,1,0.8,0.96,-0.4"/></Part></Brick>
</Bricks>
</LXFML>)";
const auto contents = Lxfml::ReadContents(lxfml);
EXPECT_EQ(contents.designIds, (std::vector<uint32_t>{ 3710, 3001 }));
EXPECT_EQ(contents.boxMin, NiPoint3(-1.2f, 0.0f, -0.4f));
EXPECT_EQ(contents.boxMax, NiPoint3(0.8f, 0.96f, 0.4f));
const auto nothing = Lxfml::ReadContents("");
EXPECT_TRUE(nothing.designIds.empty());
EXPECT_EQ(nothing.boxMin, NiPoint3Constant::ZERO);
EXPECT_TRUE(Lxfml::ReadContents("<LXFML><Bricks/></LXFML>").designIds.empty());
}
namespace {
const std::string PROBE_HEAD = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta><Bricks>)";
// Two models: bricks 0 and 1 in one rigid system at x 100.4; a flexible part (two bones) and an upside down brick,
// joined in another rigid system, around x 1222
const std::string TWO_MODELS = PROBE_HEAD +
R"(<Brick refID="0" designID="3001"><Part refID="0" designID="3001" materials="21"><Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,100.4,433.92,-62.8"/></Part></Brick>)"
R"(<Brick refID="1" designID="3001"><Part refID="1" designID="3001" materials="21"><Bone refID="1" transformation="1,0,0,0,1,0,0,0,1,100.4,434.88,-62.8"/></Part></Brick>)"
R"(<Brick refID="2" designID="73590"><Part refID="2" designID="73590" materials="26"><Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,1234.4,433.92,-10.8"/><Bone refID="3" transformation="1,0,0,0,1,0,0,0,1,1236.8,433.92,-10.8"/></Part></Brick>)"
R"(<Brick refID="3" designID="3001"><Part refID="4" designID="3001" materials="1"><Bone refID="4" transformation="1,0,0,0,-1,0,0,0,-1,1210,433.92,-10.8"/></Part></Brick>)"
R"(</Bricks><RigidSystems>)"
R"(<RigidSystem><Rigid refID="0" transformation="1,0,0,0,1,0,0,0,1,100.4,433.92,-62.8" boneRefs="0,1"/></RigidSystem>)"
R"(<RigidSystem><Rigid refID="1" transformation="1,0,0,0,1,0,0,0,1,1234.4,433.92,-10.8" boneRefs="2,3"/><Rigid refID="2" transformation="1,0,0,0,-1,0,0,0,-1,1210,433.92,-10.8" boneRefs="4"/></RigidSystem>)"
R"(</RigidSystems><GroupSystems><GroupSystem></GroupSystem></GroupSystems></LXFML>)";
// refID -> transformation of every element named `name`
std::map<std::string, std::string> Transformations(const std::string& lxfml, const char* name) {
tinyxml2::XMLDocument doc;
doc.Parse(lxfml.c_str());
std::map<std::string, std::string> out;
std::function<void(const tinyxml2::XMLElement*)> walk = [&](const tinyxml2::XMLElement* element) {
for (; element; element = element->NextSiblingElement()) {
if (std::string(element->Name()) == name) out[element->Attribute("refID")] = element->Attribute("transformation");
walk(element->FirstChildElement());
}
};
walk(doc.FirstChildElement());
return out;
}
std::vector<std::string> BrickOrder(const std::string& lxfml) {
tinyxml2::XMLDocument doc;
doc.Parse(lxfml.c_str());
std::vector<std::string> out;
for (auto* brick = doc.FirstChildElement("LXFML")->FirstChildElement("Bricks")->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) out.push_back(brick->Attribute("refID"));
return out;
}
}
TEST(LxfmlTests, SplitMovesRigidsWithTheirBones) {
const auto results = Lxfml::Split(TWO_MODELS);
ASSERT_EQ(results.size(), 2);
EXPECT_EQ(results[0].center, NiPoint3(100.8f, 433.92f, -62.4f));
const auto bones = Transformations(results[0].lxfml, "Bone");
EXPECT_EQ(bones.at("0"), "1,0,0,0,1,0,0,0,1,-0.4,0,-0.4");
EXPECT_EQ(bones.at("1"), "1,0,0,0,1,0,0,0,1,-0.4,0.96,-0.4");
// The rigid system sits where its first bone does, as the game writes it
EXPECT_EQ(Transformations(results[0].lxfml, "Rigid").at("0"), bones.at("0"));
}
TEST(LxfmlTests, SplitMovesEveryBoneOfAFlexiblePart) {
const auto results = Lxfml::Split(TWO_MODELS);
ASSERT_EQ(results.size(), 2);
// The box of every bone (1210 to 1236.8): centred at 1223.4, snapped to the grid at 1223.2 (z -10.8, halfway, to the even -11.2)
EXPECT_EQ(results[1].center, NiPoint3(1223.2f, 433.92f, -11.2f));
const auto bones = Transformations(results[1].lxfml, "Bone");
EXPECT_EQ(bones.at("2"), "1,0,0,0,1,0,0,0,1,11.2,0,0.4");
EXPECT_EQ(bones.at("3"), "1,0,0,0,1,0,0,0,1,13.6,0,0.4");
EXPECT_EQ(bones.at("4"), "1,0,0,0,-1,0,0,0,-1,-13.2,0,0.4");
const auto rigids = Transformations(results[1].lxfml, "Rigid");
EXPECT_EQ(rigids.at("1"), bones.at("2"));
EXPECT_EQ(rigids.at("2"), bones.at("4"));
}
TEST(LxfmlTests, SplitKeepsTheFileOrder) {
const auto results = Lxfml::Split(TWO_MODELS);
ASSERT_EQ(results.size(), 2);
EXPECT_EQ(BrickOrder(results[0].lxfml), (std::vector<std::string>{ "0", "1" }));
EXPECT_EQ(BrickOrder(results[1].lxfml), (std::vector<std::string>{ "2", "3" }));
// And the same model always comes out the same
EXPECT_EQ(Lxfml::Split(TWO_MODELS)[1].lxfml, results[1].lxfml);
}
TEST(LxfmlTests, NormalizeWithoutReadableBonesKeepsTheModel) {
const std::string lxfml = PROBE_HEAD + R"(<Brick refID="0" designID="3001"><Part refID="0" designID="3001"><Bone refID="0" transformation=""/></Part></Brick></Bricks></LXFML>)";
const auto result = Lxfml::NormalizePosition(lxfml);
// Not somewhere far above the world: left as it is, at the origin
EXPECT_EQ(result.center, NiPoint3Constant::ZERO);
EXPECT_EQ(result.lxfml, lxfml);
}
TEST(LxfmlTests, NormalizeAtAPositionKeepsTheBricksInPlace) {
// Bones already relative to the model; the given position is snapped to the grid and the bones make up for it
const std::string lxfml = PROBE_HEAD +
R"(<Brick refID="0" designID="3001"><Part refID="0" designID="3001"><Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick></Bricks>)"
R"(<RigidSystems><RigidSystem><Rigid refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0" boneRefs="0"/></RigidSystem></RigidSystems></LXFML>)";
const auto result = Lxfml::NormalizePosition(lxfml, NiPoint3(10.5f, 5.0f, -3.0f));
EXPECT_EQ(result.center, NiPoint3(10.4f, 5.0f, -3.2f));
EXPECT_EQ(Transformations(result.lxfml, "Bone").at("0"), "1,0,0,0,1,0,0,0,1,0.1,0,0.2");
EXPECT_EQ(Transformations(result.lxfml, "Rigid").at("0"), "1,0,0,0,1,0,0,0,1,0.1,0,0.2");
}