diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d690a31 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.10) + +project(ImNodeFlow CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +include_directories( + include +) + +set(SOURCE_FILES + src/ImNodeFlow.cpp +) + +add_library(ImNodeFlow SHARED ${SOURCE_FILES}) +target_compile_definitions(ImNodeFlow PRIVATE IMGUI_DEFINE_MATH_OPERATORS) + +target_include_directories(ImNodeFlow + PUBLIC + $ + $ +) + +install(TARGETS ImNodeFlow + EXPORT ImNodeFlowTargets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) + +install(DIRECTORY include/ DESTINATION include) + +install(EXPORT ImNodeFlowTargets + FILE ImNodeFlowConfig.cmake + NAMESPACE ImNodeFlow:: + DESTINATION lib/cmake/ImNodeFlow +) \ No newline at end of file diff --git a/include/ImNodeFlow.h b/include/ImNodeFlow.h index 3fd17cd..e654dd1 100644 --- a/include/ImNodeFlow.h +++ b/include/ImNodeFlow.h @@ -8,10 +8,12 @@ #include #include #include +#include #include #include #include -#include +#include +#include #include #include "../src/imgui_bezier_math.h" #include "../src/context_wrapper.h" @@ -174,6 +176,43 @@ namespace ImFlow static std::shared_ptr red() { return std::make_shared(IM_COL32(191,90,90,255), ImColor(233,241,244,255), 11.f); } /// @brief
Default brown style static std::shared_ptr brown() { return std::make_shared(IM_COL32(191,134,90,255), ImColor(233,241,244,255), 6.5f); } + + // Category-specific styles + /// @brief
Control flow (branch, if/else) - orange/amber + static std::shared_ptr controlFlow() { return std::make_shared(IM_COL32(230,150,50,255), ImColor(255,255,255,255), 4.f); } + /// @brief
Loops (for, while) - purple + static std::shared_ptr loop() { return std::make_shared(IM_COL32(150,90,180,255), ImColor(255,255,255,255), 4.f); } + /// @brief
Variables - teal/cyan + static std::shared_ptr variable() { return std::make_shared(IM_COL32(60,160,160,255), ImColor(255,255,255,255), 4.f); } + /// @brief
Operators - blue + static std::shared_ptr operation() { return std::make_shared(IM_COL32(70,130,200,255), ImColor(255,255,255,255), 4.f); } + /// @brief
System calls (print, play media, etc.) - pink/magenta + static std::shared_ptr syscall() { return std::make_shared(IM_COL32(200,80,150,255), ImColor(255,255,255,255), 4.f); } + /// @brief
Functions/Modules - indigo + static std::shared_ptr function() { return std::make_shared(IM_COL32(100,80,180,255), ImColor(255,255,255,255), 4.f); } + /// @brief
Events/Signals - yellow-green + static std::shared_ptr event() { return std::make_shared(IM_COL32(180,200,60,255), ImColor(40,40,40,255), 4.f); } + /// @brief
Comments - soft yellow + static std::shared_ptr comment() { return std::make_shared(IM_COL32(220,200,100,255), ImColor(60,60,60,255), 4.f); } + + /// @brief
Create custom style from header color + static std::shared_ptr fromColor(ImU32 headerColor) { return std::make_shared(headerColor, ImColor(255,255,255,255), 4.f); } + }; + + // ----------------------------------------------------------------------------------------------------------------- + // WAYPOINT + + /** + * @brief A point on a link that can be moved to organize connections + */ + struct Waypoint + { + ImVec2 pos; // Position in grid coordinates (snapped) + ImVec2 posTarget; // Target position for dragging (not snapped) + bool hovered = false; + bool dragged = false; + static constexpr float RADIUS = 6.0f; + static constexpr float HOVER_RADIUS = 10.0f; }; // ----------------------------------------------------------------------------------------------------------------- @@ -198,6 +237,24 @@ namespace ImFlow * @details Deletes references of this links form connected pins */ ~Link(); + + /** + * @brief
Disable cleanup on destruction + * @details Call this before destroying the link when pins are already destroyed + */ + void disableCleanup() { m_cleanupEnabled = false; } + + /** + * @brief
Mark link as invalid (pins are being destroyed) + * @details Call this before destroying pins to prevent access to dangling pointers + */ + void invalidate() { m_valid = false; m_cleanupEnabled = false; } + + /** + * @brief
Check if link is still valid + * @return [TRUE] if link pins are still valid + */ + [[nodiscard]] bool isValid() const { return m_valid; } /** * @brief
Looping function to update the Link @@ -205,6 +262,37 @@ namespace ImFlow */ void update(); + /** + * @brief
Add a waypoint at the given position + * @param pos Position in grid coordinates + * @return Index of the newly added waypoint + */ + int addWaypoint(const ImVec2& pos); + + /** + * @brief
Remove a waypoint by index + * @param index Index of the waypoint to remove + */ + void removeWaypoint(int index); + + /** + * @brief
Get all waypoints + * @return Reference to the vector of waypoints + */ + std::vector& getWaypoints() { return m_waypoints; } + + /** + * @brief
Set all waypoints at once (useful for deserialization) + * @param waypoints Vector of positions in grid coordinates + */ + void setWaypoints(const std::vector& positions); + + /** + * @brief
Check if mouse is hovering any waypoint + * @return Index of hovered waypoint or -1 if none + */ + int getHoveredWaypoint() const; + /** * @brief
Get Left pin of the link * @return Pointer to the Pin @@ -234,6 +322,10 @@ namespace ImFlow ImNodeFlow* m_inf; bool m_hovered = false; bool m_selected = false; + bool m_cleanupEnabled = true; + bool m_valid = true; + std::vector m_waypoints; + int m_draggedWaypointIndex = -1; }; // ----------------------------------------------------------------------------------------------------------------- @@ -265,6 +357,27 @@ namespace ImFlow InfColors colors; }; + /** + * @brief A group of nodes that can be moved together + */ + struct NodeGroup + { + using GroupUID = uint64_t; + + GroupUID uid{0}; + std::string name{"Group"}; + std::string comment; + ImU32 color{IM_COL32(100, 100, 200, 60)}; + ImU32 borderColor{IM_COL32(100, 100, 200, 200)}; + std::set members; + float padding{20.0f}; + + bool dragging{false}; + bool hovered{false}; + bool selected{false}; + ImVec2 dragOffset{0, 0}; + }; + /** * @brief Main node editor * @details Handles the infinite grid, nodes and links. Also handles all the logic. @@ -397,6 +510,13 @@ namespace ImFlow */ void consumeSingleUseClick() { m_singleUseClick = false; } + /** + * @brief
Get mouse double-clicking status + * @return [TRUE] if mouse is double-clicked and double-click hasn't been consumed + */ + [[nodiscard]] bool getDoubleUseClick() const { return m_doubleUseClick; } + void consumeDoubleUseClick() { m_doubleUseClick = false; } + /** * @brief
Get editor's name * @return Const reference to editor's name @@ -485,6 +605,12 @@ namespace ImFlow */ void hoveredNode(BaseNode* hovering) { m_hoveredNode = hovering; } + /** + * @brief
Set what link is being hovered + * @param link Shared pointer to the hovered link + */ + void hoveredLink(std::shared_ptr link) { m_hoveredLink = link; } + /** * @brief
Convert coordinates from screen to grid * @param p Point in screen coordinates to be converted @@ -511,16 +637,98 @@ namespace ImFlow */ bool on_free_space(); + /** + * @brief
Get the link currently being hovered by the mouse + * @return Weak pointer to the hovered link, or empty weak_ptr if none + */ + std::weak_ptr getHoveredLink(); + + /** + * @brief
Add a waypoint to a link at the given position + * @param link The link to add the waypoint to + * @param pos Position in grid coordinates + * @return Index of the newly created waypoint + */ + int addWaypointToLink(std::shared_ptr link, const ImVec2& pos); + + /** + * @brief
Get the link that was hovered when right-click menu opened + * @return Shared pointer to the captured hovered link + */ + std::shared_ptr getRightClickHoveredLink() { return m_hoveredLinkAux; } + /** * @brief
Get recursion blacklist for nodes * @return Reference to blacklist */ std::vector& get_recursion_blacklist() { return m_pinRecursionBlacklist; } + + // ===== GROUP MANAGEMENT ===== + + /** + * @brief Create a new group from currently selected nodes + * @param name Name of the group + * @return UID of the created group, or 0 if no nodes selected + */ + NodeGroup::GroupUID createGroupFromSelection(const std::string& name = "Group"); + + /** + * @brief Add a node to an existing group + * @param groupUid UID of the group + * @param nodeUid UID of the node to add + */ + void addNodeToGroup(NodeGroup::GroupUID groupUid, NodeUID nodeUid); + + /** + * @brief Remove a node from its group + * @param nodeUid UID of the node to remove + */ + void removeNodeFromGroup(NodeUID nodeUid); + + /** + * @brief Delete a group (nodes remain) + * @param groupUid UID of the group to delete + */ + void deleteGroup(NodeGroup::GroupUID groupUid); + + /** + * @brief Get all groups + * @return Reference to map of groups + */ + std::unordered_map& getGroups() { return m_groups; } + const std::unordered_map& getGroups() const { return m_groups; } + + NodeGroup::GroupUID getNextGroupUid() const { return m_nextGroupUid; } + void setNextGroupUid(NodeGroup::GroupUID uid) { m_nextGroupUid = uid; } + + /** + * @brief Find which group a node belongs to + * @param nodeUid UID of the node + * @return Pointer to group, or nullptr if not in any group + */ + NodeGroup* findGroupForNode(NodeUID nodeUid); + + /** + * @brief Get selected group + * @return Pointer to selected group or nullptr + */ + NodeGroup* getSelectedGroup() { return m_selectedGroup; } + + void clearGroupSelection() + { + if (m_selectedGroup) + { + m_selectedGroup->selected = false; + m_selectedGroup = nullptr; + } + } + private: std::string m_name; ContainedContext m_context; bool m_singleUseClick = false; + bool m_doubleUseClick = false; std::unordered_map> m_nodes; std::vector m_pinRecursionBlacklist; @@ -531,13 +739,27 @@ namespace ImFlow Pin* m_droppedLinkLeft = nullptr; std::function m_rightClickPopUp; BaseNode* m_hoveredNodeAux = nullptr; + std::shared_ptr m_hoveredLinkAux; // Captured hovered link when right-click menu opens BaseNode* m_hoveredNode = nullptr; bool m_draggingNode = false, m_draggingNodeNext = false; Pin* m_hovering = nullptr; Pin* m_dragOut = nullptr; + std::weak_ptr m_hoveredLink; // Currently hovered link for waypoint creation InfStyler m_style; + + // Box selection + bool m_boxSelecting = false; + ImVec2 m_boxSelectStart = ImVec2(0, 0); + ImU32 m_boxSelectColor = IM_COL32(100, 150, 255, 50); + ImU32 m_boxSelectBorderColor = IM_COL32(100, 150, 255, 200); + + // Groups + std::unordered_map m_groups; + NodeGroup::GroupUID m_nextGroupUid = 1; + NodeGroup* m_selectedGroup = nullptr; + NodeGroup* m_hoveredGroup = nullptr; }; // ----------------------------------------------------------------------------------------------------------------- @@ -895,6 +1117,18 @@ namespace ImFlow * @brief
Update the isSelected status of the node */ void updatePublicStatus() { m_selected = m_selectedNext; } + + /** + * @brief
Set node's flipped status (horizontal flip of inputs/outputs) + * @param flipped If true, inputs will be on the right and outputs on the left + */ + BaseNode* setFlipped(bool flipped) { m_flipped = flipped; return this; } + + /** + * @brief
Get node's flipped status + * @return [TRUE] if the node is flipped horizontally + */ + [[nodiscard]] bool isFlipped() const { return m_flipped; } private: NodeUID m_uid = 0; std::string m_title; @@ -906,6 +1140,7 @@ namespace ImFlow bool m_selected = false, m_selectedNext = false; bool m_dragged = false; bool m_destroyed = false; + bool m_flipped = false; std::vector> m_ins; std::vector>> m_dynamicIns; @@ -992,6 +1227,12 @@ namespace ImFlow * @brief
Delete link reference */ virtual void deleteLink() = 0; + + /** + * @brief
Invalidate all links connected to this pin + * @details Called before pin destruction to prevent dangling pointer access + */ + virtual void invalidateAllLinks() = 0; /** * @brief
Get connected status @@ -1065,11 +1306,31 @@ namespace ImFlow */ float calcWidth() { return ImGui::CalcTextSize(m_name.c_str()).x; } + /** + * @brief
Get tangent direction for link rendering + * @return Normalized direction vector for bezier tangent (accounts for flip) + */ + ImVec2 getTangentDirection(); + /** * @brief
Set pin's position * @param pos Position in screen coordinates */ void setPos(ImVec2 pos) { m_pos = pos; } + + /** + * @brief
Get socket hit bounds for interaction + * @param expand_radius Optional radius to expand the socket hitbox (default uses socket_hovered_radius) + * @return Rectangle bounds for socket interaction + */ + std::pair getSocketHitBounds(float expand_radius = -1.0f); + + /** + * @brief
Enable/disable automatic socket hitbox extension + * @param enabled If true, socket area will be included in pin hitbox + */ + void setSocketHitboxEnabled(bool enabled) { m_socketHitboxEnabled = enabled; } + protected: PinUID m_uid; std::string m_name; @@ -1080,6 +1341,7 @@ namespace ImFlow ImNodeFlow** m_inf; std::shared_ptr m_style; std::function m_renderer; + bool m_socketHitboxEnabled = true; }; /** @@ -1122,7 +1384,21 @@ namespace ImFlow /** * @brief
Delete the link connected to the pin */ - void deleteLink() override { m_link.reset(); } + void deleteLink() override; + + /** + * @brief
Invalidate all links connected to this pin + */ + void invalidateAllLinks() override { + if (m_link) { + m_link->invalidate(); + } + for (auto& l : m_links) { + if (l) { + l->invalidate(); + } + } + } /** * @brief Specify if connections from an output on the same node are allowed @@ -1130,17 +1406,35 @@ namespace ImFlow */ void allowSameNodeConnections(bool state) { m_allowSelfConnection = state; } + /** + * @brief
Allow multiple incoming links on this input pin + * @param state New state of the flag + */ + void allowMultipleLinks(bool state) { m_allowMultipleLinks = state; } + /** * @brief
Get connected status * @return [TRUE] is pin is connected to a link */ - bool isConnected() override { return m_link != nullptr; } + bool isConnected() override { return m_allowMultipleLinks ? !m_links.empty() : m_link != nullptr; } /** - * @brief
Get pin's link + * @brief
Get pin's link (first link if multiple) * @return Weak_ptr reference to the link connected to the pin */ - std::weak_ptr getLink() override { return m_link; } + std::weak_ptr getLink() override { return m_allowMultipleLinks ? (m_links.empty() ? std::weak_ptr{} : m_links.front()) : m_link; } + + /** + * @brief
Get all links connected to this pin + * @return Vector of weak_ptr references to all links + */ + const std::vector>& getLinks() const { return m_links; } + + /** + * @brief
Set link reference (used internally for multi-link support) + * @param link Smart pointer to the link + */ + void setLink(std::shared_ptr& link) override; /** * @brief
Get InPin's connection filter @@ -1158,18 +1452,45 @@ namespace ImFlow * @brief
Get pin's link attachment point (socket) * @return Grid coordinates to the attachment point between the link and the pin's socket */ - ImVec2 pinPoint() override { return m_pos + ImVec2(-m_style->extra.socket_padding, m_size.y / 2); } + ImVec2 pinPoint() override { + if (m_parent->isFlipped()) { + // Flipped: socket on right side of pin (pin is right-aligned to node edge) + return m_pos + ImVec2(m_size.x + m_style->extra.socket_padding, m_size.y / 2); + } else { + // Normal: socket on left side of pin + return m_pos + ImVec2(-m_style->extra.socket_padding, m_size.y / 2); + } + } /** * @brief
Get value carried by the connected link * @return Reference to the value of the connected OutPin. Or the default value if not connected */ const T& val(); + + /** + * @brief
Destructor - disable cleanup on links before destroying + */ + ~InPin() override { + // Disable cleanup on all links before destroying them + if (m_link) { + m_link->disableCleanup(); + m_link.reset(); + } + for (auto& l : m_links) { + if (l) { + l->disableCleanup(); + } + } + m_links.clear(); + } private: - std::shared_ptr m_link; + std::shared_ptr m_link; // Single link mode + std::vector> m_links; // Multiple links mode T m_emptyVal; std::function m_filter; bool m_allowSelfConnection = false; + bool m_allowMultipleLinks = false; }; /** @@ -1195,8 +1516,14 @@ namespace ImFlow * @brief
When parent gets deleted, remove the links */ ~OutPin() override { - std::vector> links = std::move(m_links); - for (auto &l: links) if (!l.expired()) l.lock()->right()->deleteLink(); + // Disable cleanup on all links before destroying them + // to avoid calling deleteLink() on already-destroyed pins + for (auto &l: m_links) { + if (!l.expired()) { + l.lock()->disableCleanup(); + } + } + m_links.clear(); } /** @@ -1215,6 +1542,17 @@ namespace ImFlow * @brief
Delete any expired weak pointers to a (now deleted) link */ void deleteLink() override; + + /** + * @brief
Invalidate all links connected to this pin + */ + void invalidateAllLinks() override { + for (auto& l : m_links) { + if (!l.expired()) { + l.lock()->invalidate(); + } + } + } /** * @brief
Get connected status @@ -1226,7 +1564,13 @@ namespace ImFlow * @brief
Get pin's link attachment point (socket) * @return Grid coordinates to the attachment point between the link and the pin's socket */ - ImVec2 pinPoint() override { return m_pos + ImVec2(m_size.x + m_style->extra.socket_padding, m_size.y / 2); } + ImVec2 pinPoint() override { + // If parent is flipped, socket is on the left; otherwise on the right + float x = m_parent->isFlipped() + ? -m_style->extra.socket_padding // LEFT side + : m_size.x + m_style->extra.socket_padding; // RIGHT side + return m_pos + ImVec2(x, m_size.y / 2); + } /** * @brief
Get output value diff --git a/src/ImNodeFlow.cpp b/src/ImNodeFlow.cpp index 2f49467..58b1ae6 100644 --- a/src/ImNodeFlow.cpp +++ b/src/ImNodeFlow.cpp @@ -4,35 +4,288 @@ namespace ImFlow { // ----------------------------------------------------------------------------------------------------------------- // LINK + // Helper function to check if a point is near a line segment + static bool pointNearLineSegment(const ImVec2& p, const ImVec2& a, const ImVec2& b, float threshold) { + ImVec2 ab = ImVec2(b.x - a.x, b.y - a.y); + ImVec2 ap = ImVec2(p.x - a.x, p.y - a.y); + float ab_len_sq = ab.x * ab.x + ab.y * ab.y; + if (ab_len_sq < 0.001f) return false; + float t = (ap.x * ab.x + ap.y * ab.y) / ab_len_sq; + t = ImClamp(t, 0.0f, 1.0f); + ImVec2 closest = ImVec2(a.x + t * ab.x, a.y + t * ab.y); + float dist_sq = (p.x - closest.x) * (p.x - closest.x) + (p.y - closest.y) * (p.y - closest.y); + return dist_sq < (threshold * threshold); + } + + int Link::addWaypoint(const ImVec2& pos) { + // pos is in grid coordinates + // start/end from pinPoint() are in screen coordinates - convert to grid + ImVec2 startScreen = m_left->pinPoint(); + ImVec2 endScreen = m_right->pinPoint(); + ImVec2 start = m_inf->screen2grid(startScreen); + ImVec2 end = m_inf->screen2grid(endScreen); + + // Build list of all points in grid coordinates + std::vector points; + points.push_back(start); + for (const auto& wp : m_waypoints) { + points.push_back(wp.pos); + } + points.push_back(end); + + // Find which segment the position is closest to + int bestSegment = -1; + float bestDist = FLT_MAX; + for (size_t i = 0; i < points.size() - 1; i++) { + ImVec2 a = points[i]; + ImVec2 b = points[i + 1]; + // Project pos onto segment [a, b] + ImVec2 ab = ImVec2(b.x - a.x, b.y - a.y); + ImVec2 ap = ImVec2(pos.x - a.x, pos.y - a.y); + float ab_len_sq = ab.x * ab.x + ab.y * ab.y; + if (ab_len_sq < 0.001f) continue; + float t = (ap.x * ab.x + ap.y * ab.y) / ab_len_sq; + t = ImClamp(t, 0.0f, 1.0f); + ImVec2 closest = ImVec2(a.x + t * ab.x, a.y + t * ab.y); + float dist = sqrtf((pos.x - closest.x) * (pos.x - closest.x) + (pos.y - closest.y) * (pos.y - closest.y)); + if (dist < bestDist) { + bestDist = dist; + bestSegment = i; + } + } + + Waypoint wp; + wp.pos = pos; + wp.posTarget = pos; // Initialize target to same position + if (bestSegment >= 0 && bestSegment < (int)m_waypoints.size()) { + m_waypoints.insert(m_waypoints.begin() + bestSegment + 1, wp); + return bestSegment + 1; + } else { + m_waypoints.push_back(wp); + return (int)m_waypoints.size() - 1; + } + } + + void Link::removeWaypoint(int index) { + if (index >= 0 && index < (int)m_waypoints.size()) { + m_waypoints.erase(m_waypoints.begin() + index); + } + } + + void Link::setWaypoints(const std::vector& positions) { + m_waypoints.clear(); + for (const auto& pos : positions) { + Waypoint wp; + wp.pos = pos; + wp.posTarget = pos; // Initialize target to same position + m_waypoints.push_back(wp); + } + } + + int Link::getHoveredWaypoint() const { + ImVec2 mousePos = m_inf->screen2grid(ImGui::GetMousePos()); + for (int i = 0; i < (int)m_waypoints.size(); i++) { + float dist = sqrtf((mousePos.x - m_waypoints[i].pos.x) * (mousePos.x - m_waypoints[i].pos.x) + + (mousePos.y - m_waypoints[i].pos.y) * (mousePos.y - m_waypoints[i].pos.y)); + if (dist < Waypoint::HOVER_RADIUS) { + return i; + } + } + return -1; + } + void Link::update() { - ImVec2 start = m_left->pinPoint(); - ImVec2 end = m_right->pinPoint(); + // Skip update if link has been invalidated (pins destroyed) + if (!m_valid) return; + + // start/end from pinPoint() are in screen coordinates - convert to grid + ImVec2 startScreen = m_left->pinPoint(); + ImVec2 endScreen = m_right->pinPoint(); + ImVec2 start = m_inf->screen2grid(startScreen); + ImVec2 end = m_inf->screen2grid(endScreen); + float thickness = m_left->getStyle()->extra.link_thickness; bool mouseClickState = m_inf->getSingleUseClick(); + ImVec2 mousePos = ImGui::GetMousePos(); + ImVec2 mouseGridPos = m_inf->screen2grid(mousePos); + + // Build list of all points (start, waypoints, end) - all in GRID coordinates + std::vector points; + points.push_back(start); + for (auto& wp : m_waypoints) { + points.push_back(wp.pos); + } + points.push_back(end); + + // Helper to normalize a vector + auto normalize = [](const ImVec2& v) -> ImVec2 { + float len = sqrt(v.x * v.x + v.y * v.y); + if (len < 0.001f) return ImVec2(0, 0); + return ImVec2(v.x / len, v.y / len); + }; + + // Calculate tangents for each segment based on neighboring points + // tangent1[i] = outgoing tangent at points[i] + // tangent2[i] = incoming tangent at points[i+1] + std::vector tangent1(points.size() - 1); + std::vector tangent2(points.size() - 1); + + for (size_t i = 0; i < points.size() - 1; i++) { + ImVec2 curr = points[i]; + ImVec2 next = points[i + 1]; + + // For the first point (output pin), use pin's tangent direction (accounts for flip) + if (i == 0) { + tangent1[i] = m_left->getTangentDirection(); + } else { + // For waypoints, use direction from previous to next point for smooth curves + ImVec2 prev = points[i - 1]; + tangent1[i] = normalize(ImVec2(next.x - prev.x, next.y - prev.y)); + } + + // For the last point (input pin), use pin's tangent direction (accounts for flip) + if (i == points.size() - 2) { + tangent2[i] = m_right->getTangentDirection(); + } else { + // For waypoints, use direction from current to next-next point + ImVec2 nextnext = points[i + 2]; + tangent2[i] = normalize(ImVec2(nextnext.x - curr.x, nextnext.y - curr.y)); + } + } + + // Check if link is hovered (any segment) - use screen coordinates for collision + m_hovered = false; + for (size_t i = 0; i < points.size() - 1; i++) { + bool hit = false; + // Always use tangent-aware collision to respect pin directions (flip mode) + hit = smart_bezier_collider_with_tangents(mousePos, + m_inf->grid2screen(points[i]), m_inf->grid2screen(points[i + 1]), + tangent1[i], tangent2[i], 2.5); + if (hit) { + m_hovered = true; + thickness = m_left->getStyle()->extra.link_hovered_thickness; + if (mouseClickState && getHoveredWaypoint() < 0) { + m_inf->consumeSingleUseClick(); + m_selected = true; + } + break; + } + } + + // Handle waypoint interactions + int hoveredWp = getHoveredWaypoint(); + + // Update hovered waypoint + for (size_t i = 0; i < m_waypoints.size(); i++) { + m_waypoints[i].hovered = (i == hoveredWp); + } + + // Start dragging waypoint + if (hoveredWp >= 0 && mouseClickState) { + m_draggedWaypointIndex = hoveredWp; + m_waypoints[hoveredWp].dragged = true; + m_waypoints[hoveredWp].posTarget = m_waypoints[hoveredWp].pos; // Initialize target + m_inf->consumeSingleUseClick(); + } + + // Drag waypoint + if (m_draggedWaypointIndex >= 0) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + float step = m_inf->getStyle().grid_size / m_inf->getStyle().grid_subdivisions; + // Apply delta to target position (not snapped) + m_waypoints[m_draggedWaypointIndex].posTarget += m_inf->getScreenSpaceDelta(); + // Snap to grid for display + m_waypoints[m_draggedWaypointIndex].pos.x = round(m_waypoints[m_draggedWaypointIndex].posTarget.x / step) * step; + m_waypoints[m_draggedWaypointIndex].pos.y = round(m_waypoints[m_draggedWaypointIndex].posTarget.y / step) * step; + } else { + m_waypoints[m_draggedWaypointIndex].dragged = false; + m_waypoints[m_draggedWaypointIndex].posTarget = m_waypoints[m_draggedWaypointIndex].pos; // Sync target + m_draggedWaypointIndex = -1; + } + } - if (!ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + // Delete waypoint on Delete key when hovered + if (hoveredWp >= 0 && ImGui::IsKeyPressed(ImGuiKey_Delete, false)) { + removeWaypoint(hoveredWp); + } + // Delete selected link on Delete key (only if no waypoint is hovered) + else if (m_selected && hoveredWp < 0 && ImGui::IsKeyPressed(ImGuiKey_Delete, false)) { + // Invalidate the link - this will cause it to be cleaned up + m_valid = false; + m_right->deleteLink(); + } + + // Deselect on click elsewhere + if (!ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !m_hovered && m_draggedWaypointIndex < 0) { m_selected = false; + } - if (smart_bezier_collider(ImGui::GetMousePos(), start, end, 2.5)) { - m_hovered = true; - thickness = m_left->getStyle()->extra.link_hovered_thickness; - if (mouseClickState) { - m_inf->consumeSingleUseClick(); - m_selected = true; + // Draw the link segments + ImU32 color = m_left->getStyle()->color; + ImU32 outlineColor = m_left->getStyle()->extra.outline_color; + + // Rebuild points in case waypoints moved (still in GRID coords) + points.clear(); + points.push_back(start); + for (auto& wp : m_waypoints) { + points.push_back(wp.pos); + } + points.push_back(end); + + // Recalculate tangents after points rebuild + tangent1.resize(points.size() - 1); + tangent2.resize(points.size() - 1); + for (size_t i = 0; i < points.size() - 1; i++) { + ImVec2 curr = points[i]; + ImVec2 next = points[i + 1]; + + if (i == 0) { + tangent1[i] = m_left->getTangentDirection(); + } else { + ImVec2 prev = points[i - 1]; + tangent1[i] = normalize(ImVec2(next.x - prev.x, next.y - prev.y)); + } + + if (i == points.size() - 2) { + tangent2[i] = m_right->getTangentDirection(); + } else { + ImVec2 nextnext = points[i + 2]; + tangent2[i] = normalize(ImVec2(nextnext.x - curr.x, nextnext.y - curr.y)); } - } else { m_hovered = false; } + } - if (m_selected) - smart_bezier(start, end, m_left->getStyle()->extra.outline_color, - thickness + m_left->getStyle()->extra.link_selected_outline_thickness); - smart_bezier(start, end, m_left->getStyle()->color, thickness); + // Draw outline if selected - convert to screen for drawing + if (m_selected) { + for (size_t i = 0; i < points.size() - 1; i++) { + smart_bezier_with_tangents(m_inf->grid2screen(points[i]), m_inf->grid2screen(points[i + 1]), + tangent1[i], tangent2[i], outlineColor, + thickness + m_left->getStyle()->extra.link_selected_outline_thickness); + } + } - if (m_selected && ImGui::IsKeyPressed(ImGuiKey_Delete, false)) - m_right->deleteLink(); + // Draw segments - convert to screen for drawing + for (size_t i = 0; i < points.size() - 1; i++) { + smart_bezier_with_tangents(m_inf->grid2screen(points[i]), m_inf->grid2screen(points[i + 1]), + tangent1[i], tangent2[i], color, thickness); + } + + // Draw waypoints - convert to screen for drawing + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + for (auto& wp : m_waypoints) { + ImVec2 screenPos = m_inf->grid2screen(wp.pos); + float radius = Waypoint::RADIUS; + if (wp.hovered || wp.dragged) { + radius = Waypoint::HOVER_RADIUS; + } + draw_list->AddCircleFilled(screenPos, radius, color); + if (wp.hovered || wp.dragged) { + draw_list->AddCircle(screenPos, radius, outlineColor, 12, 2.0f); + } + } } Link::~Link() { - if (!m_left) return; + if (!m_cleanupEnabled || !m_left) return; m_left->deleteLink(); } @@ -67,14 +320,17 @@ namespace ImFlow { float headerH = ImGui::GetItemRectSize().y; float titleW = ImGui::GetItemRectSize().x; - // Inputs - if (!m_ins.empty() || !m_dynamicIns.empty()) { + // Render order depends on flipped state + if (m_flipped) { + // FLIPPED: Outputs on LEFT, Content, Inputs on RIGHT + + // Outputs (left side when flipped) - align to left edge ImGui::BeginGroup(); - for (auto &p: m_ins) { + for (auto &p: m_outs) { p->setPos(ImGui::GetCursorPos()); p->update(); } - for (auto &p: m_dynamicIns) { + for (auto &p: m_dynamicOuts) { if (p.first == 1) { p.second->setPos(ImGui::GetCursorPos()); p.second->update(); @@ -83,53 +339,114 @@ namespace ImFlow { } ImGui::EndGroup(); ImGui::SameLine(); - } - // Content - ImGui::BeginGroup(); - draw(); - ImGui::Dummy(ImVec2(0.f, 0.f)); - ImGui::EndGroup(); - ImGui::SameLine(); + // Content (center) + ImGui::BeginGroup(); + draw(); + ImGui::Dummy(ImVec2(0.f, 0.f)); + ImGui::EndGroup(); + ImGui::SameLine(); - // Outputs - float maxW = 0.0f; - for (auto &p: m_outs) { - float w = p->calcWidth(); - if (w > maxW) - maxW = w; - } - for (auto &p: m_dynamicOuts) { - float w = p.second->calcWidth(); - if (w > maxW) - maxW = w; - } - ImGui::BeginGroup(); - for (auto &p: m_outs) { - // FIXME: This looks horrible - if ((m_pos + ImVec2(titleW, 0) + m_inf->getGrid().scroll()).x < - ImGui::GetCursorPos().x + ImGui::GetWindowPos().x + maxW) - p->setPos(ImGui::GetCursorPos() + ImGui::GetWindowPos() + ImVec2(maxW - p->calcWidth(), 0.f)); - else - p->setPos(ImVec2((m_pos + ImVec2(titleW - p->calcWidth(), 0) + m_inf->getGrid().scroll()).x, - ImGui::GetCursorPos().y + ImGui::GetWindowPos().y)); - p->update(); - } - for (auto &p: m_dynamicOuts) { - // FIXME: This looks horrible - if ((m_pos + ImVec2(titleW, 0) + m_inf->getGrid().scroll()).x < - ImGui::GetCursorPos().x + ImGui::GetWindowPos().x + maxW) - p.second->setPos( - ImGui::GetCursorPos() + ImGui::GetWindowPos() + ImVec2(maxW - p.second->calcWidth(), 0.f)); - else - p.second->setPos( - ImVec2((m_pos + ImVec2(titleW - p.second->calcWidth(), 0) + m_inf->getGrid().scroll()).x, - ImGui::GetCursorPos().y + ImGui::GetWindowPos().y)); - p.second->update(); - p.first -= 1; - } + // Inputs (right side when flipped) - right-align to node edge + if (!m_ins.empty() || !m_dynamicIns.empty()) { + float maxInW = 0.0f; + for (auto &p: m_ins) { + float w = p->getSize().x; + if (w > maxInW) + maxInW = w; + } + for (auto &p: m_dynamicIns) { + float w = p.second->getSize().x; + if (w > maxInW) + maxInW = w; + } + ImGui::BeginGroup(); + for (auto &p: m_ins) { + float pinW = p->getSize().x; + if (pinW > 0.f && maxInW > pinW) + p->setPos(ImGui::GetCursorPos() + ImVec2(maxInW - pinW, 0.f)); + else + p->setPos(ImGui::GetCursorPos()); + p->update(); + } + for (auto &p: m_dynamicIns) { + if (p.first == 1) { + float pinW = p.second->getSize().x; + if (pinW > 0.f && maxInW > pinW) + p.second->setPos(ImGui::GetCursorPos() + ImVec2(maxInW - pinW, 0.f)); + else + p.second->setPos(ImGui::GetCursorPos()); + p.second->update(); + p.first = 0; + } + } + ImGui::EndGroup(); + } - ImGui::EndGroup(); + } else { + // NORMAL: Inputs on LEFT, Content, Outputs on RIGHT + + // Inputs + if (!m_ins.empty() || !m_dynamicIns.empty()) { + ImGui::BeginGroup(); + for (auto &p: m_ins) { + p->setPos(ImGui::GetCursorPos()); + p->update(); + } + for (auto &p: m_dynamicIns) { + if (p.first == 1) { + p.second->setPos(ImGui::GetCursorPos()); + p.second->update(); + p.first = 0; + } + } + ImGui::EndGroup(); + ImGui::SameLine(); + } + + // Content + ImGui::BeginGroup(); + draw(); + ImGui::Dummy(ImVec2(0.f, 0.f)); + ImGui::EndGroup(); + ImGui::SameLine(); + + // Outputs + float maxW = 0.0f; + for (auto &p: m_outs) { + float w = p->calcWidth(); + if (w > maxW) + maxW = w; + } + for (auto &p: m_dynamicOuts) { + float w = p.second->calcWidth(); + if (w > maxW) + maxW = w; + } + ImGui::BeginGroup(); + for (auto &p: m_outs) { + if ((m_pos + ImVec2(titleW, 0) + m_inf->getGrid().scroll()).x < + ImGui::GetCursorPos().x + ImGui::GetWindowPos().x + maxW) + p->setPos(ImGui::GetCursorPos() + ImGui::GetWindowPos() + ImVec2(maxW - p->calcWidth(), 0.f)); + else + p->setPos(ImVec2((m_pos + ImVec2(titleW - p->calcWidth(), 0) + m_inf->getGrid().scroll()).x, + ImGui::GetCursorPos().y + ImGui::GetWindowPos().y)); + p->update(); + } + for (auto &p: m_dynamicOuts) { + if ((m_pos + ImVec2(titleW, 0) + m_inf->getGrid().scroll()).x < + ImGui::GetCursorPos().x + ImGui::GetWindowPos().x + maxW) + p.second->setPos( + ImGui::GetCursorPos() + ImGui::GetWindowPos() + ImVec2(maxW - p.second->calcWidth(), 0.f)); + else + p.second->setPos( + ImVec2((m_pos + ImVec2(titleW - p.second->calcWidth(), 0) + m_inf->getGrid().scroll()).x, + ImGui::GetCursorPos().y + ImGui::GetWindowPos().y)); + p.second->update(); + p.first -= 1; + } + ImGui::EndGroup(); + } ImGui::EndGroup(); m_size = ImGui::GetItemRectSize(); @@ -167,6 +484,8 @@ namespace ImFlow { if (isHovered()) { m_inf->hoveredNode(this); if (mouseClickState) { + // Selecting a node should deselect any selected group (exclusive selection) + m_inf->clearGroupSelection(); selected(true); m_inf->consumeSingleUseClick(); } @@ -219,7 +538,27 @@ namespace ImFlow { return std::all_of(m_nodes.begin(), m_nodes.end(), [](const auto &n) { return !n.second->isHovered(); }) && std::all_of(m_links.begin(), m_links.end(), - [](const auto &l) { return !l.lock()->isHovered(); }); + [](const auto &l) { + auto link = l.lock(); + return !link || (!link->isHovered() && link->getHoveredWaypoint() < 0); + }); + } + + std::weak_ptr ImNodeFlow::getHoveredLink() { + for (auto& l : m_links) { + auto link = l.lock(); + if (link && link->isHovered()) { + return l; + } + } + return std::weak_ptr(); + } + + int ImNodeFlow::addWaypointToLink(std::shared_ptr link, const ImVec2& pos) { + if (link) { + return link->addWaypoint(pos); + } + return -1; } ImVec2 ImNodeFlow::screen2grid( const ImVec2 & p ) @@ -244,8 +583,10 @@ namespace ImFlow { // Updating looping stuff m_hovering = nullptr; m_hoveredNode = nullptr; + m_hoveredLink.reset(); // Reset hovered link each frame m_draggingNode = m_draggingNodeNext; m_singleUseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + m_doubleUseClick = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left); // Create child canvas m_context.begin(); @@ -267,14 +608,128 @@ namespace ImFlow { draw_list->AddLine(ImVec2(0.0f, y), ImVec2(gridSize.x, y), m_style.colors.subGrid); } + // Update and draw groups (background) + m_hoveredGroup = nullptr; + for (auto& [gid, group] : m_groups) { + if (group.members.empty()) continue; + + // Calculate bounding box of all member nodes + ImVec2 minPos(FLT_MAX, FLT_MAX); + ImVec2 maxPos(-FLT_MAX, -FLT_MAX); + bool hasValidNodes = false; + + for (NodeUID nodeUid : group.members) { + auto it = m_nodes.find(nodeUid); + if (it == m_nodes.end()) continue; + + hasValidNodes = true; + ImVec2 nodePos = it->second->getPos(); + ImVec2 nodeSize = it->second->getSize(); + + minPos.x = std::min(minPos.x, nodePos.x); + minPos.y = std::min(minPos.y, nodePos.y); + maxPos.x = std::max(maxPos.x, nodePos.x + nodeSize.x); + maxPos.y = std::max(maxPos.y, nodePos.y + nodeSize.y); + } + + if (!hasValidNodes) continue; + + // Add padding + minPos.x -= group.padding; + minPos.y -= group.padding + 20; // Extra space for title + maxPos.x += group.padding; + maxPos.y += group.padding; + + // Convert to screen coordinates + ImVec2 screenMin = grid2screen(minPos); + ImVec2 screenMax = grid2screen(maxPos); + + // Check if hovered + ImVec2 mousePos = ImGui::GetMousePos(); + group.hovered = (mousePos.x >= screenMin.x && mousePos.x <= screenMax.x && + mousePos.y >= screenMin.y && mousePos.y <= screenMax.y); + if (group.hovered) { + m_hoveredGroup = &group; + } + + // Draw group background + ImU32 bgColor = group.color; + ImU32 borderCol = group.borderColor; + if (group.selected) { + borderCol = IM_COL32(255, 200, 100, 255); + } else if (group.hovered) { + bgColor = IM_COL32( + (group.color & 0xFF), + ((group.color >> 8) & 0xFF), + ((group.color >> 16) & 0xFF), + std::min(255, (int)((group.color >> 24) & 0xFF) + 30) + ); + } + + draw_list->AddRectFilled(screenMin, screenMax, bgColor, 8.0f); + draw_list->AddRect(screenMin, screenMax, borderCol, 8.0f, 0, 2.0f); + + // Draw group name + ImVec2 textPos(screenMin.x + 8, screenMin.y + 4); + draw_list->AddText(textPos, IM_COL32(255, 255, 255, 200), group.name.c_str()); + + // Handle group dragging + if (group.hovered && !m_hoveredNode && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + group.dragging = true; + group.dragOffset = ImVec2(mousePos.x - screenMin.x, mousePos.y - screenMin.y); + m_selectedGroup = &group; + // Selecting a group should deselect all nodes (exclusive selection) + for (auto& [nid, node] : m_nodes) { + node->selected(false); + } + // Deselect other groups + for (auto& [oid, other] : m_groups) { + if (oid != gid) other.selected = false; + } + group.selected = true; + } + + if (group.dragging) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + // Calculate delta in grid coordinates + ImVec2 delta = getScreenSpaceDelta(); + + // Move all member nodes + for (NodeUID nodeUid : group.members) { + auto it = m_nodes.find(nodeUid); + if (it != m_nodes.end()) { + ImVec2 nodePos = it->second->getPos(); + it->second->setPos(nodePos + screen2grid(delta) - screen2grid(ImVec2(0,0))); + } + } + } else { + group.dragging = false; + } + } + } + + // Delete selected group on Delete key + if (m_selectedGroup && ImGui::IsKeyPressed(ImGuiKey_Delete, false) && !m_hoveredNode) { + deleteGroup(m_selectedGroup->uid); + } + // Update and draw nodes // TODO: I don't like this draw_list->ChannelsSplit(2); for (auto &node: m_nodes) { node.second->update(); } // Remove "toDelete" nodes for (auto iter = m_nodes.begin(); iter != m_nodes.end();) { - if (iter->second->toDestroy()) + if (iter->second->toDestroy()) { + // Invalidate all links on all pins BEFORE destroying the node + // to prevent dangling pointer access in Link::update() + for (auto& pin : iter->second->getIns()) { + pin->invalidateAllLinks(); + } + for (auto& pin : iter->second->getOuts()) { + pin->invalidateAllLinks(); + } iter = m_nodes.erase(iter); + } else ++iter; } @@ -312,9 +767,59 @@ namespace ImFlow { m_dragOut = nullptr; } + // Box selection + // Note: groups are not considered by on_free_space(), so also exclude hovered group. + if (on_free_space() && !m_hoveredGroup && !m_draggingNode && !m_dragOut && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::IsWindowHovered()) { + m_boxSelecting = true; + m_boxSelectStart = ImGui::GetMousePos(); + // Deselect all nodes if not holding Ctrl + if (!ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && !ImGui::IsKeyDown(ImGuiKey_RightCtrl)) { + for (auto& node : m_nodes) { + node.second->selected(false); + } + // Also deselect any selected group when clicking on empty space + if (m_selectedGroup) { + m_selectedGroup->selected = false; + m_selectedGroup = nullptr; + } + } + } + + if (m_boxSelecting) { + ImVec2 boxEnd = ImGui::GetMousePos(); + ImVec2 boxMin = ImVec2(std::min(m_boxSelectStart.x, boxEnd.x), std::min(m_boxSelectStart.y, boxEnd.y)); + ImVec2 boxMax = ImVec2(std::max(m_boxSelectStart.x, boxEnd.x), std::max(m_boxSelectStart.y, boxEnd.y)); + + // Draw selection rectangle + draw_list->AddRectFilled(boxMin, boxMax, m_boxSelectColor); + draw_list->AddRect(boxMin, boxMax, m_boxSelectBorderColor, 0.0f, 0, 1.5f); + + // Select nodes that intersect with the box + for (auto& node : m_nodes) { + ImVec2 nodePos = grid2screen(node.second->getPos()); + ImVec2 nodeSize = node.second->getSize() * m_context.scale(); + ImVec2 nodeMin = nodePos; + ImVec2 nodeMax = nodePos + nodeSize; + + // Check if node intersects with selection box + bool intersects = !(nodeMax.x < boxMin.x || nodeMin.x > boxMax.x || + nodeMax.y < boxMin.y || nodeMin.y > boxMax.y); + + if (intersects) { + node.second->selected(true); + } + } + + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + m_boxSelecting = false; + } + } + // Right-click PopUp if (m_rightClickPopUp && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered()) { m_hoveredNodeAux = m_hoveredNode; + // Capture hovered link at the moment of right-click + m_hoveredLinkAux = getHoveredLink().lock(); ImGui::OpenPopup("RightClickPopUp"); } if (ImGui::BeginPopup("RightClickPopUp")) { @@ -335,6 +840,87 @@ namespace ImFlow { // Clearing recursion blacklist m_pinRecursionBlacklist.clear(); + // Ctrl+G to create group from selection + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_G, false) && ImGui::IsWindowFocused()) { + createGroupFromSelection(); + } + m_context.end(); } + + // ===== GROUP MANAGEMENT IMPLEMENTATION ===== + + NodeGroup::GroupUID ImNodeFlow::createGroupFromSelection(const std::string& name) + { + std::set selectedNodes; + for (auto& [uid, node] : m_nodes) { + if (node->isSelected()) { + selectedNodes.insert(uid); + } + } + + if (selectedNodes.empty()) { + return 0; + } + + // Remove nodes from any existing groups + for (NodeUID nodeUid : selectedNodes) { + removeNodeFromGroup(nodeUid); + } + + NodeGroup group; + group.uid = m_nextGroupUid++; + group.name = name; + group.members = selectedNodes; + + m_groups[group.uid] = group; + return group.uid; + } + + void ImNodeFlow::addNodeToGroup(NodeGroup::GroupUID groupUid, NodeUID nodeUid) + { + auto it = m_groups.find(groupUid); + if (it == m_groups.end()) return; + + // Remove from any existing group first + removeNodeFromGroup(nodeUid); + + it->second.members.insert(nodeUid); + } + + void ImNodeFlow::removeNodeFromGroup(NodeUID nodeUid) + { + for (auto& [gid, group] : m_groups) { + group.members.erase(nodeUid); + } + // Clean up empty groups + for (auto it = m_groups.begin(); it != m_groups.end();) { + if (it->second.members.empty()) { + it = m_groups.erase(it); + } else { + ++it; + } + } + } + + void ImNodeFlow::deleteGroup(NodeGroup::GroupUID groupUid) + { + m_groups.erase(groupUid); + if (m_selectedGroup && m_selectedGroup->uid == groupUid) { + m_selectedGroup = nullptr; + } + if (m_hoveredGroup && m_hoveredGroup->uid == groupUid) { + m_hoveredGroup = nullptr; + } + } + + NodeGroup* ImNodeFlow::findGroupForNode(NodeUID nodeUid) + { + for (auto& [gid, group] : m_groups) { + if (group.members.count(nodeUid) > 0) { + return &group; + } + } + return nullptr; + } } diff --git a/src/ImNodeFlow.inl b/src/ImNodeFlow.inl index 7546123..a57ea64 100644 --- a/src/ImNodeFlow.inl +++ b/src/ImNodeFlow.inl @@ -4,30 +4,80 @@ namespace ImFlow { - inline void smart_bezier(const ImVec2& p1, const ImVec2& p2, ImU32 color, float thickness) + // Helper to calculate bezier control points for pin-to-pin or simple segments + inline void calc_smart_bezier_controls(const ImVec2& p1, const ImVec2& p2, ImVec2& p11, ImVec2& p22) { - ImDrawList* dl = ImGui::GetWindowDrawList(); float distance = sqrt(pow((p2.x - p1.x), 2.f) + pow((p2.y - p1.y), 2.f)); float delta = distance * 0.45f; if (p2.x < p1.x) delta += 0.2f * (p1.x - p2.x); - // float vert = (p2.x < p1.x - 20.f) ? 0.062f * distance * (p2.y - p1.y) * 0.005f : 0.f; float vert = 0.f; - ImVec2 p22 = p2 - ImVec2(delta, vert); + p22 = p2 - ImVec2(delta, vert); if (p2.x < p1.x - 50.f) delta *= -1.f; - ImVec2 p11 = p1 + ImVec2(delta, vert); + p11 = p1 + ImVec2(delta, vert); + } + + inline void smart_bezier(const ImVec2& p1, const ImVec2& p2, ImU32 color, float thickness) + { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 p11, p22; + calc_smart_bezier_controls(p1, p2, p11, p22); dl->AddBezierCubic(p1, p11, p22, p2, color, thickness); } inline bool smart_bezier_collider(const ImVec2& p, const ImVec2& p1, const ImVec2& p2, float radius) { + ImVec2 p11, p22; + calc_smart_bezier_controls(p1, p2, p11, p22); + return ImProjectOnCubicBezier(p, p1, p11, p22, p2).Distance < radius; + } + + // Bezier with explicit tangent directions for waypoint segments + // tangent1: direction vector for outgoing tangent at p1 (normalized or zero) + // tangent2: direction vector for incoming tangent at p2 (normalized or zero) + inline void smart_bezier_with_tangents(const ImVec2& p1, const ImVec2& p2, + const ImVec2& tangent1, const ImVec2& tangent2, + ImU32 color, float thickness) + { + ImDrawList* dl = ImGui::GetWindowDrawList(); float distance = sqrt(pow((p2.x - p1.x), 2.f) + pow((p2.y - p1.y), 2.f)); - float delta = distance * 0.45f; - if (p2.x < p1.x) delta += 0.2f * (p1.x - p2.x); - // float vert = (p2.x < p1.x - 20.f) ? 0.062f * distance * (p2.y - p1.y) * 0.005f : 0.f; - float vert = 0.f; - ImVec2 p22 = p2 - ImVec2(delta, vert); - if (p2.x < p1.x - 50.f) delta *= -1.f; - ImVec2 p11 = p1 + ImVec2(delta, vert); + float controlLen = distance * 0.4f; + + // If tangents are provided, use them; otherwise fall back to horizontal + ImVec2 p11, p22; + if (tangent1.x != 0.f || tangent1.y != 0.f) { + p11 = p1 + ImVec2(tangent1.x * controlLen, tangent1.y * controlLen); + } else { + p11 = p1 + ImVec2(controlLen, 0.f); + } + + if (tangent2.x != 0.f || tangent2.y != 0.f) { + p22 = p2 - ImVec2(tangent2.x * controlLen, tangent2.y * controlLen); + } else { + p22 = p2 - ImVec2(controlLen, 0.f); + } + + dl->AddBezierCubic(p1, p11, p22, p2, color, thickness); + } + + inline bool smart_bezier_collider_with_tangents(const ImVec2& p, const ImVec2& p1, const ImVec2& p2, + const ImVec2& tangent1, const ImVec2& tangent2, float radius) + { + float distance = sqrt(pow((p2.x - p1.x), 2.f) + pow((p2.y - p1.y), 2.f)); + float controlLen = distance * 0.4f; + + ImVec2 p11, p22; + if (tangent1.x != 0.f || tangent1.y != 0.f) { + p11 = p1 + ImVec2(tangent1.x * controlLen, tangent1.y * controlLen); + } else { + p11 = p1 + ImVec2(controlLen, 0.f); + } + + if (tangent2.x != 0.f || tangent2.y != 0.f) { + p22 = p2 - ImVec2(tangent2.x * controlLen, tangent2.y * controlLen); + } else { + p22 = p2 - ImVec2(controlLen, 0.f); + } + return ImProjectOnCubicBezier(p, p1, p11, p22, p2).Distance < radius; } @@ -89,6 +139,8 @@ namespace ImFlow { if (it->get()->getUid() == h) { + // Invalidate ALL links before removing the pin to prevent dangling pointer access + it->get()->invalidateAllLinks(); m_ins.erase(it); return; } @@ -146,6 +198,8 @@ namespace ImFlow { if (it->get()->getUid() == h) { + // Invalidate ALL links before removing the pin to prevent dangling pointer access + it->get()->invalidateAllLinks(); m_outs.erase(it); return; } @@ -230,34 +284,84 @@ namespace ImFlow // ----------------------------------------------------------------------------------------------------------------- // PIN + inline ImVec2 Pin::getTangentDirection() + { + // Output pins: link goes OUT from the pin + // Input pins: link comes IN to the pin + // The tangent direction indicates where the bezier curve should go + bool isFlipped = m_parent->isFlipped(); + bool isOutput = (m_type == PinType_Output); + + // Normal mode: + // Output socket on RIGHT -> link goes right -> tangent (1,0) + // Input socket on LEFT -> link comes from left -> tangent (1,0) + // Flipped mode: + // Output socket on LEFT -> link goes left -> tangent (-1,0) + // Input socket on RIGHT -> link comes from right -> tangent (-1,0) + // For bezier control points: p22 = p2 - (tangent * controlLen) + // So for input in flipped mode, tangent (-1,0) gives p22 = p2 - (-controlLen, 0) = p2 + (controlLen, 0) + // This places control point to the RIGHT of input, making curve enter from outside + if (isOutput) { + return isFlipped ? ImVec2(-1.0f, 0.0f) : ImVec2(1.0f, 0.0f); + } else { + // Input: tangent points in direction link arrives from + return isFlipped ? ImVec2(-1.0f, 0.0f) : ImVec2(1.0f, 0.0f); + } + } + + inline std::pair Pin::getSocketHitBounds(float expand_radius) + { + if (expand_radius < 0.0f) + expand_radius = m_style->socket_hovered_radius; + + ImVec2 center = pinPoint(); + // Use a wider hitbox horizontally to cover the full arrow/socket shape + float hitW = expand_radius * 2.0f; + float hitH = expand_radius; + ImVec2 tl = center - ImVec2(hitW, hitH); + ImVec2 br = center + ImVec2(hitW, hitH); + + return {tl, br}; + } + inline void Pin::drawSocket() { ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 tl = pinPoint() - ImVec2(m_style->socket_radius, m_style->socket_radius); - ImVec2 br = pinPoint() + ImVec2(m_style->socket_radius, m_style->socket_radius); + auto [tl, br] = getSocketHitBounds(); + + // Déterminer si on est en hover (vérifié maintenant dans update()) + bool socketHovered = ImGui::IsMouseHoveringRect(tl, br); if (isConnected()) draw_list->AddCircleFilled(pinPoint(), m_style->socket_connected_radius, m_style->color, m_style->socket_shape); else { - if (ImGui::IsItemHovered() || ImGui::IsMouseHoveringRect(tl, br)) + if (socketHovered) draw_list->AddCircle(pinPoint(), m_style->socket_hovered_radius, m_style->color, m_style->socket_shape, m_style->socket_thickness); else draw_list->AddCircle(pinPoint(), m_style->socket_radius, m_style->color, m_style->socket_shape, m_style->socket_thickness); } - - if (ImGui::IsMouseHoveringRect(tl, br)) - (*m_inf)->hovering(this); } inline void Pin::drawDecoration() { ImDrawList* draw_list = ImGui::GetWindowDrawList(); - if (ImGui::IsItemHovered()) + // Vérifier hover sur le texte OU le socket + bool itemHovered = ImGui::IsItemHovered(); + bool socketHovered = false; + + if (m_socketHitboxEnabled) + { + auto [socket_tl, socket_br] = getSocketHitBounds(); + socketHovered = ImGui::IsMouseHoveringRect(socket_tl, socket_br); + } + + if (itemHovered || socketHovered) draw_list->AddRectFilled(m_pos - m_style->extra.padding, m_pos + m_size + m_style->extra.padding, m_style->extra.bg_hover_color, m_style->extra.bg_radius); else draw_list->AddRectFilled(m_pos - m_style->extra.padding, m_pos + m_size + m_style->extra.padding, m_style->extra.bg_color, m_style->extra.bg_radius); + draw_list->AddRect(m_pos - m_style->extra.padding, m_pos + m_size + m_style->extra.padding, m_style->extra.border_color, m_style->extra.bg_radius, 0, m_style->extra.border_thickness); } @@ -270,11 +374,24 @@ namespace ImFlow m_renderer(this); ImGui::EndGroup(); m_size = ImGui::GetItemRectSize(); - if (ImGui::IsItemHovered()) + + // For custom-rendered pins, only use socket hitbox for drop detection + // (the rendered widget is just a layout spacer, not the interactive area) + bool socketHovered = false; + + if (m_socketHitboxEnabled) + { + auto [socket_tl, socket_br] = getSocketHitBounds(); + socketHovered = ImGui::IsMouseHoveringRect(socket_tl, socket_br); + } + + if (socketHovered) (*m_inf)->hovering(this); + return; } + // Rendu standard ImGui::SetCursorPos(m_pos); ImGui::Text("%s", m_name.c_str()); m_size = ImGui::GetItemRectSize(); @@ -282,7 +399,17 @@ namespace ImFlow drawDecoration(); drawSocket(); - if (ImGui::IsItemHovered()) + // Vérifier le hover sur texte + socket + bool itemHovered = ImGui::IsItemHovered(); + bool socketHovered = false; + + if (m_socketHitboxEnabled) + { + auto [socket_tl, socket_br] = getSocketHitBounds(); + socketHovered = ImGui::IsMouseHoveringRect(socket_tl, socket_br); + } + + if (itemHovered || socketHovered) (*m_inf)->hovering(this); } @@ -298,6 +425,33 @@ namespace ImFlow return reinterpret_cast*>(m_link->left())->val(); } + template + void InPin::deleteLink() + { + if (m_allowMultipleLinks) + { + m_links.erase(std::remove_if(m_links.begin(), m_links.end(), + [](const std::shared_ptr& l) { return !l || !l->left() || !l->isValid(); }), m_links.end()); + } + else + { + m_link.reset(); + } + } + + template + void InPin::setLink(std::shared_ptr& link) + { + if (m_allowMultipleLinks) + { + m_links.emplace_back(link); + } + else + { + m_link = link; + } + } + template void InPin::createLink(Pin *other) { @@ -307,18 +461,45 @@ namespace ImFlow if (m_parent == other->getParent() && !m_allowSelfConnection) return; - if (m_link && m_link->left() == other) + if (m_allowMultipleLinks) { - m_link.reset(); - return; + // Check if already connected to this output + for (auto& link : m_links) + { + if (link && link->left() == other) + { + // Remove this specific link + link.reset(); + m_links.erase(std::remove_if(m_links.begin(), m_links.end(), + [](const std::shared_ptr& l) { return !l; }), m_links.end()); + return; + } + } + + if (!m_filter(other, this)) // Check Filter + return; + + auto link = std::make_shared(other, this, (*m_inf)); + m_links.emplace_back(link); + other->setLink(link); + (*m_inf)->addLink(link); } + else + { + // Original single-link behavior + if (m_link && m_link->left() == other) + { + m_link.reset(); + return; + } - if (!m_filter(other, this)) // Check Filter - return; + if (!m_filter(other, this)) // Check Filter + return; - m_link = std::make_shared(other, this, (*m_inf)); - other->setLink(m_link); - (*m_inf)->addLink(m_link); + m_link = std::make_shared(other, this, (*m_inf)); + other->setLink(m_link); + (*m_inf)->addLink(m_link); + } } // -----------------------------------------------------------------------------------------------------------------