LXFML SPLITTING

Included test file
This commit is contained in:
Aaron Kimbrell
2025-09-13 20:49:29 -05:00
parent 68f2e2dee2
commit 25848349a3
5 changed files with 773 additions and 0 deletions

View File

@@ -5,6 +5,10 @@
#include "TinyXmlUtils.h"
#include <ranges>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <sstream>
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
Result toReturn;
@@ -128,3 +132,270 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
toReturn.center = newRootPos;
return toReturn;
}
// Deep-clone an XMLElement (attributes, text, and child elements) into a target document
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc) {
if (!src) return nullptr;
auto* dst = dstDoc.NewElement(src->Name());
// copy attributes
for (const tinyxml2::XMLAttribute* attr = src->FirstAttribute(); attr; attr = attr->Next()) {
dst->SetAttribute(attr->Name(), attr->Value());
}
// copy children (elements and text)
for (const tinyxml2::XMLNode* child = src->FirstChild(); child; child = child->NextSibling()) {
if (const tinyxml2::XMLElement* childElem = child->ToElement()) {
dst->InsertEndChild(CloneElementDeep(childElem, dstDoc));
} else if (const tinyxml2::XMLText* txt = child->ToText()) {
auto* n = dstDoc.NewText(txt->Value());
dst->InsertEndChild(n);
} else if (const tinyxml2::XMLComment* c = child->ToComment()) {
auto* n = dstDoc.NewComment(c->Value());
dst->InsertEndChild(n);
}
}
return dst;
}
std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
std::vector<Result> results;
tinyxml2::XMLDocument doc;
const auto err = doc.Parse(data.data());
if (err != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
return results;
}
auto* lxfml = doc.FirstChildElement("LXFML");
if (!lxfml) {
LOG("Failed to find LXFML element.");
return results;
}
// Build maps: partRef -> Part element, partRef -> Brick element, boneRef -> partRef, brickRef -> Brick element
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToPart;
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToBrick;
std::unordered_map<std::string, std::string> boneRefToPartRef;
std::unordered_map<std::string, tinyxml2::XMLElement*> brickByRef;
auto* bricksParent = lxfml->FirstChildElement("Bricks");
if (bricksParent) {
for (auto* brick = bricksParent->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
const char* brickRef = brick->Attribute("refID");
if (brickRef) brickByRef.emplace(std::string(brickRef), brick);
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
const char* partRef = part->Attribute("refID");
if (partRef) {
partRefToPart.emplace(std::string(partRef), part);
partRefToBrick.emplace(std::string(partRef), brick);
}
auto* bone = part->FirstChildElement("Bone");
if (bone) {
const char* boneRef = bone->Attribute("refID");
if (boneRef) boneRefToPartRef.emplace(std::string(boneRef), partRef ? std::string(partRef) : std::string());
}
}
}
}
// Collect RigidSystem elements
std::vector<tinyxml2::XMLElement*> rigidSystems;
auto* rigidSystemsParent = lxfml->FirstChildElement("RigidSystems");
if (rigidSystemsParent) {
for (auto* rs = rigidSystemsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidSystems.push_back(rs);
}
}
// Collect top-level groups (immediate children of GroupSystem)
std::vector<tinyxml2::XMLElement*> groupRoots;
auto* groupSystemsParent = lxfml->FirstChildElement("GroupSystems");
if (groupSystemsParent) {
for (auto* gs = groupSystemsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* group = gs->FirstChildElement("Group"); group; group = group->NextSiblingElement("Group")) {
groupRoots.push_back(group);
}
}
}
// Track used bricks and rigidsystems
std::unordered_set<std::string> usedBrickRefs;
std::unordered_set<tinyxml2::XMLElement*> usedRigidSystems;
auto splitListAttr = [](const char* attr) {
std::vector<std::string> out;
if (!attr) return out;
std::stringstream ss(attr);
std::string token;
while (std::getline(ss, token, ',')) {
if (!token.empty()) out.push_back(token);
}
return out;
};
// Helper to create output document from sets of brick refs and rigidsystem pointers
auto makeOutput = [&](const std::unordered_set<std::string>& bricksToInclude, const std::vector<tinyxml2::XMLElement*>& rigidSystemsToInclude, const std::vector<tinyxml2::XMLElement*>& groupsToInclude = {}) {
tinyxml2::XMLDocument outDoc;
outDoc.Parse(Lxfml::base.c_str());
auto* outRoot = outDoc.FirstChildElement("LXFML");
auto* outBricks = outRoot->FirstChildElement("Bricks");
auto* outRigidSystems = outRoot->FirstChildElement("RigidSystems");
auto* outGroupSystems = outRoot->FirstChildElement("GroupSystems");
// clone and insert bricks
for (const auto& bref : bricksToInclude) {
auto it = brickByRef.find(bref);
if (it == brickByRef.end()) continue;
tinyxml2::XMLElement* cloned = CloneElementDeep(it->second, outDoc);
if (cloned) outBricks->InsertEndChild(cloned);
}
// clone and insert rigidsystems
for (auto* rsPtr : rigidSystemsToInclude) {
tinyxml2::XMLElement* cloned = CloneElementDeep(rsPtr, outDoc);
if (cloned) outRigidSystems->InsertEndChild(cloned);
}
// clone and insert group(s) if requested
if (outGroupSystems && !groupsToInclude.empty()) {
// clear default children
while (outGroupSystems->FirstChild()) outGroupSystems->DeleteChild(outGroupSystems->FirstChild());
// create a GroupSystem element and append requested groups
auto* newGS = outDoc.NewElement("GroupSystem");
for (auto* gptr : groupsToInclude) {
tinyxml2::XMLElement* clonedG = CloneElementDeep(gptr, outDoc);
if (clonedG) newGS->InsertEndChild(clonedG);
}
outGroupSystems->InsertEndChild(newGS);
}
// Print to string
tinyxml2::XMLPrinter printer;
outDoc.Print(&printer);
// Normalize position and compute center using existing helper
auto normalized = NormalizePosition(printer.CStr(), curPosition);
return normalized;
};
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
for (auto* groupRoot : groupRoots) {
// collect all partRefs in this group's subtree
std::unordered_set<std::string> partRefs;
std::function<void(const tinyxml2::XMLElement*)> collectParts = [&](const tinyxml2::XMLElement* g) {
if (!g) return;
const char* partAttr = g->Attribute("partRefs");
if (partAttr) {
for (auto& tok : splitListAttr(partAttr)) partRefs.insert(tok);
}
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child);
};
collectParts(groupRoot);
// Build initial sets of bricks and boneRefs
std::unordered_set<std::string> bricksIncluded;
std::unordered_set<std::string> boneRefsIncluded;
for (const auto& pref : partRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
// Iteratively include any RigidSystems that reference any boneRefsIncluded
bool changed = true;
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
while (changed) {
changed = false;
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
// parse boneRefs of this rigid system (from its <Rigid> children)
bool intersects = false;
std::vector<std::string> rsBoneRefs;
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : splitListAttr(battr)) {
rsBoneRefs.push_back(tok);
if (boneRefsIncluded.find(tok) != boneRefsIncluded.end()) intersects = true;
}
}
if (!intersects) continue;
// include this rigid system and all boneRefs it references
usedRigidSystems.insert(rs);
rigidSystemsToInclude.push_back(rs);
for (const auto& br : rsBoneRefs) {
boneRefsIncluded.insert(br);
auto bpIt = boneRefToPartRef.find(br);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref && bricksIncluded.insert(std::string(bref)).second) changed = true;
}
}
}
// also include bricks for any newly discovered parts
}
}
// include bricks from bricksIncluded into used set
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
// make output doc and push result (include this group's XML)
std::vector<tinyxml2::XMLElement*> groupsVec{ groupRoot };
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsVec);
results.push_back(normalized);
}
// 2) Process remaining RigidSystems (each becomes its own file)
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
std::unordered_set<std::string> bricksIncluded;
// collect boneRefs referenced by this rigid system
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : splitListAttr(battr)) {
auto bpIt = boneRefToPartRef.find(tok);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
}
}
}
// mark used
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
usedRigidSystems.insert(rs);
std::vector<tinyxml2::XMLElement*> rsVec{ rs };
auto normalized = makeOutput(bricksIncluded, rsVec);
results.push_back(normalized);
}
// 3) Any remaining bricks not included become their own files
for (const auto& [bref, brickPtr] : brickByRef) {
if (usedBrickRefs.find(bref) != usedBrickRefs.end()) continue;
std::unordered_set<std::string> bricksIncluded{ bref };
auto normalized = makeOutput(bricksIncluded, {});
results.push_back(normalized);
usedBrickRefs.insert(bref);
}
return results;
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include <string_view>
#include <vector>
#include "NiPoint3.h"
@@ -18,6 +19,25 @@ namespace Lxfml {
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
// Returns a struct of its new center and the updated LXFML containing these edits.
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
// The base LXFML xml file to use when creating new models.
static inline std::string base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
</Bricks>
<RigidSystems>
</RigidSystems>
<GroupSystems>
<GroupSystem>
</GroupSystem>
</GroupSystems>
</LXFML>)";
// these are only for the migrations due to a bug in one of the implementations.
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);

