mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
When a brick by brick save is split into models, each model was centred on the middle of its bones' origins, on the lowest origin. A brick's origin is at a corner of it, not its middle, so a single 2x2 came back offset by half a brick, and the model's floor was taken from the origins rather than from the bricks. Now each model is placed as the live server placed it: at the bottom middle of the box around its bricks. Each brick's own box comes from the client's brickdb by its design ID and is turned and moved by its bone (all 8 corners), and the model's box holds every brick's. Its x and z middle and its lowest y are the new origin, still snapped to the 0.8 grid on x and z, and every bone moves by that; rigids and rotations are untouched. A design the brickdb has no box for counts its bones' origins, as before, so no brick is left out. Lxfml::NormalizePosition and Split take the boxes by design ID, and the brick by brick save passes the ones read from the client (read on the first save, then kept). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
564 lines
23 KiB
C++
564 lines
23 KiB
C++
#include "Lxfml.h"
|
|
|
|
#include "GeneralUtils.h"
|
|
#include "StringifiedEnum.h"
|
|
#include "TinyXmlUtils.h"
|
|
|
|
#include <algorithm>
|
|
#include <ranges>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <functional>
|
|
#include <sstream>
|
|
#include <charconv>
|
|
#include <optional>
|
|
#include <array>
|
|
|
|
namespace {
|
|
// The base LXFML xml file to use when creating new models.
|
|
std::string g_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>)";
|
|
}
|
|
|
|
Lxfml::Contents Lxfml::ReadContents(const std::string_view data) {
|
|
Contents contents;
|
|
if (data.empty()) return contents;
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
if (doc.Parse(data.data(), data.size()) != tinyxml2::XML_SUCCESS) return contents;
|
|
const auto* bricks = doc.FirstChildElement("LXFML") ? doc.FirstChildElement("LXFML")->FirstChildElement("Bricks") : nullptr;
|
|
if (!bricks) return contents;
|
|
|
|
bool anyBone = false;
|
|
for (const auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
|
|
// designID may carry a suffix ("3001;A")
|
|
const std::string_view design = brick->Attribute("designID") ? brick->Attribute("designID") : "";
|
|
const auto digits = design.substr(0, design.find_first_not_of("0123456789"));
|
|
const auto designId = GeneralUtils::TryParse<uint32_t>(digits);
|
|
if (designId) contents.designIds.push_back(*designId);
|
|
|
|
for (const auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
|
|
for (const auto* bone = part->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("Bone")) {
|
|
const auto* transformation = bone->Attribute("transformation");
|
|
if (!transformation) continue;
|
|
const auto split = GeneralUtils::SplitString(transformation, ',');
|
|
if (split.size() < 12) continue;
|
|
const auto x = GeneralUtils::TryParse<float>(split[9]);
|
|
const auto y = GeneralUtils::TryParse<float>(split[10]);
|
|
const auto z = GeneralUtils::TryParse<float>(split[11]);
|
|
if (!x || !y || !z) continue;
|
|
const NiPoint3 position{ *x, *y, *z };
|
|
if (!anyBone) {
|
|
contents.boxMin = position;
|
|
contents.boxMax = position;
|
|
anyBone = true;
|
|
continue;
|
|
}
|
|
contents.boxMin = NiPoint3(std::min(contents.boxMin.x, position.x), std::min(contents.boxMin.y, position.y), std::min(contents.boxMin.z, position.z));
|
|
contents.boxMax = NiPoint3(std::max(contents.boxMax.x, position.x), std::max(contents.boxMax.y, position.y), std::max(contents.boxMax.z, position.z));
|
|
}
|
|
}
|
|
}
|
|
return contents;
|
|
}
|
|
|
|
namespace {
|
|
// A transformation attribute: 9 rotation values kept as written, then the position
|
|
struct Transformation {
|
|
std::vector<std::string> rotation;
|
|
double x{}, y{}, z{};
|
|
};
|
|
|
|
std::optional<Transformation> ParseTransformation(const char* text) {
|
|
if (!text) return std::nullopt;
|
|
auto split = GeneralUtils::SplitString(text, ',');
|
|
if (split.size() < 12) return std::nullopt;
|
|
const auto x = GeneralUtils::TryParse<double>(split[9]);
|
|
const auto y = GeneralUtils::TryParse<double>(split[10]);
|
|
const auto z = GeneralUtils::TryParse<double>(split[11]);
|
|
if (!x || !y || !z) return std::nullopt;
|
|
split.resize(9);
|
|
return Transformation{ std::move(split), *x, *y, *z };
|
|
}
|
|
|
|
// The shortest text that reads back as the same float
|
|
std::string FormatNumber(const double value) {
|
|
char buffer[32];
|
|
const auto [end, error] = std::to_chars(buffer, buffer + sizeof(buffer), static_cast<float>(value));
|
|
return error == std::errc() ? std::string(buffer, end) : std::to_string(value);
|
|
}
|
|
|
|
std::string FormatTransformation(const Transformation& transformation) {
|
|
std::string text;
|
|
for (const auto& value : transformation.rotation) text += value + ',';
|
|
return text + FormatNumber(transformation.x) + ',' + FormatNumber(transformation.y) + ',' + FormatNumber(transformation.z);
|
|
}
|
|
|
|
// A box in world space, grown point by point
|
|
struct Box {
|
|
bool any = false;
|
|
double minX{}, minY{}, minZ{}, maxX{}, maxY{}, maxZ{};
|
|
|
|
void Add(const double x, const double y, const double z) {
|
|
minX = any ? std::min(minX, x) : x; maxX = any ? std::max(maxX, x) : x;
|
|
minY = any ? std::min(minY, y) : y; maxY = any ? std::max(maxY, y) : y;
|
|
minZ = any ? std::min(minZ, z) : z; maxZ = any ? std::max(maxZ, z) : z;
|
|
any = true;
|
|
}
|
|
|
|
// A brick's own box, placed in the world by its bone: all 8 corners, rotated then moved. A rotation that can't
|
|
// be read counts as the bone's origin.
|
|
void Add(const BrickBounds::Bounds& bounds, const Transformation& transformation) {
|
|
std::array<double, 9> rotation{};
|
|
for (size_t i = 0; i < rotation.size(); i++) {
|
|
const auto value = GeneralUtils::TryParse<double>(transformation.rotation[i]);
|
|
if (!value) {
|
|
Add(transformation.x, transformation.y, transformation.z);
|
|
return;
|
|
}
|
|
rotation[i] = *value;
|
|
}
|
|
for (uint32_t corner = 0; corner < 8; corner++) {
|
|
const double x = (corner & 1) ? bounds.max.x : bounds.min.x;
|
|
const double y = (corner & 2) ? bounds.max.y : bounds.min.y;
|
|
const double z = (corner & 4) ? bounds.max.z : bounds.min.z;
|
|
Add(x * rotation[0] + y * rotation[3] + z * rotation[6] + transformation.x,
|
|
x * rotation[1] + y * rotation[4] + z * rotation[7] + transformation.y,
|
|
x * rotation[2] + y * rotation[5] + z * rotation[8] + transformation.z);
|
|
}
|
|
}
|
|
};
|
|
|
|
// The box of a part's design: the part's designID, or its brick's
|
|
const BrickBounds::Bounds* FindBounds(const BrickBounds::DesignBounds& bounds, const tinyxml2::XMLElement* part, const tinyxml2::XMLElement* brick) {
|
|
if (bounds.empty()) return nullptr;
|
|
for (const auto* element : { part, brick }) {
|
|
const char* design = element->Attribute("designID");
|
|
const auto id = design ? GeneralUtils::TryParse<uint32_t>(design) : std::nullopt;
|
|
if (!id) continue;
|
|
const auto it = bounds.find(*id);
|
|
if (it != bounds.end()) return &it->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition, const BrickBounds::DesignBounds& bounds) {
|
|
Result toReturn;
|
|
if (data.empty()) return toReturn;
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
// Use length-based parsing to avoid expensive string copy
|
|
if (doc.Parse(data.data(), data.size()) != tinyxml2::XML_SUCCESS) return toReturn;
|
|
auto* lxfml = doc.FirstChildElement("LXFML");
|
|
if (!lxfml) return toReturn;
|
|
|
|
// Every bone of every part (flexible parts have several). Only bones move, as the live server did: the rigids keep
|
|
// the transformations the client sent.
|
|
// The box around the model: each part's own box placed by its first bone, as the brick is in the world; a part
|
|
// whose design has no box counts the origins of its bones instead.
|
|
std::vector<tinyxml2::XMLElement*> bones;
|
|
Box box;
|
|
if (auto* bricks = lxfml->FirstChildElement("Bricks")) {
|
|
for (auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
|
|
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
|
|
const auto* design = FindBounds(bounds, part, brick);
|
|
bool placed = false;
|
|
for (auto* bone = part->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("Bone")) {
|
|
bones.push_back(bone);
|
|
const auto transformation = ParseTransformation(bone->Attribute("transformation"));
|
|
if (!transformation || placed) continue;
|
|
if (design) {
|
|
box.Add(*design, *transformation);
|
|
placed = true;
|
|
} else {
|
|
box.Add(transformation->x, transformation->y, transformation->z);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The new origin: the middle of the box, on its floor, as the live server placed models. x and z are snapped to
|
|
// the LEGO grid (0.8) so the model's position stays on it; the bricks don't move in the world either way, only the
|
|
// model's pivot does.
|
|
double rootX = curPosition.x, rootY = curPosition.y, rootZ = curPosition.z;
|
|
if (curPosition == NiPoint3Constant::ZERO) {
|
|
// Nothing to place it by: keep the model as it is, at the origin
|
|
if (!box.any) {
|
|
toReturn.lxfml = std::string(data);
|
|
return toReturn;
|
|
}
|
|
rootX = (box.minX + box.maxX) / 2.0;
|
|
rootY = box.minY;
|
|
rootZ = (box.minZ + box.maxZ) / 2.0;
|
|
}
|
|
rootX = GeneralUtils::RountToNearestEven(rootX, 0.8);
|
|
rootZ = GeneralUtils::RountToNearestEven(rootZ, 0.8);
|
|
|
|
// Every bone moves by the same amount: onto the new origin, then by the given position
|
|
const double offsetX = curPosition.x - rootX, offsetY = curPosition.y - rootY, offsetZ = curPosition.z - rootZ;
|
|
for (auto* bone : bones) {
|
|
auto transformation = ParseTransformation(bone->Attribute("transformation"));
|
|
if (!transformation) continue;
|
|
transformation->x += offsetX;
|
|
transformation->y += offsetY;
|
|
transformation->z += offsetZ;
|
|
bone->SetAttribute("transformation", FormatTransformation(*transformation).c_str());
|
|
}
|
|
|
|
tinyxml2::XMLPrinter printer;
|
|
doc.Print(&printer);
|
|
|
|
toReturn.lxfml = printer.CStr();
|
|
toReturn.center = NiPoint3(static_cast<float>(rootX), static_cast<float>(rootY), static_cast<float>(rootZ));
|
|
return toReturn;
|
|
}
|
|
|
|
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc, int maxDepth = 100) {
|
|
if (!src || maxDepth <= 0) 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()) {
|
|
// Recursively clone child elements with decremented depth
|
|
auto* clonedChild = CloneElementDeep(childElem, dstDoc, maxDepth - 1);
|
|
if (clonedChild) dst->InsertEndChild(clonedChild);
|
|
} 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, const BrickBounds::DesignBounds& bounds) {
|
|
std::vector<Result> results;
|
|
|
|
// Handle empty or invalid input
|
|
if (data.empty()) {
|
|
return results;
|
|
}
|
|
|
|
// Prevent processing extremely large inputs that could cause hangs
|
|
if (data.size() > 10000000) { // 10MB limit
|
|
return results;
|
|
}
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
// Use length-based parsing to avoid expensive string copy
|
|
const auto err = doc.Parse(data.data(), data.size());
|
|
if (err != tinyxml2::XML_SUCCESS) {
|
|
return results;
|
|
}
|
|
|
|
auto* lxfml = doc.FirstChildElement("LXFML");
|
|
if (!lxfml) {
|
|
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;
|
|
std::vector<tinyxml2::XMLElement*> brickOrder;
|
|
|
|
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);
|
|
brickOrder.push_back(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);
|
|
}
|
|
// Flexible parts have a bone per section
|
|
for (auto* bone = part->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("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;
|
|
|
|
// Track used groups to avoid processing them twice
|
|
std::unordered_set<tinyxml2::XMLElement*> usedGroups;
|
|
|
|
// 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(g_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 and rigid systems in the order the file has them, so the same model always
|
|
// comes out the same
|
|
for (auto* brick : brickOrder) {
|
|
const char* bref = brick->Attribute("refID");
|
|
// (a refID used twice: the first brick with it, as the maps have it)
|
|
if (!bref || !bricksToInclude.contains(bref) || brickByRef.at(bref) != brick) continue;
|
|
tinyxml2::XMLElement* cloned = CloneElementDeep(brick, outDoc);
|
|
if (cloned) outBricks->InsertEndChild(cloned);
|
|
}
|
|
for (auto* rsPtr : rigidSystems) {
|
|
if (std::find(rigidSystemsToInclude.begin(), rigidSystemsToInclude.end(), rsPtr) == rigidSystemsToInclude.end()) continue;
|
|
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, then normalize position and compute center (the input is at most 10 MB, so each part is too)
|
|
tinyxml2::XMLPrinter printer;
|
|
outDoc.Print(&printer);
|
|
return NormalizePosition(printer.CStr(), curPosition, bounds);
|
|
};
|
|
|
|
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
|
|
for (auto* groupRoot : groupRoots) {
|
|
// Skip if this group was already processed as part of another group
|
|
if (usedGroups.find(groupRoot) != usedGroups.end()) continue;
|
|
|
|
// Helper to collect all partRefs in a group's subtree
|
|
std::function<void(const tinyxml2::XMLElement*, std::unordered_set<std::string>&)> collectParts = [&](const tinyxml2::XMLElement* g, std::unordered_set<std::string>& partRefs) {
|
|
if (!g) return;
|
|
const char* partAttr = g->Attribute("partRefs");
|
|
if (partAttr) {
|
|
for (auto& tok : GeneralUtils::SplitString(partAttr, ',')) partRefs.insert(tok);
|
|
}
|
|
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child, partRefs);
|
|
};
|
|
|
|
// Collect all groups that need to be merged into this output
|
|
std::vector<tinyxml2::XMLElement*> groupsToInclude{ groupRoot };
|
|
usedGroups.insert(groupRoot);
|
|
|
|
// Build initial sets of bricks and boneRefs from the starting group
|
|
std::unordered_set<std::string> partRefs;
|
|
collectParts(groupRoot, partRefs);
|
|
|
|
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()) {
|
|
for (auto* bone = partIt->second->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("Bone")) {
|
|
const char* bref = bone->Attribute("refID");
|
|
if (bref) boneRefsIncluded.insert(std::string(bref));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iteratively include any RigidSystems that reference any boneRefsIncluded
|
|
// and check if those rigid systems' bricks span other groups
|
|
bool changed = true;
|
|
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
|
|
int maxIterations = 1000; // Safety limit to prevent infinite loops
|
|
int iteration = 0;
|
|
while (changed && iteration < maxIterations) {
|
|
changed = false;
|
|
iteration++;
|
|
|
|
// First, expand rigid systems based on current boneRefsIncluded
|
|
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 : GeneralUtils::SplitString(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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second, check if the newly included bricks span any other groups
|
|
// If so, merge those groups into the current output
|
|
for (auto* otherGroup : groupRoots) {
|
|
if (usedGroups.find(otherGroup) != usedGroups.end()) continue;
|
|
|
|
// Collect partRefs from this other group
|
|
std::unordered_set<std::string> otherPartRefs;
|
|
collectParts(otherGroup, otherPartRefs);
|
|
|
|
// Check if any of these partRefs correspond to bricks we've already included
|
|
bool spansOtherGroup = false;
|
|
for (const auto& pref : otherPartRefs) {
|
|
auto pit = partRefToBrick.find(pref);
|
|
if (pit != partRefToBrick.end()) {
|
|
const char* bref = pit->second->Attribute("refID");
|
|
if (bref && bricksIncluded.find(std::string(bref)) != bricksIncluded.end()) {
|
|
spansOtherGroup = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (spansOtherGroup) {
|
|
// Merge this group into the current output
|
|
usedGroups.insert(otherGroup);
|
|
groupsToInclude.push_back(otherGroup);
|
|
changed = true;
|
|
|
|
// Add all partRefs, boneRefs, and bricks from this group
|
|
for (const auto& pref : otherPartRefs) {
|
|
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()) {
|
|
for (auto* bone = partIt->second->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("Bone")) {
|
|
const char* bref = bone->Attribute("refID");
|
|
if (bref) boneRefsIncluded.insert(std::string(bref));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// (Every pass that goes on adds a rigid system or group, so the limit is never reached; hitting it anyway still
|
|
// outputs what was collected, and the rest of the file comes out as further models below.)
|
|
// include bricks from bricksIncluded into used set
|
|
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
|
|
|
|
// make output doc and push result (include all merged groups' XML)
|
|
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsToInclude);
|
|
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 : GeneralUtils::SplitString(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 (auto* brick : brickOrder) {
|
|
const char* brefAttr = brick->Attribute("refID");
|
|
if (!brefAttr) continue;
|
|
const std::string bref(brefAttr);
|
|
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;
|
|
}
|