Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
GraphDocument.cpp
Go to the documentation of this file.
1/**
2 * @file GraphDocument.cpp
3 * @brief Implementation of GraphDocument class
4 * @author Olympe Engine
5 * @date 2026-02-18
6 */
7
8#include "GraphDocument.h"
9#include "../system/system_utils.h"
10#include <algorithm>
11
13
14namespace Olympe {
15namespace NodeGraph {
16
17// ============================================================================
18// Constructor / Destructor
19// ============================================================================
20
22 : type("AIGraph")
23 , graphKind("BehaviorTree")
24 , m_nextNodeId(1)
25 , m_nextLinkId(1)
26 , m_isDirty(false)
27{
28 metadata = json::object();
29}
30
34
35// ============================================================================
36// CRUD Operations - Nodes
37// ============================================================================
38
39NodeId GraphDocument::CreateNode(const std::string& nodeType, Vector2 pos)
40{
43 node.type = nodeType;
44 node.name = nodeType;
45 node.position = pos;
46
47 m_nodes.push_back(node);
48 m_isDirty = true;
49
50 return node.id;
51}
52
54{
55 auto it = std::find_if(m_nodes.begin(), m_nodes.end(),
56 [id](const NodeData& n) { return n.id == id; });
57
58 if (it != m_nodes.end())
59 {
60 m_nodes.erase(it);
61 m_isDirty = true;
62
63 // Remove links connected to this node
64 m_links.erase(
65 std::remove_if(m_links.begin(), m_links.end(),
66 [id](const LinkData& link) {
67 // We would need to check pin ownership here
68 // For now, just keep links
69 return false;
70 }),
71 m_links.end()
72 );
73
74 return true;
75 }
76
77 return false;
78}
79
81{
82 NodeData* node = GetNode(id);
83 if (node != nullptr)
84 {
85 node->position = newPos;
86 m_isDirty = true;
87 return true;
88 }
89 return false;
90}
91
92bool GraphDocument::UpdateNodeParameters(NodeId id, const std::map<std::string, std::string>& params)
93{
94 NodeData* node = GetNode(id);
95 if (node != nullptr)
96 {
97 node->parameters = params;
98 m_isDirty = true;
99 return true;
100 }
101 return false;
102}
103
105{
106 for (auto& node : m_nodes)
107 {
108 if (node.id == id)
109 return &node;
110 }
111 return nullptr;
112}
113
115{
116 for (const auto& node : m_nodes)
117 {
118 if (node.id == id)
119 return &node;
120 }
121 return nullptr;
122}
123
125{
126 // Find node in document
127 for (size_t i = 0; i < m_nodes.size(); ++i)
128 {
129 if (m_nodes[i].id.value == nodeId.value)
130 {
131 // Update node data while preserving ID
132 NodeId originalId = m_nodes[i].id;
133 m_nodes[i] = newData;
134 m_nodes[i].id = originalId; // Preserve original ID
135
136 SYSTEM_LOG << "[GraphDocument] Updated node ID="
137 << nodeId.value
138 << " type=" << newData.type
139 << " pos=(" << newData.position.x
140 << "," << newData.position.y << ")"
141 << std::endl;
142
143 return true;
144 }
145 }
146
147 SYSTEM_LOG << "[GraphDocument] WARNING: UpdateNode failed - node ID="
148 << nodeId.value << " not found" << std::endl;
149 return false;
150}
151
152// ============================================================================
153// CRUD Operations - Links
154// ============================================================================
155
157{
160 link.fromPin = fromPin;
161 link.toPin = toPin;
162
163 m_links.push_back(link);
164 m_isDirty = true;
165
166 return link.id;
167}
168
170{
171 auto it = std::find_if(m_links.begin(), m_links.end(),
172 [id](const LinkData& l) { return l.id == id; });
173
174 if (it != m_links.end())
175 {
176 m_links.erase(it);
177 m_isDirty = true;
178 return true;
179 }
180
181 return false;
182}
183
185{
186 for (auto& link : m_links)
187 {
188 if (link.id == id)
189 return &link;
190 }
191 return nullptr;
192}
193
195{
196 for (const auto& link : m_links)
197 {
198 if (link.id == id)
199 return &link;
200 }
201 return nullptr;
202}
203
204// ============================================================================
205// Validation
206// ============================================================================
207
208bool GraphDocument::ValidateGraph(std::string& errorMessage) const
209{
210 // Check if we have at least one node
211 if (m_nodes.empty())
212 {
213 errorMessage = "Graph has no nodes";
214 return false;
215 }
216
217 // Check if root node exists
218 if (rootNodeId.value != 0)
219 {
221 if (rootNode == nullptr)
222 {
223 errorMessage = "Root node not found";
224 return false;
225 }
226 }
227
228 // Check for cycles
229 if (HasCycles())
230 {
231 errorMessage = "Graph contains cycles";
232 return false;
233 }
234
235 // Validate composite nodes have children
236 for (const auto& node : m_nodes)
237 {
238 if (node.type == "BT_Selector" || node.type == "BT_Sequence")
239 {
240 if (node.children.empty())
241 {
242 errorMessage = "Composite node '" + node.name + "' has 0 children";
243 return false;
244 }
245 }
246 }
247
248 return true;
249}
250
252{
253 if (m_nodes.empty())
254 return false;
255
256 std::vector<NodeId> visited;
257 std::vector<NodeId> recursionStack;
258
259 for (const auto& node : m_nodes)
260 {
261 if (std::find(visited.begin(), visited.end(), node.id) == visited.end())
262 {
264 return true;
265 }
266 }
267
268 return false;
269}
270
271bool GraphDocument::HasCyclesHelper(NodeId nodeId, std::vector<NodeId>& visited, std::vector<NodeId>& recursionStack) const
272{
273 visited.push_back(nodeId);
274 recursionStack.push_back(nodeId);
275
276 const NodeData* node = GetNode(nodeId);
277 if (node == nullptr)
278 return false;
279
280 // Check children
281 for (const auto& childId : node->children)
282 {
283 if (std::find(visited.begin(), visited.end(), childId) == visited.end())
284 {
286 return true;
287 }
288 else if (std::find(recursionStack.begin(), recursionStack.end(), childId) != recursionStack.end())
289 {
290 return true;
291 }
292 }
293
294 // Check decorator child
295 if (node->decoratorChild.value != 0)
296 {
297 if (std::find(visited.begin(), visited.end(), node->decoratorChild) == visited.end())
298 {
299 if (HasCyclesHelper(node->decoratorChild, visited, recursionStack))
300 return true;
301 }
302 else if (std::find(recursionStack.begin(), recursionStack.end(), node->decoratorChild) != recursionStack.end())
303 {
304 return true;
305 }
306 }
307
308 recursionStack.erase(std::remove(recursionStack.begin(), recursionStack.end(), nodeId), recursionStack.end());
309 return false;
310}
311
312// ============================================================================
313// Serialization
314// ============================================================================
315
317{
318 json j = json::object();
319
320 j["schemaVersion"] = 2;
321 j["type"] = type;
322 j["graphKind"] = graphKind;
323 j["metadata"] = metadata;
324
325 // Editor state
326 json editorStateJson = json::object();
328
329 json scrollJson = json::object();
332 editorStateJson["scrollOffset"] = scrollJson;
333
334 json selectedJson = json::array();
335 for (const auto& nodeId : editorState.selectedNodes)
336 {
337 selectedJson.push_back(static_cast<int>(nodeId.value));
338 }
339 editorStateJson["selectedNodes"] = selectedJson;
340 editorStateJson["layoutDirection"] = editorState.layoutDirection;
341
342 j["editorState"] = editorStateJson;
343
344 // Data section
345 json dataJson = json::object();
346 dataJson["rootNodeId"] = static_cast<int>(rootNodeId.value);
347
348 // Nodes
349 json nodesJson = json::array();
350 for (const auto& node : m_nodes)
351 {
352 json nodeJson = json::object();
353 nodeJson["id"] = static_cast<int>(node.id.value);
354 nodeJson["type"] = node.type;
355 nodeJson["name"] = node.name;
356
357 json posJson = json::object();
358 posJson["x"] = node.position.x;
359 posJson["y"] = node.position.y;
360 nodeJson["position"] = posJson;
361
362 // Children
363 json childrenJson = json::array();
364 for (const auto& childId : node.children)
365 {
366 childrenJson.push_back(static_cast<int>(childId.value));
367 }
368 nodeJson["children"] = childrenJson;
369
370 // Parameters
371 json paramsJson = json::object();
372 for (auto it = node.parameters.begin(); it != node.parameters.end(); ++it)
373 {
374 paramsJson[it->first] = it->second;
375 }
376 nodeJson["parameters"] = paramsJson;
377
378 // Decorator child
379 if (node.decoratorChild.value != 0)
380 {
381 nodeJson["decoratorChildId"] = static_cast<int>(node.decoratorChild.value);
382 }
383
384 nodesJson.push_back(nodeJson);
385 }
386 dataJson["nodes"] = nodesJson;
387
388 // Links
389 json linksJson = json::array();
390 for (const auto& link : m_links)
391 {
392 json linkJson = json::object();
393 linkJson["id"] = static_cast<int>(link.id.value);
394
395 json fromPinJson = json::object();
396 fromPinJson["nodeId"] = static_cast<int>(link.fromPin.value);
397 fromPinJson["pinId"] = "output";
398 linkJson["fromPin"] = fromPinJson;
399
400 json toPinJson = json::object();
401 toPinJson["nodeId"] = static_cast<int>(link.toPin.value);
402 toPinJson["pinId"] = "input";
403 linkJson["toPin"] = toPinJson;
404
405 linksJson.push_back(linkJson);
406 }
407 dataJson["links"] = linksJson;
408
409 j["data"] = dataJson;
410
411 // Phase 2.0 - Annotations
412 j["annotations"] = m_nodeAnnotations.ToJson();
413
414 // Phase 2.1 - Blackboard
415 j["blackboard"] = m_blackboard.ToJson();
416
417 return j;
418}
419
421{
423
424 // Basic properties
425 doc.type = JsonHelper::GetString(j, "type", "AIGraph");
426 doc.graphKind = JsonHelper::GetString(j, "graphKind", "BehaviorTree");
427
428 // Metadata
429 if (j.contains("metadata") && j["metadata"].is_object())
430 {
431 doc.metadata = j["metadata"];
432 }
433
434 // Editor state
435 if (j.contains("editorState") && j["editorState"].is_object())
436 {
437 const json& es = j["editorState"];
438 doc.editorState.zoom = JsonHelper::GetFloat(es, "zoom", 1.0f);
439 doc.editorState.layoutDirection = JsonHelper::GetString(es, "layoutDirection", "TopToBottom");
440
441 if (es.contains("scrollOffset") && es["scrollOffset"].is_object())
442 {
443 const json& scroll = es["scrollOffset"];
444 doc.editorState.scrollOffset.x = JsonHelper::GetFloat(scroll, "x", 0.0f);
445 doc.editorState.scrollOffset.y = JsonHelper::GetFloat(scroll, "y", 0.0f);
446 }
447
448 if (es.contains("selectedNodes") && es["selectedNodes"].is_array())
449 {
450 const json& selArray = es["selectedNodes"];
451 for (size_t i = 0; i < selArray.size(); ++i)
452 {
453 if (selArray[i].is_number())
454 {
455 NodeId nodeId;
456 nodeId.value = selArray[i].get<uint32_t>();
457 doc.editorState.selectedNodes.push_back(nodeId);
458 }
459 }
460 }
461 }
462
463 // Data section
464 if (j.contains("data") && j["data"].is_object())
465 {
466 const json& data = j["data"];
467
468 doc.rootNodeId.value = JsonHelper::GetUInt(data, "rootNodeId", 0);
469
470 // Nodes
471 if (data.contains("nodes") && data["nodes"].is_array())
472 {
473 const json& nodesArray = data["nodes"];
474 for (size_t i = 0; i < nodesArray.size(); ++i)
475 {
476 const json& nodeJson = nodesArray[i];
477
480 node.type = JsonHelper::GetString(nodeJson, "type", "");
481 node.name = JsonHelper::GetString(nodeJson, "name", "");
482
483 if (nodeJson.contains("position") && nodeJson["position"].is_object())
484 {
485 const json& pos = nodeJson["position"];
486 node.position.x = JsonHelper::GetFloat(pos, "x", 0.0f);
487 node.position.y = JsonHelper::GetFloat(pos, "y", 0.0f);
488 }
489
490 // Children
491 if (nodeJson.contains("children") && nodeJson["children"].is_array())
492 {
493 const json& childrenArray = nodeJson["children"];
494 for (size_t c = 0; c < childrenArray.size(); ++c)
495 {
497 {
498 NodeId childId;
499 childId.value = childrenArray[c].get<uint32_t>();
500 node.children.push_back(childId);
501 }
502 }
503 }
504
505 // Parameters
506 if (nodeJson.contains("parameters") && nodeJson["parameters"].is_object())
507 {
508 const json& params = nodeJson["parameters"];
509 for (auto it = params.begin(); it != params.end(); ++it)
510 {
511 std::string key = it.key();
512 std::string value;
513 if (it.value().is_string())
514 value = it.value().get<std::string>();
515 else if (it.value().is_number())
516 value = std::to_string(it.value().get<double>());
517 else if (it.value().is_boolean())
518 value = it.value().get<bool>() ? "true" : "false";
519
520 node.parameters[key] = value;
521 }
522 }
523
524 // Decorator child
525 node.decoratorChild.value = JsonHelper::GetUInt(nodeJson, "decoratorChildId", 0);
526
527 doc.m_nodes.push_back(node);
528
529 // Update next node ID
530 if (node.id.value >= doc.m_nextNodeId)
531 {
532 doc.m_nextNodeId = node.id.value + 1;
533 }
534 }
535 }
536
537 // Links
538 if (data.contains("links") && data["links"].is_array())
539 {
540 const json& linksArray = data["links"];
541 for (size_t i = 0; i < linksArray.size(); ++i)
542 {
543 const json& linkJson = linksArray[i];
544
547
548 if (linkJson.contains("fromPin") && linkJson["fromPin"].is_object())
549 {
550 const json& fromPin = linkJson["fromPin"];
551 link.fromPin.value = JsonHelper::GetUInt(fromPin, "nodeId", 0);
552 }
553
554 if (linkJson.contains("toPin") && linkJson["toPin"].is_object())
555 {
556 const json& toPin = linkJson["toPin"];
557 link.toPin.value = JsonHelper::GetUInt(toPin, "nodeId", 0);
558 }
559
560 doc.m_links.push_back(link);
561
562 // Update next link ID
563 if (link.id.value >= doc.m_nextLinkId)
564 {
565 doc.m_nextLinkId = link.id.value + 1;
566 }
567 }
568 }
569 }
570
571 // Phase 2.0 - Annotations (backward compatible: missing key = no annotations)
572 if (j.contains("annotations") && j["annotations"].is_array())
573 {
574 doc.m_nodeAnnotations.FromJson(j["annotations"]);
575 }
576
577 // Phase 2.1 - Blackboard (backward compatible: missing key = empty blackboard)
578 if (j.contains("blackboard") && j["blackboard"].is_array())
579 {
580 doc.m_blackboard.FromJson(j["blackboard"]);
581 }
582
583 doc.m_isDirty = false;
584 return doc;
585}
586
587// ============================================================================
588// Auto-Layout
589// ============================================================================
590
592{
593 // Validate layout direction
594 if (config.direction == LayoutDirection::LeftToRight ||
595 config.direction == LayoutDirection::RightToLeft)
596 {
597 SYSTEM_LOG << "[GraphDocument] AutoLayout failed: LeftToRight and RightToLeft not yet implemented" << std::endl;
598 return false;
599 }
600
601 // Validate graph has root node
602 if (rootNodeId.value == 0)
603 {
604 SYSTEM_LOG << "[GraphDocument] AutoLayout failed: No root node defined" << std::endl;
605 return false;
606 }
607
608 if (m_nodes.empty())
609 {
610 SYSTEM_LOG << "[GraphDocument] AutoLayout failed: Graph is empty" << std::endl;
611 return false;
612 }
613
614 // Check root node exists
616 if (rootNode == nullptr)
617 {
618 SYSTEM_LOG << "[GraphDocument] AutoLayout failed: Root node not found" << std::endl;
619 return false;
620 }
621
622 SYSTEM_LOG << "[GraphDocument] Starting auto-layout from root node ID=" << rootNodeId.value << std::endl;
623
624 // Track visited nodes for cycle detection
625 std::map<NodeId, bool> visited;
626
627 // Start layout from root
628 AutoLayoutNode(rootNodeId, config, config.paddingX, config.paddingY, 0, visited);
629
630 // Mark document as modified
631 m_isDirty = true;
632
633 SYSTEM_LOG << "[GraphDocument] Auto-layout completed successfully" << std::endl;
634 return true;
635}
636
638 NodeId nodeId,
640 float startX,
641 float startY,
642 int depth,
643 std::map<NodeId, bool>& visited)
644{
645 // Cycle detection
646 auto it = visited.find(nodeId);
647 if (it != visited.end() && it->second)
648 {
649 SYSTEM_LOG << "[GraphDocument] AutoLayout: Cycle detected at node ID=" << nodeId.value << std::endl;
650 return config.nodeWidth + config.horizontalSpacing;
651 }
652
653 visited[nodeId] = true;
654
655 // Get node data
656 NodeData* node = GetNode(nodeId);
657 if (node == nullptr)
658 {
659 return config.nodeWidth + config.horizontalSpacing;
660 }
661
662 // Calculate Y position based on depth
663 float nodeY = 0.0f;
664 if (config.direction == LayoutDirection::TopToBottom)
665 {
666 nodeY = startY + static_cast<float>(depth) * config.verticalSpacing;
667 }
668 else // BottomToTop
669 {
670 nodeY = startY - static_cast<float>(depth) * config.verticalSpacing;
671 }
672
673 // Calculate total width of children
674 float totalChildrenWidth = 0.0f;
675 float childX = startX;
676
677 for (size_t i = 0; i < node->children.size(); ++i)
678 {
680 node->children[i],
681 config,
682 childX,
683 startY,
684 depth + 1,
685 visited
686 );
687
690 }
691
692 // Calculate X position for this node
693 float nodeX = 0.0f;
694 if (node->children.empty())
695 {
696 // Leaf node - use startX
697 nodeX = startX;
698 }
699 else
700 {
701 // Center above children
702 // Each child returns (width + spacing), so last child has trailing spacing
703 // Subtract one spacing to get the actual span occupied by children
704 float childrenSpan = totalChildrenWidth - config.horizontalSpacing;
705 nodeX = startX + childrenSpan * 0.5f - config.nodeWidth * 0.5f;
706 }
707
708 // Apply position to node
710 newPos.x = nodeX;
711 newPos.y = nodeY;
712 UpdateNodePosition(nodeId, newPos);
713
714 // Handle decorator child (place to the right of parent)
715 if (node->decoratorChild.value != 0)
716 {
717 float decoratorX = nodeX + config.nodeWidth + config.horizontalSpacing;
719 node->decoratorChild,
720 config,
722 nodeY,
723 depth,
724 visited
725 );
726 }
727
728 // Return width consumed by this subtree
729 if (totalChildrenWidth > 0.0f)
730 {
731 return totalChildrenWidth;
732 }
733 else
734 {
735 return config.nodeWidth + config.horizontalSpacing;
736 }
737}
738
739} // namespace NodeGraph
740} // namespace Olympe
nlohmann::json json
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Document class for managing node graphs.
json ToJson() const
Serialize blackboard to JSON.
Main document class for a node graph.
bool UpdateNode(NodeId nodeId, const NodeData &newData)
Update an existing node's data.
bool DeleteNode(NodeId id)
Delete a node from the graph.
float AutoLayoutNode(NodeId nodeId, const AutoLayoutConfig &config, float startX, float startY, int depth, std::map< NodeId, bool > &visited)
Helper to recursively layout a node and its children.
bool AutoLayout(const AutoLayoutConfig &config)
Automatically layout nodes in a hierarchical arrangement.
bool UpdateNodeParameters(NodeId id, const std::map< std::string, std::string > &params)
Update node parameters.
LinkId ConnectPins(PinId fromPin, PinId toPin)
Connect two pins with a link.
bool HasCyclesHelper(NodeId nodeId, std::vector< NodeId > &visited, std::vector< NodeId > &recursionStack) const
LinkData * GetLink(LinkId id)
Get a link by ID.
json ToJson() const
Convert graph to JSON format (v2 schema)
bool DisconnectLink(LinkId id)
Disconnect a link.
std::vector< LinkData > m_links
static GraphDocument FromJson(const json &j)
Create graph from JSON.
NodeAnnotationsManager m_nodeAnnotations
NodeId CreateNode(const std::string &nodeType, Vector2 pos)
Create a new node in the graph.
NodeData * GetNode(NodeId id)
Get a node by ID.
bool UpdateNodePosition(NodeId id, Vector2 newPos)
Update node position.
bool HasCycles() const
Check if the graph has cycles.
bool ValidateGraph(std::string &errorMessage) const
Validate the graph structure.
std::vector< NodeData > m_nodes
json ToJson() const
Serialize all annotations to JSON.
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
uint32_t GetUInt(const json &j, const std::string &key, uint32_t defaultValue=0)
Safely get an unsigned integer value from JSON.
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
Safely get a float value from JSON.
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
nlohmann::json json
std::vector< NodeId > selectedNodes
#define SYSTEM_LOG