2026-04-15 08:24:06 +08:00
|
|
|
#include "HierarchyModel.h"
|
2026-04-12 11:12:27 +08:00
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <functional>
|
|
|
|
|
#include <optional>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
|
|
|
|
namespace XCEngine::UI::Editor::App {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* FindNodeRecursive(
|
|
|
|
|
const std::vector<HierarchyNode>& nodes,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view nodeId) {
|
2026-04-15 08:24:06 +08:00
|
|
|
for (const HierarchyNode& node : nodes) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (node.nodeId == nodeId) {
|
|
|
|
|
return &node;
|
|
|
|
|
}
|
2026-04-15 08:24:06 +08:00
|
|
|
if (const HierarchyNode* child =
|
2026-04-12 11:12:27 +08:00
|
|
|
FindNodeRecursive(node.children, nodeId);
|
|
|
|
|
child != nullptr) {
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode* FindNodeRecursive(
|
|
|
|
|
std::vector<HierarchyNode>& nodes,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view nodeId) {
|
2026-04-15 08:24:06 +08:00
|
|
|
for (HierarchyNode& node : nodes) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (node.nodeId == nodeId) {
|
|
|
|
|
return &node;
|
|
|
|
|
}
|
2026-04-15 08:24:06 +08:00
|
|
|
if (HierarchyNode* child =
|
2026-04-12 11:12:27 +08:00
|
|
|
FindNodeRecursive(node.children, nodeId);
|
|
|
|
|
child != nullptr) {
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FindNodeParentRecursive(
|
2026-04-15 08:24:06 +08:00
|
|
|
const std::vector<HierarchyNode>& nodes,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view nodeId,
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode*& parent) {
|
|
|
|
|
for (const HierarchyNode& node : nodes) {
|
|
|
|
|
for (const HierarchyNode& child : node.children) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (child.nodeId == nodeId) {
|
|
|
|
|
parent = &node;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (FindNodeParentRecursive(node.children, nodeId, parent)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ExtractNodeRecursive(
|
2026-04-15 08:24:06 +08:00
|
|
|
std::vector<HierarchyNode>& nodes,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view nodeId,
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode& extractedNode) {
|
2026-04-12 11:12:27 +08:00
|
|
|
for (std::size_t index = 0u; index < nodes.size(); ++index) {
|
|
|
|
|
if (nodes[index].nodeId == nodeId) {
|
|
|
|
|
extractedNode = std::move(nodes[index]);
|
|
|
|
|
nodes.erase(nodes.begin() + static_cast<std::ptrdiff_t>(index));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ExtractNodeRecursive(nodes[index].children, nodeId, extractedNode)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:30:58 +08:00
|
|
|
HierarchyNode CloneNodeRecursive(
|
|
|
|
|
const HierarchyNode& source,
|
|
|
|
|
const std::function<std::string()>& allocateNodeId) {
|
|
|
|
|
HierarchyNode duplicate = {};
|
|
|
|
|
duplicate.nodeId = allocateNodeId();
|
|
|
|
|
duplicate.label = source.label;
|
|
|
|
|
duplicate.children.reserve(source.children.size());
|
|
|
|
|
for (const HierarchyNode& child : source.children) {
|
|
|
|
|
duplicate.children.push_back(CloneNodeRecursive(child, allocateNodeId));
|
|
|
|
|
}
|
|
|
|
|
return duplicate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DuplicateNodeRecursive(
|
|
|
|
|
std::vector<HierarchyNode>& nodes,
|
|
|
|
|
std::string_view nodeId,
|
|
|
|
|
const std::function<std::string()>& allocateNodeId,
|
|
|
|
|
std::string& duplicatedNodeId) {
|
|
|
|
|
for (std::size_t index = 0u; index < nodes.size(); ++index) {
|
|
|
|
|
if (nodes[index].nodeId == nodeId) {
|
|
|
|
|
HierarchyNode duplicate = CloneNodeRecursive(nodes[index], allocateNodeId);
|
|
|
|
|
duplicatedNodeId = duplicate.nodeId;
|
|
|
|
|
nodes.insert(
|
|
|
|
|
nodes.begin() + static_cast<std::ptrdiff_t>(index + 1u),
|
|
|
|
|
std::move(duplicate));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (DuplicateNodeRecursive(
|
|
|
|
|
nodes[index].children,
|
|
|
|
|
nodeId,
|
|
|
|
|
allocateNodeId,
|
|
|
|
|
duplicatedNodeId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 11:12:27 +08:00
|
|
|
void BuildTreeItemsRecursive(
|
2026-04-15 08:24:06 +08:00
|
|
|
const std::vector<HierarchyNode>& nodes,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::uint32_t depth,
|
|
|
|
|
const ::XCEngine::UI::UITextureHandle& icon,
|
|
|
|
|
std::vector<Widgets::UIEditorTreeViewItem>& items) {
|
2026-04-15 08:24:06 +08:00
|
|
|
for (const HierarchyNode& node : nodes) {
|
2026-04-12 11:12:27 +08:00
|
|
|
Widgets::UIEditorTreeViewItem item = {};
|
|
|
|
|
item.itemId = node.nodeId;
|
|
|
|
|
item.label = node.label;
|
|
|
|
|
item.depth = depth;
|
|
|
|
|
item.forceLeaf = node.children.empty();
|
|
|
|
|
item.leadingIcon = icon;
|
|
|
|
|
items.push_back(std::move(item));
|
|
|
|
|
BuildTreeItemsRecursive(node.children, depth + 1u, icon, items);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyModel HierarchyModel::BuildDefault() {
|
|
|
|
|
HierarchyModel model = {};
|
2026-04-12 11:12:27 +08:00
|
|
|
model.m_roots = {
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode{ "main_camera", "Main Camera", {} },
|
|
|
|
|
HierarchyNode{ "directional_light", "Directional Light", {} },
|
|
|
|
|
HierarchyNode{
|
2026-04-12 11:12:27 +08:00
|
|
|
"player",
|
|
|
|
|
"Player",
|
|
|
|
|
{
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode{ "camera_pivot", "Camera Pivot", {} },
|
|
|
|
|
HierarchyNode{ "player_mesh", "Mesh", {} }
|
2026-04-12 11:12:27 +08:00
|
|
|
} },
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode{
|
2026-04-12 11:12:27 +08:00
|
|
|
"environment",
|
|
|
|
|
"Environment",
|
|
|
|
|
{
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode{ "ground", "Ground", {} },
|
|
|
|
|
HierarchyNode{
|
2026-04-12 11:12:27 +08:00
|
|
|
"props",
|
|
|
|
|
"Props",
|
|
|
|
|
{
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode{ "crate_01", "Crate_01", {} },
|
|
|
|
|
HierarchyNode{ "barrel_01", "Barrel_01", {} }
|
2026-04-12 11:12:27 +08:00
|
|
|
} }
|
|
|
|
|
} }
|
|
|
|
|
};
|
|
|
|
|
model.m_nextGeneratedNodeId = 1u;
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::Empty() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
return m_roots.empty();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::ContainsNode(std::string_view nodeId) const {
|
2026-04-12 11:12:27 +08:00
|
|
|
return FindNode(nodeId) != nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* HierarchyModel::FindNode(std::string_view nodeId) const {
|
2026-04-12 11:12:27 +08:00
|
|
|
return FindNodeRecursive(m_roots, nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode* HierarchyModel::FindNode(std::string_view nodeId) {
|
2026-04-12 11:12:27 +08:00
|
|
|
return FindNodeRecursive(m_roots, nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
std::optional<std::string> HierarchyModel::GetParentId(std::string_view nodeId) const {
|
|
|
|
|
const HierarchyNode* parent = nullptr;
|
2026-04-12 11:12:27 +08:00
|
|
|
if (!FindNodeParentRecursive(m_roots, nodeId, parent) || parent == nullptr) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parent->nodeId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::RenameNode(std::string_view nodeId, std::string label) {
|
|
|
|
|
HierarchyNode* node = FindNode(nodeId);
|
2026-04-12 11:12:27 +08:00
|
|
|
if (node == nullptr || label.empty() || node->label == label) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node->label = std::move(label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
std::string HierarchyModel::CreateChild(
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view parentId,
|
|
|
|
|
std::string_view label) {
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode* parent = FindNode(parentId);
|
2026-04-12 11:12:27 +08:00
|
|
|
if (parent == nullptr) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode node = {};
|
2026-04-12 11:12:27 +08:00
|
|
|
node.nodeId = AllocateNodeId();
|
|
|
|
|
node.label = label.empty() ? std::string("GameObject") : std::string(label);
|
|
|
|
|
parent->children.push_back(std::move(node));
|
|
|
|
|
return parent->children.back().nodeId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:30:58 +08:00
|
|
|
std::string HierarchyModel::DuplicateNode(std::string_view nodeId) {
|
|
|
|
|
if (nodeId.empty() || !ContainsNode(nodeId)) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string duplicatedNodeId = {};
|
|
|
|
|
const auto allocateNodeId = [this]() {
|
|
|
|
|
return AllocateNodeId();
|
|
|
|
|
};
|
|
|
|
|
if (!DuplicateNodeRecursive(m_roots, nodeId, allocateNodeId, duplicatedNodeId)) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return duplicatedNodeId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::DeleteNode(std::string_view nodeId) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (nodeId.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode removed = {};
|
2026-04-12 11:12:27 +08:00
|
|
|
return ExtractNodeRecursive(m_roots, nodeId, removed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::CanReparent(
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view sourceNodeId,
|
|
|
|
|
std::string_view targetParentId) const {
|
|
|
|
|
if (sourceNodeId.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* source = FindNode(sourceNodeId);
|
2026-04-12 11:12:27 +08:00
|
|
|
if (source == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetParentId.empty()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* targetParent = FindNode(targetParentId);
|
2026-04-12 11:12:27 +08:00
|
|
|
return CanAdopt(sourceNodeId, targetParent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::Reparent(
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view sourceNodeId,
|
|
|
|
|
std::string_view targetParentId) {
|
|
|
|
|
if (!CanReparent(sourceNodeId, targetParentId) || targetParentId.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::optional<std::string> currentParentId = GetParentId(sourceNodeId);
|
|
|
|
|
if (currentParentId.has_value() && currentParentId.value() == targetParentId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode movedNode = {};
|
2026-04-12 11:12:27 +08:00
|
|
|
if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode* targetParent = FindNode(targetParentId);
|
2026-04-12 11:12:27 +08:00
|
|
|
if (targetParent == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetParent->children.push_back(std::move(movedNode));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::MoveToRoot(std::string_view sourceNodeId) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (sourceNodeId.empty() || !ContainsNode(sourceNodeId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!GetParentId(sourceNodeId).has_value()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
HierarchyNode movedNode = {};
|
2026-04-12 11:12:27 +08:00
|
|
|
if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_roots.push_back(std::move(movedNode));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
std::vector<Widgets::UIEditorTreeViewItem> HierarchyModel::BuildTreeItems(
|
2026-04-12 11:12:27 +08:00
|
|
|
const ::XCEngine::UI::UITextureHandle& icon) const {
|
|
|
|
|
std::vector<Widgets::UIEditorTreeViewItem> items = {};
|
|
|
|
|
BuildTreeItemsRecursive(m_roots, 0u, icon, items);
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
std::string HierarchyModel::AllocateNodeId() {
|
2026-04-12 11:12:27 +08:00
|
|
|
std::ostringstream stream = {};
|
|
|
|
|
stream << "generated_node_" << m_nextGeneratedNodeId++;
|
|
|
|
|
return stream.str();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::CanAdopt(
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view sourceNodeId,
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* targetParent) const {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (targetParent == nullptr || sourceNodeId == targetParent->nodeId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
const HierarchyNode* source = FindNode(sourceNodeId);
|
2026-04-12 11:12:27 +08:00
|
|
|
if (source == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !ContainsDescendant(*source, targetParent->nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
bool HierarchyModel::ContainsDescendant(
|
|
|
|
|
const HierarchyNode& node,
|
2026-04-12 11:12:27 +08:00
|
|
|
std::string_view candidateId) const {
|
2026-04-15 08:24:06 +08:00
|
|
|
for (const HierarchyNode& child : node.children) {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (child.nodeId == candidateId || ContainsDescendant(child, candidateId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace XCEngine::UI::Editor::App
|
2026-04-15 08:24:06 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|