View File

@@ -10,6 +10,7 @@ set(DCOMMONTEST_SOURCES
"TestLUString.cpp"
"TestLUWString.cpp"
"dCommonDependencies.cpp"
"lxfml_split_tests.cpp"
)
add_subdirectory(dEnumsTests)

View File

@@ -0,0 +1,145 @@
#include "gtest/gtest.h"
#include "Lxfml.h"
#include "TinyXmlUtils.h"
#include <fstream>
#include <sstream>
#include <unordered_set>
#include <filesystem>
using namespace TinyXmlUtils;
static std::string ReadFile(const std::string& path) {
std::ifstream in(path, std::ios::in | std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
TEST(LxfmlSplitTests, SplitUsesAllBricksAndNoDuplicates) {
// Read the sample test.lxfml included in tests. Resolve path relative to this source file.
std::filesystem::path srcDir = std::filesystem::path(__FILE__).parent_path();
std::filesystem::path filePath = srcDir / "test.lxfml";
std::string data = ReadFile(filePath.string());
ASSERT_FALSE(data.empty()) << "Failed to read " << filePath.string();
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0);
// Write split outputs to disk for manual inspection
std::filesystem::path outDir = srcDir / "lxfml_splits";
std::error_code ec;
std::filesystem::create_directories(outDir, ec);
for (size_t i = 0; i < results.size(); ++i) {
auto outPath = outDir / ("split_" + std::to_string(i) + ".lxfml");
std::ofstream ofs(outPath, std::ios::out | std::ios::binary);
ASSERT_TRUE(ofs) << "Failed to open output file: " << outPath.string();
ofs << results[i].lxfml;
ofs.close();
}
// parse original to count bricks
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS);
DocumentReader reader(doc);
auto lxfml = reader["LXFML"];
ASSERT_TRUE(lxfml);
// Collect original RigidSystems and Groups (serialize each element string)
auto serializeElement = [](tinyxml2::XMLElement* elem) {
tinyxml2::XMLPrinter p;
elem->Accept(&p);
return std::string(p.CStr());
};
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(serializeElement(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;
for (const auto& res : results) {
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 = serializeElement(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);
}
}
}
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);
}
}
}
// 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;
}
// 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;
}
// Ensure all original rigid systems and groups were used exactly once
ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch";
for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits";
ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch";
for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits";
}

View File

@@ -0,0 +1,336 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679" materials="23">
<Bone refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.7959,4.83179,1.36732"/>
</Part>
<Part refID="1" designID="3680" materials="23">
<Bone refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.8444,4.54236,1.49497"/>
</Part>
</Brick>
<Brick refID="1" designID="41533">
<Part refID="2" designID="41533" materials="23">
<Bone refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
</Part>
</Brick>
<Brick refID="2" designID="74340">
<Part refID="3" designID="3679" materials="23">
<Bone refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
</Part>
<Part refID="4" designID="3680" materials="23">
<Bone refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.6174,2.99808,3.31576"/>
</Part>
</Brick>
<Brick refID="3" designID="41533">
<Part refID="5" designID="41533" materials="23">
<Bone refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,12.3419,1.74316,5.0089"/>
</Part>
</Brick>
<Brick refID="4" designID="3614">
<Part refID="6" designID="3614" materials="23">
<Bone refID="6" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,13.1227,1.67822,5.36752"/>
</Part>
</Brick>
<Brick refID="5" designID="3004">
<Part refID="7" designID="3004" materials="23,0,0,0" decoration="0,0,0">
<Bone refID="7" transformation="0,0,-1,0,1,0,1,0,0,22,0.320038,10.8"/>
</Part>
</Brick>
<Brick refID="6" designID="3036">
<Part refID="8" designID="3036" materials="23">
<Bone refID="8" transformation="1,0,0,0,1,0,0,0,1,18,3.05176e-05,12.4"/>
</Part>
</Brick>
<Brick refID="7" designID="3036">
<Part refID="9" designID="3036" materials="23">
<Bone refID="9" transformation="1,0,0,0,1,0,0,0,1,14.8,0.320038,13.2"/>
</Part>
</Brick>
<Brick refID="8" designID="3036">
<Part refID="10" designID="3036" materials="23">
<Bone refID="10" transformation="1,0,0,0,1,0,0,0,1,13.2,0.640045,11.6"/>
</Part>
</Brick>
<Brick refID="9" designID="6106">
<Part refID="11" designID="6106" materials="23">
<Bone refID="11" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,0.960052,6.8"/>
</Part>
</Brick>
<Brick refID="10" designID="6106">
<Part refID="12" designID="6106" materials="23">
<Bone refID="12" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,13.2,1.28006,6.8"/>
</Part>
</Brick>
<Brick refID="11" designID="6106">
<Part refID="13" designID="6106" materials="23">
<Bone refID="13" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,1.60007,6.8"/>
</Part>
</Brick>
<Brick refID="12" designID="3730">
<Part refID="14" designID="3730" materials="23">
<Bone refID="14" transformation="-1,0,0,0,1,0,0,0,-1,13.2,1.92007,6.8"/>
</Part>
</Brick>
<Brick refID="13" designID="73587">
<Part refID="15" designID="4592" materials="23">
<Bone refID="15" transformation="1,0,0,0,1,0,0,0,1,15.6,1.92007,6.8"/>
</Part>
<Part refID="16" designID="4593" materials="23">
<Bone refID="16" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,15.6,2.34009,6.8"/>
</Part>
</Brick>
<Brick refID="14" designID="3688">
<Part refID="17" designID="3688" materials="23">
<Bone refID="17" transformation="1,0,0,0,1,0,0,0,1,3.6,0.960022,15.6"/>
</Part>
</Brick>
<Brick refID="15" designID="4085">
<Part refID="18" designID="4085" materials="23">
<Bone refID="18" transformation="1,0,0,0,1,0,0,0,1,5.2,0.960022,15.6"/>
</Part>
</Brick>
<Brick refID="16" designID="3046">
<Part refID="19" designID="3046" materials="23">
<Bone refID="19" transformation="1,0,0,0,1,0,0,0,1,4.4,3.05176e-05,14.8"/>
</Part>
</Brick>
<Brick refID="17" designID="73587">
<Part refID="20" designID="4592" materials="23">
<Bone refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-21.2,3.05176e-05,-9.2"/>
</Part>
<Part refID="21" designID="4593" materials="23">
<Bone refID="21" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-21.2,0.420044,-9.2"/>
</Part>
</Brick>
<Brick refID="18" designID="74340">
<Part refID="22" designID="3679" materials="23">
<Bone refID="22" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
</Part>
<Part refID="23" designID="3680" materials="23">
<Bone refID="23" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-15.8842,2.99805,-6.01737"/>
</Part>
</Brick>
<Brick refID="19" designID="41533">
<Part refID="24" designID="41533" materials="23">
<Bone refID="24" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
</Part>
</Brick>
<Brick refID="20" designID="41533">
<Part refID="25" designID="41533" materials="23">
<Bone refID="25" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-14.1911,1.74313,-6.74193"/>
</Part>
</Brick>
<Brick refID="21" designID="3730">
<Part refID="26" designID="3730" materials="23">
<Bone refID="26" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-12.4,1.92004,-7.60001"/>
</Part>
</Brick>
<Brick refID="22" designID="6106">
<Part refID="27" designID="6106" materials="23">
<Bone refID="27" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.60004,-6.80001"/>
</Part>
</Brick>
<Brick refID="23" designID="6106">
<Part refID="28" designID="6106" materials="23">
<Bone refID="28" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.28003,-7.60001"/>
</Part>
</Brick>
<Brick refID="24" designID="6106">
<Part refID="29" designID="6106" materials="23">
<Bone refID="29" transformation="0,0,-1,0,1,0,1,0,0,-12.4,0.960022,-6.80001"/>
</Part>
</Brick>
<Brick refID="25" designID="3036">
<Part refID="30" designID="3036" materials="23">
<Bone refID="30" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-7.6,0.640015,-7.60001"/>
</Part>
</Brick>
<Brick refID="26" designID="3036">
<Part refID="31" designID="3036" materials="23">
<Bone refID="31" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6,0.320007,-9.2"/>
</Part>
</Brick>
<Brick refID="27" designID="3036">
<Part refID="32" designID="3036" materials="23">
<Bone refID="32" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6.8,0,-12.4"/>
</Part>
</Brick>
<Brick refID="28" designID="3004">
<Part refID="33" designID="3004" materials="23,0,0,0" decoration="0,0,0">
<Bone refID="33" transformation="-1,0,0,0,0.99999994039535522,0,0,0,-1,-8.40001,0.320007,-16.4"/>
</Part>
</Brick>
<Brick refID="29" designID="73587">
<Part refID="34" designID="4592" materials="23">
<Bone refID="34" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-12.4,1.92004,-10"/>
</Part>
<Part refID="35" designID="4593" materials="23">
<Bone refID="35" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-12.4,2.34006,-10"/>
</Part>
</Brick>
<Brick refID="30" designID="3614">
<Part refID="36" designID="3614" materials="23">
<Bone refID="36" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-13.8325,1.67819,-7.52268"/>
</Part>
</Brick>
<Brick refID="31" designID="74340">
<Part refID="37" designID="3679" materials="23">
<Bone refID="37" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.8327,4.83176,-5.19592"/>
</Part>
<Part refID="38" designID="3680" materials="23">
<Bone refID="38" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.705,4.54233,-5.24437"/>
</Part>
</Brick>
<Brick refID="32" designID="3046">
<Part refID="39" designID="3046" materials="23">
<Bone refID="39" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-4.4,0,1.2"/>
</Part>
</Brick>
<Brick refID="33" designID="4085">
<Part refID="40" designID="4085" materials="23">
<Bone refID="40" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.6,0.959991,0.399994"/>
</Part>
</Brick>
<Brick refID="34" designID="3688">
<Part refID="41" designID="3688" materials="23">
<Bone refID="41" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.60001,0.959991,2"/>
</Part>
</Brick>
<Brick refID="35" designID="73587">
<Part refID="42" designID="4592" materials="23">
<Bone refID="42" transformation="1,0,0,0,1,0,0,0,1,7.60001,6.10352e-05,-9.2"/>
</Part>
<Part refID="43" designID="4593" materials="23">
<Bone refID="43" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,7.6,0.420074,-9.2"/>
</Part>
</Brick>
</Bricks>
<RigidSystems>
<RigidSystem>
<Rigid refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,5.9959182739257812,459.87173461914062,69.367317199707031" boneRefs="0"/>
<Rigid refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.0443649291992187,459.58230590820312,69.494972229003906" boneRefs="1"/>
<Rigid refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.7689208984375,458.32742309570312,71.188117980957031" boneRefs="2,3"/>
<Rigid refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.8173675537109375,458.03802490234375,71.315757751464844" boneRefs="4"/>
<Rigid refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,7.54193115234375,456.78311157226562,73.008895874023438" boneRefs="5"/>
<Rigid refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,8.3226776123046875,456.71817016601562,73.367523193359375" boneRefs="6"/>
<Rigid refID="6" transformation="0,0,-1,0,1,0,1,0,0,17.19999885559082,455.3599853515625,78.799995422363281" boneRefs="7,8,9,10,11,12,13,14,15"/>
<Rigid refID="7" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,10.800004959106445,457.38003540039062,74.800003051757813" boneRefs="16"/>
<Joint type="hinge">
<RigidRef rigidRef="0" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="1" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="1" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
<RigidRef rigidRef="2" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="2" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="3" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
<RigidRef rigidRef="3" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="5" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="ball">
<RigidRef rigidRef="6" a="-1,0,0" z="0,1,0" t="4.799992561340332,1.7600365877151489,-9.1999912261962891"/>
<RigidRef rigidRef="5" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="6" a="1,0,0" z="0,1,0" t="3.9999923706054687,2.0200366973876953,-6.3999929428100586"/>
<RigidRef rigidRef="7" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="8" transformation="1,0,0,0,1,0,0,0,1,-1.1999999284744263,455.99996948242187,83.599998474121094" boneRefs="17"/>
<Rigid refID="9" transformation="1,0,0,0,1,0,0,0,1,0.40000009536743164,455.99996948242187,83.600006103515625" boneRefs="18,19"/>
<Joint type="hinge">
<RigidRef rigidRef="9" a="0,1,0" z="0,0,1" t="-0.80000007152557373,0,-0.8000030517578125"/>
<RigidRef rigidRef="8" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="10" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-26.000003814697266,455.03997802734375,58.799995422363281" boneRefs="20"/>
<Rigid refID="11" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-26.000003814697266,455.45999145507812,58.799995422363281" boneRefs="21"/>
<Joint type="hinge">
<RigidRef rigidRef="10" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
<RigidRef rigidRef="11" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="12" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-20.811885833740234,458.327392578125,62.031078338623047" boneRefs="22,24"/>
<Rigid refID="13" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-20.684246063232422,458.03799438476562,61.982631683349609" boneRefs="23"/>
<Rigid refID="14" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.991107940673828,456.7830810546875,61.258068084716797" boneRefs="25"/>
<Rigid refID="15" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-17.200000762939453,456.95999145507812,60.399990081787109" boneRefs="26,27,28,29,30,31,32,33,34"/>
<Rigid refID="16" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.632480621337891,456.7181396484375,60.477321624755859" boneRefs="36"/>
<Rigid refID="17" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-17.200000762939453,457.3800048828125,57.999996185302734" boneRefs="35"/>
<Rigid refID="18" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.632686614990234,459.8717041015625,62.804080963134766" boneRefs="37"/>
<Rigid refID="19" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.505031585693359,459.582275390625,62.755634307861328" boneRefs="38"/>
<Joint type="hinge">
<RigidRef rigidRef="12" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="13" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="12" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
<RigidRef rigidRef="19" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="13" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="16" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="ball">
<RigidRef rigidRef="15" a="0,0,-1" z="0,1,0" t="0.40000000596046448,0.15999999642372131,0.80000001192092896"/>
<RigidRef rigidRef="16" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="15" a="0,0,1" z="0,1,0" t="-2.3999977111816406,0.41999998688697815,0"/>
<RigidRef rigidRef="17" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="18" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="19" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-9.2000007629394531,455.03994750976562,69.199996948242188" boneRefs="39,40"/>
<Rigid refID="21" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-8.4000053405761719,455.99993896484375,70" boneRefs="41"/>
<Joint type="hinge">
<RigidRef rigidRef="20" a="0,1,0" z="0,0,1" t="0,0.95999997854232788,0"/>
<RigidRef rigidRef="21" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="22" transformation="1,0,0,0,1,0,0,0,1,2.8000054359436035,455.04000854492187,58.799999237060547" boneRefs="42"/>
<Rigid refID="23" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,2.8000044822692871,455.46002197265625,58.799999237060547" boneRefs="43"/>
<Joint type="hinge">
<RigidRef rigidRef="22" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
<RigidRef rigidRef="23" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="15,16">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="19,18,17">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="11,12,13">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="10,9,7,8"/>
</Group>
</Group>
</Group>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="25,34,35,20,21,31,37,38,30,24,39,22,23,32,41,26,28,27,36,40,29,33"/>
</GroupSystem>
</GroupSystems>
</LXFML>