From 76cbfe5680d483e3ec8159de9dfaee1eaeaab92b Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Mon, 4 Apr 2022 10:44:18 +0300 Subject: [PATCH 1/9] Replace link object pool with link vector --- imnodes.cpp | 472 +++++++++++++++++++-------------------------- imnodes_internal.h | 44 +++-- 2 files changed, 230 insertions(+), 286 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index c22bfac..a73c3d1 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -39,12 +39,6 @@ namespace { // [SECTION] bezier curve helpers -struct CubicBezier -{ - ImVec2 P0, P1, P2, P3; - int NumSegments; -}; - inline ImVec2 EvalCubicBezier( const float t, const ImVec2& P0, @@ -64,15 +58,39 @@ inline ImVec2 EvalCubicBezier( b0 * P0.y + b1 * P1.y + b2 * P2.y + b3 * P3.y); } +ImCubicBezier CalcCubicBezier( + ImVec2 start, + ImVec2 end, + const ImNodesAttributeType start_type, + const float line_segments_per_length) +{ + IM_ASSERT( + (start_type == ImNodesAttributeType_Input) || (start_type == ImNodesAttributeType_Output)); + if (start_type == ImNodesAttributeType_Input) + { + ImSwap(start, end); + } + + const float link_length = ImSqrt(ImLengthSqr(end - start)); + const ImVec2 offset = ImVec2(0.25f * link_length, 0.f); + ImCubicBezier cb; + cb.P0 = start; + cb.P1 = start + offset; + cb.P2 = end - offset; + cb.P3 = end; + cb.NumSegments = ImMax(static_cast(link_length * line_segments_per_length), 1); + return cb; +} + // Calculates the closest point along each bezier curve segment. -ImVec2 GetClosestPointOnCubicBezier(const int num_segments, const ImVec2& p, const CubicBezier& cb) +ImVec2 GetClosestPointOnCubicBezier(const ImVec2& p, const ImCubicBezier& cb) { - IM_ASSERT(num_segments > 0); + IM_ASSERT(cb.NumSegments > 0); ImVec2 p_last = cb.P0; ImVec2 p_closest; float p_closest_dist = FLT_MAX; - float t_step = 1.0f / (float)num_segments; - for (int i = 1; i <= num_segments; ++i) + float t_step = 1.0f / (float)cb.NumSegments; + for (int i = 1; i <= cb.NumSegments; ++i) { ImVec2 p_current = EvalCubicBezier(t_step * i, cb.P0, cb.P1, cb.P2, cb.P3); ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); @@ -87,18 +105,15 @@ ImVec2 GetClosestPointOnCubicBezier(const int num_segments, const ImVec2& p, con return p_closest; } -inline float GetDistanceToCubicBezier( - const ImVec2& pos, - const CubicBezier& cubic_bezier, - const int num_segments) +inline float GetDistanceToCubicBezier(const ImVec2& pos, const ImCubicBezier& cubic_bezier) { - const ImVec2 point_on_curve = GetClosestPointOnCubicBezier(num_segments, pos, cubic_bezier); + const ImVec2 point_on_curve = GetClosestPointOnCubicBezier(pos, cubic_bezier); const ImVec2 to_curve = point_on_curve - pos; return ImSqrt(ImLengthSqr(to_curve)); } -inline ImRect GetContainingRectForCubicBezier(const CubicBezier& cb) +inline ImRect GetContainingRectForCubicBezier(const ImCubicBezier& cb) { const ImVec2 min = ImVec2(ImMin(cb.P0.x, cb.P3.x), ImMin(cb.P0.y, cb.P3.y)); const ImVec2 max = ImVec2(ImMax(cb.P0.x, cb.P3.x), ImMax(cb.P0.y, cb.P3.y)); @@ -113,30 +128,6 @@ inline ImRect GetContainingRectForCubicBezier(const CubicBezier& cb) return rect; } -inline CubicBezier GetCubicBezier( - ImVec2 start, - ImVec2 end, - const ImNodesAttributeType start_type, - const float line_segments_per_length) -{ - IM_ASSERT( - (start_type == ImNodesAttributeType_Input) || (start_type == ImNodesAttributeType_Output)); - if (start_type == ImNodesAttributeType_Input) - { - ImSwap(start, end); - } - - const float link_length = ImSqrt(ImLengthSqr(end - start)); - const ImVec2 offset = ImVec2(0.25f * link_length, 0.f); - CubicBezier cubic_bezier; - cubic_bezier.P0 = start; - cubic_bezier.P1 = start + offset; - cubic_bezier.P2 = end - offset; - cubic_bezier.P3 = end; - cubic_bezier.NumSegments = ImMax(static_cast(link_length * line_segments_per_length), 1); - return cubic_bezier; -} - inline float EvalImplicitLineEq(const ImVec2& p1, const ImVec2& p2, const ImVec2& p) { return (p2.y - p1.y) * p.x + (p1.x - p2.x) * p.y + (p2.x * p1.y - p1.x * p2.y); @@ -193,19 +184,13 @@ inline bool RectangleOverlapsLineSegment(const ImRect& rect, const ImVec2& p1, c return abs(sum) != sum_abs; } -inline bool RectangleOverlapsBezier(const ImRect& rectangle, const CubicBezier& cubic_bezier) +inline bool RectangleOverlapsBezier(const ImRect& rectangle, const ImCubicBezier& cb) { - ImVec2 current = - EvalCubicBezier(0.f, cubic_bezier.P0, cubic_bezier.P1, cubic_bezier.P2, cubic_bezier.P3); - const float dt = 1.0f / cubic_bezier.NumSegments; - for (int s = 0; s < cubic_bezier.NumSegments; ++s) + ImVec2 current = EvalCubicBezier(0.f, cb.P0, cb.P1, cb.P2, cb.P3); + const float dt = 1.0f / cb.NumSegments; + for (int s = 0; s < cb.NumSegments; ++s) { - ImVec2 next = EvalCubicBezier( - static_cast((s + 1) * dt), - cubic_bezier.P0, - cubic_bezier.P1, - cubic_bezier.P2, - cubic_bezier.P3); + ImVec2 next = EvalCubicBezier(static_cast((s + 1) * dt), cb.P0, cb.P1, cb.P2, cb.P3); if (RectangleOverlapsLineSegment(rectangle, current, next)) { return true; @@ -215,15 +200,11 @@ inline bool RectangleOverlapsBezier(const ImRect& rectangle, const CubicBezier& return false; } -inline bool RectangleOverlapsLink( - const ImRect& rectangle, - const ImVec2& start, - const ImVec2& end, - const ImNodesAttributeType start_type) +inline bool RectangleOverlapsLink(const ImRect& rectangle, const ImCubicBezier& cb) { // First level: simple rejection test via rectangle overlap: - ImRect lrect = ImRect(start, end); + ImRect lrect = ImRect(cb.P0, cb.P3); if (lrect.Min.x > lrect.Max.x) { ImSwap(lrect.Min.x, lrect.Max.x); @@ -239,7 +220,7 @@ inline bool RectangleOverlapsLink( // First, check if either one or both endpoinds are trivially contained // in the rectangle - if (rectangle.Contains(start) || rectangle.Contains(end)) + if (rectangle.Contains(cb.P0) || rectangle.Contains(cb.P3)) { return true; } @@ -247,9 +228,7 @@ inline bool RectangleOverlapsLink( // Second level of refinement: do a more expensive test against the // link - const CubicBezier cubic_bezier = - GetCubicBezier(start, end, start_type, GImNodes->Style.LinkLineSegmentsPerLength); - return RectangleOverlapsBezier(rectangle, cubic_bezier); + return RectangleOverlapsBezier(rectangle, cb); } return false; @@ -643,9 +622,13 @@ void BeginLinkSelection(ImNodesEditorContext& editor, const int link_idx) editor.SelectedLinkIndices.push_back(link_idx); } -void BeginLinkDetach(ImNodesEditorContext& editor, const int link_idx, const int detach_pin_idx) +void BeginLinkDetach( + ImNodesEditorContext& editor, + const ImVector& links, + const int link_idx, + const int detach_pin_idx) { - const ImLinkData& link = editor.Links.Pool[link_idx]; + const ImLink& link = links[link_idx]; ImClickInteractionState& state = editor.ClickInteraction; state.Type = ImNodesClickInteractionType_LinkCreation; state.LinkCreation.EndPinIdx.Reset(); @@ -664,9 +647,10 @@ void BeginLinkCreation(ImNodesEditorContext& editor, const int hovered_pin_idx) } void BeginLinkInteraction( - ImNodesEditorContext& editor, - const int link_idx, - const ImOptionalIndex pin_idx = ImOptionalIndex()) + ImNodesEditorContext& editor, + const ImVector& links, + const int link_idx, + const ImOptionalIndex pin_idx = ImOptionalIndex()) { // Check if we are clicking the link with the modifier pressed. // This will in a link detach via clicking. @@ -677,16 +661,16 @@ void BeginLinkInteraction( if (modifier_pressed) { - const ImLinkData& link = editor.Links.Pool[link_idx]; - const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; - const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + const ImLinkData& link = GImNodes->Links[link_idx]; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPindId); const ImVec2& mouse_pos = GImNodes->MousePos; const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); const int closest_pin_idx = dist_to_start < dist_to_end ? link.StartPinIdx : link.EndPinIdx; editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; - BeginLinkDetach(editor, link_idx, closest_pin_idx); + BeginLinkDetach(editor, links, link_idx, closest_pin_idx); editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; } else @@ -743,7 +727,10 @@ void BeginCanvasInteraction(ImNodesEditorContext& editor) } } -void BoxSelectorUpdateSelection(ImNodesEditorContext& editor, ImRect box_rect) +void BoxSelectorUpdateSelection( + ImNodesEditorContext& editor, + const ImVector& curves, + ImRect box_rect) { // Invert box selector coordinates as needed @@ -781,27 +768,13 @@ void BoxSelectorUpdateSelection(ImNodesEditorContext& editor, ImRect box_rect) // Test for overlap against links - for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) + for (int idx = 0; idx < links.size(); ++idx) { - if (editor.Links.InUse[link_idx]) - { - const ImLinkData& link = editor.Links.Pool[link_idx]; - - const ImPinData& pin_start = editor.Pins.Pool[link.StartPinIdx]; - const ImPinData& pin_end = editor.Pins.Pool[link.EndPinIdx]; - const ImRect& node_start_rect = editor.Nodes.Pool[pin_start.ParentNodeIdx].Rect; - const ImRect& node_end_rect = editor.Nodes.Pool[pin_end.ParentNodeIdx].Rect; + const ImCubicBezier& cb = curves[idx]; - const ImVec2 start = GetScreenSpacePinCoordinates( - node_start_rect, pin_start.AttributeRect, pin_start.Type); - const ImVec2 end = - GetScreenSpacePinCoordinates(node_end_rect, pin_end.AttributeRect, pin_end.Type); - - // Test - if (RectangleOverlapsLink(box_rect, start, end, pin_start.Type)) - { - editor.SelectedLinkIndices.push_back(link_idx); - } + if (RectangleOverlapsLink(box_rect, cb)) + { + editor.SelectedlinkIndices.push_back(idx); } } } @@ -849,49 +822,32 @@ void TranslateSelectedNodes(ImNodesEditorContext& editor) } } -struct LinkPredicate +ImOptionalIndex FindDuplicateLink( + const ImNodesEditorContext& editor, + const ImVector& links, + const int start_pin_id, + const int end_pin_id) { - bool operator()(const ImLinkData& lhs, const ImLinkData& rhs) const + for (const ImLink& link : links) { - // Do a unique compare by sorting the pins' addresses. - // This catches duplicate links, whether they are in the - // same direction or not. - // Sorting by pin index should have the uniqueness guarantees as sorting - // by id -- each unique id will get one slot in the link pool array. - - int lhs_start = lhs.StartPinIdx; - int lhs_end = lhs.EndPinIdx; - int rhs_start = rhs.StartPinIdx; - int rhs_end = rhs.EndPinIdx; + int lhs_start_id = start_pin_id; + int lhs_end_id = end_pin_id; + int rhs_start_id = link.StartPinId; + int rhs_end_id = link.EndPinId; - if (lhs_start > lhs_end) + if (lhs_start_id > lhs_end_id) { - ImSwap(lhs_start, lhs_end); + ImSwap(lhs_start_id, lhs_end_id); } - if (rhs_start > rhs_end) + if (rhs_start_id > rhs_end_id) { - ImSwap(rhs_start, rhs_end); + ImSwap(rhs_start_id, rhs_end_id); } - return lhs_start == rhs_start && lhs_end == rhs_end; - } -}; - -ImOptionalIndex FindDuplicateLink( - const ImNodesEditorContext& editor, - const int start_pin_idx, - const int end_pin_idx) -{ - ImLinkData test_link(0); - test_link.StartPinIdx = start_pin_idx; - test_link.EndPinIdx = end_pin_idx; - for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) - { - const ImLinkData& link = editor.Links.Pool[link_idx]; - if (LinkPredicate()(test_link, link) && editor.Links.InUse[link_idx]) + if (lhs_start_id == rhs_start_id && lhs_end_id == rhs_end_id) { - return ImOptionalIndex(link_idx); + return ImOptionalIndex(idx); } } @@ -1041,20 +997,20 @@ void ClickInteractionUpdate(ImNodesEditorContext& editor) editor, editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]) : GImNodes->MousePos; - const CubicBezier cubic_bezier = GetCubicBezier( + const CubicBezier cb = CalcCubicBezier( start_pos, end_pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); #if IMGUI_VERSION_NUM < 18000 GImNodes->CanvasDrawList->AddBezierCurve( #else GImNodes->CanvasDrawList->AddBezierCubic( #endif - cubic_bezier.P0, - cubic_bezier.P1, - cubic_bezier.P2, - cubic_bezier.P3, + cb.P0, + cb.P1, + cb.P2, + cb.P3, GImNodes->Style.Colors[ImNodesCol_Link], GImNodes->Style.LinkThickness, - cubic_bezier.NumSegments); + cb.NumSegments); const bool link_creation_on_snap = GImNodes->HoveredPinIdx.HasValue() && @@ -1229,9 +1185,29 @@ ImOptionalIndex ResolveHoveredNode(const ImVector& depth_stack) return ImOptionalIndex(node_idx_on_top); } +int ResolveAttachedLink( + const ImVector& links, + const ImObjectPool& pins, + const int hovered_pin_idx) +{ + const int pin_id = pins.Pool[hovered_Pin_idx].Id; + for (int idx = 0; idx < links.size(); ++idx) + { + const ImLinkds& link = links[idx]; + if (pin_id == link.StartPinId || pin_id == link.EndPinId) + { + return idx; + } + } + + IM_ASSERT(!"unreachable code ResolveAttachedLink"); + return 0; +} + ImOptionalIndex ResolveHoveredLink( - const ImObjectPool& links, - const ImObjectPool& pins) + const ImVector& links, + const ImVector& curves, + const ImObjectPool& pins) { float smallest_distance = FLT_MAX; ImOptionalIndex link_idx_with_smallest_distance; @@ -1244,54 +1220,27 @@ ImOptionalIndex ResolveHoveredLink( // The latter is a requirement for link detaching with drag click to work, as both a link and // pin are required to be hovered over for the feature to work. - for (int idx = 0; idx < links.Pool.Size; ++idx) + const bool is_pin_hovered = GImNodes->HoveredPinIdx.HasValue(); + if (is_pin_hovered) { - if (!links.InUse[idx]) - { - continue; - } - - const ImLinkData& link = links.Pool[idx]; - const ImPinData& start_pin = pins.Pool[link.StartPinIdx]; - const ImPinData& end_pin = pins.Pool[link.EndPinIdx]; - - // If there is a hovered pin links can only be considered hovered if they use that pin - if (GImNodes->HoveredPinIdx.HasValue()) - { - if (GImNodes->HoveredPinIdx == link.StartPinIdx || - GImNodes->HoveredPinIdx == link.EndPinIdx) - { - return idx; - } - continue; - } - - // TODO: the calculated CubicBeziers could be cached since we generate them again when - // rendering the links + return ResolveAttachedLink(links, pins, GImNodes->HoveredPinIdx.Value()); + } - const CubicBezier cubic_bezier = GetCubicBezier( - start_pin.Pos, end_pin.Pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); + for (int idx = 0; idx < curves.size(); ++idx) + { + const ImBezierCurve& curve = curves[idx]; // The distance test + const ImRect curve_bounds = GetContainingRectForCubicBezier(curve); + + if (curve_bounds.Contains(GImNodes->MousePos)) { - const ImRect link_rect = GetContainingRectForCubicBezier(cubic_bezier); + const float distance = GetDistanceToCubicBezier(GImNodes->MousePos, curve); - // First, do a simple bounding box test against the box containing the link - // to see whether calculating the distance to the link is worth doing. - if (link_rect.Contains(GImNodes->MousePos)) + if (distance < GImNodes->Style.LinkHoverDistance && distance < smallest_distance) { - const float distance = GetDistanceToCubicBezier( - GImNodes->MousePos, cubic_bezier, cubic_bezier.NumSegments); - - // TODO: GImNodes->Style.LinkHoverDistance could be also copied into ImLinkData, - // since we're not calling this function in the same scope as ImNodes::Link(). The - // rendered/detected link might have a different hover distance than what the user - // had specified when calling Link() - if (distance < GImNodes->Style.LinkHoverDistance && distance < smallest_distance) - { - smallest_distance = distance; - link_idx_with_smallest_distance = idx; - } + smallest_distance = distance; + link_idx_with_smallest_distance = idx; } } } @@ -1579,56 +1528,55 @@ void DrawNode(ImNodesEditorContext& editor, const int node_idx) } } -void DrawLink(ImNodesEditorContext& editor, const int link_idx) +void CalcLinkGeometries( + const ObjectPool& pins, + const ImVector& links, + ImVector& curves) { - const ImLinkData& link = editor.Links.Pool[link_idx]; - const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; - const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + IM_ASSERT(!link_ids.empty()); - const CubicBezier cubic_bezier = GetCubicBezier( - start_pin.Pos, end_pin.Pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); + const float link_line_segments_per_length = GImNodes->Style.LinkLineSegmentsPerLength; - const bool link_hovered = - GImNodes->HoveredLinkIdx == link_idx && - editor.ClickInteraction.Type != ImNodesClickInteractionType_BoxSelection; - - if (link_hovered) + for (const ImLink& link : links) { - GImNodes->HoveredLinkIdx = link_idx; - } + const ImPinData& start_pin = FindOrCreateObject(pins, link.StartPinId); + const ImPinData& end_pin = FindOrCreateObject(pins, link.EndPinId); - // It's possible for a link to be deleted in begin_link_interaction. A user - // may detach a link, resulting in the link wire snapping to the mouse - // position. - // - // In other words, skip rendering the link if it was deleted. - if (GImNodes->DeletedLinkIdx == link_idx) - { - return; + curves.push_back(CalcCubicBezier( + start_pin.Pos, end_pin.Pos, start_pin.Type, link_line_segments_per_length)); } +} - ImU32 link_color = link.ColorStyle.Base; - if (editor.SelectedLinkIndices.contains(link_idx)) - { - link_color = link.ColorStyle.Selected; - } - else if (link_hovered) +void DrawLinks(const ImVector& links, const ImVector& curves) +{ + const int num_links = links.size(); + IM_ASSERT(num_links == curves.size()); + + const float link_thickness = GImNodes->Style.LinkThickness; + for (int idx = 0; idx < num_links; ++idx) { - link_color = link.ColorStyle.Hovered; - } + const ImLink& link = links[idx]; + const ImBezierCurve& curve = curves[idx]; + + const bool = link_hovered = GImNodes->HoveredLinkIdx == link_idx; + + ImU32 link_color = link.BaseColor; + if (link_hovered) + { + link_color = link.HoveredColor; + } + else if (editor.SelectedLinkIds.contains(link.Id)) + { + link_color = link.SelectedColor; + } #if IMGUI_VERSION_NUM < 18000 - GImNodes->CanvasDrawList->AddBezierCurve( + GImNodes->CanvasDrawList->AddBezierCurve( #else - GImNodes->CanvasDrawList->AddBezierCubic( + GImNodes->CanvasDrawList->AddBezierCubic( #endif - cubic_bezier.P0, - cubic_bezier.P1, - cubic_bezier.P2, - cubic_bezier.P3, - link_color, - GImNodes->Style.LinkThickness, - cubic_bezier.NumSegments); + curve.P0, curve.P1, curve.P2, curve.P3, link_color, link_thickness, curve.NumSegments); + } } void BeginPinAttribute( @@ -1735,7 +1683,7 @@ static inline void CalcMiniMapLayout() const ImVec2 grid_content_size = editor.GridContentBounds.IsInverted() ? max_size : ImFloor(editor.GridContentBounds.GetSize()); - const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; + const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; mini_map_size = ImFloor( grid_content_aspect_ratio > max_size_aspect_ratio ? ImVec2(max_size.x, max_size.x / grid_content_aspect_ratio) @@ -1821,45 +1769,39 @@ static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx) node_rect.Min, node_rect.Max, mini_map_node_outline, mini_map_node_rounding); } -static void MiniMapDrawLink(ImNodesEditorContext& editor, const int link_idx) +void MiniMapDrawLinks( + const ImNodesEditorContext& editor, + const ImVector& links, + const ImVector& curves) { - const ImLinkData& link = editor.Links.Pool[link_idx]; - const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; - const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + const int num_links = links.size(); + IM_ASSERT(num_links == curves.size()); - const CubicBezier cubic_bezier = GetCubicBezier( - ScreenSpaceToMiniMapSpace(editor, start_pin.Pos), - ScreenSpaceToMiniMapSpace(editor, end_pin.Pos), - start_pin.Type, - GImNodes->Style.LinkLineSegmentsPerLength / editor.MiniMapScaling); + const float link_thickness = GImNodes->Style.LinkThickness * editor.MiniMapScaling; - // It's possible for a link to be deleted in begin_link_interaction. A user - // may detach a link, resulting in the link wire snapping to the mouse - // position. - // - // In other words, skip rendering the link if it was deleted. - if (GImNodes->DeletedLinkIdx == link_idx) + for (int idx = 0; idx < num_links; ++idx) { - return; - } + const ImLink& link = links[idx]; + const ImBezierCurve& curve = curves[idx]; - const ImU32 link_color = - GImNodes->Style.Colors - [editor.SelectedLinkIndices.contains(link_idx) ? ImNodesCol_MiniMapLinkSelected - : ImNodesCol_MiniMapLink]; + const int color_idx = editor.SelectedLinkIds.contains(link.Id) + ? ImNodesCol_MiniMapLinkSelected + : ImNodesCol_MiniMapLink; + const ImU32 link_color = GImNodes->Style.Colors[color_idx]; #if IMGUI_VERSION_NUM < 18000 - GImNodes->CanvasDrawList->AddBezierCurve( + GImNodes->CanvasDrawList->AddBezierCurve( #else - GImNodes->CanvasDrawList->AddBezierCubic( + GImNodes->CanvasDrawList->AddBezierCubic( #endif - cubic_bezier.P0, - cubic_bezier.P1, - cubic_bezier.P2, - cubic_bezier.P3, - link_color, - GImNodes->Style.LinkThickness * editor.MiniMapScaling, - cubic_bezier.NumSegments); + ScreenSpaceToMiniMapSpace(editor, curve.P0), + ScreenSpaceToMiniMapSpace(editor, curve.P1), + ScreenSpaceToMiniMapSpace(editor, curve.P2), + ScreenSpaceToMiniMapSpace(editor, curve.P3), + link_color, + link_thickness, + curve.NumSegments); + } } static void MiniMapUpdate() @@ -1896,13 +1838,7 @@ static void MiniMapUpdate() mini_map_rect.Min, mini_map_rect.Max, true /* intersect with editor clip-rect */); // Draw links first so they appear under nodes, and we can use the same draw channel - for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) - { - if (editor.Links.InUse[link_idx]) - { - MiniMapDrawLink(editor, link_idx); - } - } + MiniMapDrawLinks(editor, GImNodes->Links, GImNodes->Curves); for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) { @@ -2200,7 +2136,9 @@ void BeginNodeEditor() editor.MiniMapEnabled = false; ObjectPoolReset(editor.Nodes); ObjectPoolReset(editor.Pins); - ObjectPoolReset(editor.Links); + + GImNodes->Links.resize(0); + GImNodes->Curves.resize(0); GImNodes->HoveredNodeIdx.Reset(); GImNodes->HoveredLinkIdx.Reset(); @@ -2270,6 +2208,8 @@ void EndNodeEditor() ImNodesEditorContext& editor = EditorContextGet(); + CalcLinkGeometries(editor.Pins, GImNodes->Links, GImNodes->Curves); + bool no_grid_content = editor.GridContentBounds.IsInverted(); if (no_grid_content) { @@ -2310,7 +2250,8 @@ void EndNodeEditor() // dragging, we need to have both a link and pin hovered. if (!GImNodes->HoveredNodeIdx.HasValue()) { - GImNodes->HoveredLinkIdx = ResolveHoveredLink(editor.Links, editor.Pins); + GImNodes->HoveredLinkIdx = + ResolveHoveredLink(GImNodes->Links, GImNodes->Curves, editor.Pins); } } @@ -2327,13 +2268,7 @@ void EndNodeEditor() // channel. GImNodes->CanvasDrawList->ChannelsSetCurrent(0); - for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) - { - if (editor.Links.InUse[link_idx]) - { - DrawLink(editor, link_idx); - } - } + DrawLinks(editor, GImNodes->Links, GImNodes->Curves); // Render the click interaction UI elements (partial links, box selector) on top of everything // else. @@ -2399,9 +2334,6 @@ void EndNodeEditor() DrawListSortChannelsByDepth(editor.NodeDepthOrder); - // After the links have been rendered, the link pool can be updated as well. - ObjectPoolUpdate(editor.Links); - // Finally, merge the draw channels GImNodes->CanvasDrawList->ChannelsMerge(); @@ -2591,14 +2523,11 @@ void Link(const int id, const int start_attr_id, const int end_attr_id) { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); - ImNodesEditorContext& editor = EditorContextGet(); - ImLinkData& link = ObjectPoolFindOrCreateObject(editor.Links, id); - link.Id = id; - link.StartPinIdx = ObjectPoolFindOrCreateIndex(editor.Pins, start_attr_id); - link.EndPinIdx = ObjectPoolFindOrCreateIndex(editor.Pins, end_attr_id); - link.ColorStyle.Base = GImNodes->Style.Colors[ImNodesCol_Link]; - link.ColorStyle.Hovered = GImNodes->Style.Colors[ImNodesCol_LinkHovered]; - link.ColorStyle.Selected = GImNodes->Style.Colors[ImNodesCol_LinkSelected]; + const int link_idx = GImNodes->Links.Ids.size(); + GImNodes->Links.push_back(ImLink(id, start_attr_id, end_attr_id, GImNodes->Style.Colors)); + + const ImNodesEditorContext& editor = EditorContextGet(); + const ImPinData& end_pin = FindOrCreateObject(editor.Pins, link.EndPinId); // Check if this link was created by the current link event if ((editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation && @@ -2808,8 +2737,7 @@ bool IsLinkHovered(int* const link_id) const bool is_hovered = GImNodes->HoveredLinkIdx.HasValue(); if (is_hovered) { - const ImNodesEditorContext& editor = EditorContextGet(); - *link_id = editor.Links.Pool[GImNodes->HoveredLinkIdx.Value()].Id; + *link_id = GImNodes->Links[GImNodes->HoveredLinkIdx.Value].Id; } return is_hovered; } @@ -2862,7 +2790,7 @@ void GetSelectedLinks(int* link_ids) for (int i = 0; i < editor.SelectedLinkIndices.size(); ++i) { const int link_idx = editor.SelectedLinkIndices[i]; - link_ids[i] = editor.Links.Pool[link_idx].Id; + link_ids[i] = GImNodes->Links[link_idx].Id; } } @@ -3080,7 +3008,7 @@ bool IsLinkDestroyed(int* const link_id) { const ImNodesEditorContext& editor = EditorContextGet(); const int link_idx = GImNodes->DeletedLinkIdx.Value(); - *link_id = editor.Links.Pool[link_idx].Id; + *link_id = GImNodes->Links[link_ids].Id; } return link_destroyed; diff --git a/imnodes_internal.h b/imnodes_internal.h index 593ab49..873e908 100644 --- a/imnodes_internal.h +++ b/imnodes_internal.h @@ -184,17 +184,28 @@ struct ImPinData } }; -struct ImLinkData +struct ImLink { - int Id; - int StartPinIdx, EndPinIdx; - - struct + int Id; + int StartPinId, EndPinId; + ImU32 BaseColor, HoveredColor, SelectedColor; + + ImLink( + const int id, + const int start_pin_id, + const int end_pin_id, + const unsigned int (&colors)[ImNodesCol_COUNT]) + : Id(id), StartPinId(start_pin_id), EndPinId(end_pin_id), + BaseColor(colors[ImNodesCol_Link]), HoveredColor(colors[ImNodesCol_LinkHovered]), + SelectedColor(colors[ImNodesCol_LinkSelected]) { - ImU32 Base, Hovered, Selected; - } ColorStyle; + } +}; - ImLinkData(const int link_id) : Id(link_id), StartPinIdx(), EndPinIdx(), ColorStyle() {} +struct ImCubicBezier +{ + ImVec2 P0, P1, P2, P3; + int NumSegments; }; struct ImClickInteractionState @@ -247,7 +258,6 @@ struct ImNodesEditorContext { ImObjectPool Nodes; ImObjectPool Pins; - ImObjectPool Links; ImVector NodeDepthOrder; @@ -264,7 +274,7 @@ struct ImNodesEditorContext // Relative origins of selected nodes for snapping of dragged nodes ImVector SelectedNodeOffsets; // Offset of the primary node origin relative to the mouse cursor. - ImVec2 PrimaryNodeOffset; + ImVec2 PrimaryNodeOffset; ImClickInteractionState ClickInteraction; @@ -283,11 +293,10 @@ struct ImNodesEditorContext float MiniMapScaling; ImNodesEditorContext() - : Nodes(), Pins(), Links(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIndices(), + : Nodes(), Pins(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIndices(), SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), ClickInteraction(), - MiniMapEnabled(false), MiniMapSizeFraction(0.0f), - MiniMapNodeHoveringCallback(NULL), MiniMapNodeHoveringCallbackUserData(NULL), - MiniMapScaling(0.0f) + MiniMapEnabled(false), MiniMapSizeFraction(0.0f), MiniMapNodeHoveringCallback(NULL), + MiniMapNodeHoveringCallbackUserData(NULL), MiniMapScaling(0.0f) { } }; @@ -308,6 +317,13 @@ struct ImNodesContext ImVec2 CanvasOriginScreenSpace; ImRect CanvasRectScreenSpace; + // Frame state + + // Links + + ImVector Links; + ImVector Curves; + // Debug helpers ImNodesScope CurrentScope; From 40c8edd7e94e350daf76cf66fc0bc5d1e8910bed Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Sat, 9 Apr 2022 12:56:06 +0300 Subject: [PATCH 2/9] Store link ids in selection instead of indices --- imnodes.cpp | 54 +++++++++++++++++++++++++--------------------- imnodes_internal.h | 4 ++-- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index a73c3d1..509df23 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -573,7 +573,7 @@ void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) // moved at once. if (!editor.SelectedNodeIndices.contains(node_idx)) { - editor.SelectedLinkIndices.clear(); + editor.SelectedLinkIds.clear(); if (!GImNodes->MultipleSelectModifier) { editor.SelectedNodeIndices.clear(); @@ -612,14 +612,14 @@ void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) } } -void BeginLinkSelection(ImNodesEditorContext& editor, const int link_idx) +void BeginLinkSelection(ImNodesEditorContext& editor, const int link_id) { editor.ClickInteraction.Type = ImNodesClickInteractionType_Link; // When a link is selected, clear all other selections, and insert the link // as the sole selection. editor.SelectedNodeIndices.clear(); - editor.SelectedLinkIndices.clear(); - editor.SelectedLinkIndices.push_back(link_idx); + editor.SelectedLinkIds.clear(); + editor.SelectedLinkIds.push_back(link_id); } void BeginLinkDetach( @@ -661,12 +661,12 @@ void BeginLinkInteraction( if (modifier_pressed) { - const ImLinkData& link = GImNodes->Links[link_idx]; - const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); - const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPindId); - const ImVec2& mouse_pos = GImNodes->MousePos; - const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); - const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); + const ImLink& link = GImNodes->Links[link_idx]; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPindId); + const ImVec2& mouse_pos = GImNodes->MousePos; + const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); + const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); const int closest_pin_idx = dist_to_start < dist_to_end ? link.StartPinIdx : link.EndPinIdx; editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; @@ -692,7 +692,7 @@ void BeginLinkInteraction( } else { - BeginLinkSelection(editor, link_idx); + BeginLinkSelection(editor, link.Id); } } } @@ -729,6 +729,7 @@ void BeginCanvasInteraction(ImNodesEditorContext& editor) void BoxSelectorUpdateSelection( ImNodesEditorContext& editor, + const ImVector& links, const ImVector& curves, ImRect box_rect) { @@ -764,17 +765,18 @@ void BoxSelectorUpdateSelection( // Update link selection - editor.SelectedLinkIndices.clear(); + editor.SelectedLinkIds.clear(); // Test for overlap against links for (int idx = 0; idx < links.size(); ++idx) { + const ImLink& link = links[idx]; const ImCubicBezier& cb = curves[idx]; if (RectangleOverlapsLink(box_rect, cb)) { - editor.SelectedlinkIndices.push_back(idx); + editor.SelectedLinkIds.push_back(link.Id); } } } @@ -885,7 +887,10 @@ bool ShouldLinkSnapToPin( return true; } -void ClickInteractionUpdate(ImNodesEditorContext& editor) +void ClickInteractionUpdate( + ImNodesEditorContext& editor, + const ImVector& links, + const ImVector& curves) { switch (editor.ClickInteraction.Type) { @@ -898,7 +903,7 @@ void ClickInteractionUpdate(ImNodesEditorContext& editor) box_rect.Min = GridSpaceToScreenSpace(editor, box_rect.Min); box_rect.Max = GridSpaceToScreenSpace(editor, box_rect.Max); - BoxSelectorUpdateSelection(editor, box_rect); + BoxSelectorUpdateSelection(editor, links, curves, box_rect); const ImU32 box_selector_color = GImNodes->Style.Colors[ImNodesCol_BoxSelector]; const ImU32 box_selector_outline = GImNodes->Style.Colors[ImNodesCol_BoxSelectorOutline]; @@ -2210,6 +2215,8 @@ void EndNodeEditor() CalcLinkGeometries(editor.Pins, GImNodes->Links, GImNodes->Curves); + IM_ASSERT(GImNodes->Links.size() == GImNodes.Curves.size()); + bool no_grid_content = editor.GridContentBounds.IsInverted(); if (no_grid_content) { @@ -2324,7 +2331,7 @@ void EndNodeEditor() editor.Panning += editor.AutoPanningDelta; } } - ClickInteractionUpdate(editor); + ClickInteractionUpdate(editor, GImNodes->Links, GImNodes->Curves); // At this point, draw commands have been issued for all nodes (and pins). Update the node pool // to detect unused node slots and remove those indices from the depth stack before sorting the @@ -2767,7 +2774,7 @@ int NumSelectedLinks() { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); const ImNodesEditorContext& editor = EditorContextGet(); - return editor.SelectedLinkIndices.size(); + return editor.SelectedLinkIds.size(); } void GetSelectedNodes(int* node_ids) @@ -2787,10 +2794,9 @@ void GetSelectedLinks(int* link_ids) IM_ASSERT(link_ids != NULL); const ImNodesEditorContext& editor = EditorContextGet(); - for (int i = 0; i < editor.SelectedLinkIndices.size(); ++i) + for (int i = 0; i < editor.SelectedLinkIds.size(); ++i) { - const int link_idx = editor.SelectedLinkIndices[i]; - link_ids[i] = GImNodes->Links[link_idx].Id; + link_idx[i] = editor.SelectedLinkIds[i]; } } @@ -2809,13 +2815,13 @@ void ClearNodeSelection(int node_id) void ClearLinkSelection() { ImNodesEditorContext& editor = EditorContextGet(); - editor.SelectedLinkIndices.clear(); + editor.SelectedLinkIds.clear(); } void ClearLinkSelection(int link_id) { ImNodesEditorContext& editor = EditorContextGet(); - ClearObjectSelection(editor.Links, editor.SelectedLinkIndices, link_id); + editor.SelectedLinkIds.find_erase_unsorted(link_id); } void SelectNode(int node_id) @@ -2827,7 +2833,7 @@ void SelectNode(int node_id) void SelectLink(int link_id) { ImNodesEditorContext& editor = EditorContextGet(); - SelectObject(editor.Links, editor.SelectedLinkIndices, link_id); + editor.SelectedLinkIds.push_back(link_id); } bool IsNodeSelected(int node_id) @@ -2839,7 +2845,7 @@ bool IsNodeSelected(int node_id) bool IsLinkSelected(int link_id) { ImNodesEditorContext& editor = EditorContextGet(); - return IsObjectSelected(editor.Links, editor.SelectedLinkIndices, link_id); + return editor.SelectedLinkIds.contains(link_id); } bool IsAttributeActive() diff --git a/imnodes_internal.h b/imnodes_internal.h index 873e908..a34402e 100644 --- a/imnodes_internal.h +++ b/imnodes_internal.h @@ -269,7 +269,7 @@ struct ImNodesEditorContext ImRect GridContentBounds; ImVector SelectedNodeIndices; - ImVector SelectedLinkIndices; + ImVector SelectedLinkIds; // Relative origins of selected nodes for snapping of dragged nodes ImVector SelectedNodeOffsets; @@ -293,7 +293,7 @@ struct ImNodesEditorContext float MiniMapScaling; ImNodesEditorContext() - : Nodes(), Pins(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIndices(), + : Nodes(), Pins(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIds(), SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), ClickInteraction(), MiniMapEnabled(false), MiniMapSizeFraction(0.0f), MiniMapNodeHoveringCallback(NULL), MiniMapNodeHoveringCallbackUserData(NULL), MiniMapScaling(0.0f) From 6b7c7448c2bd7c9e0ccc8e308ca8906f8606ad51 Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Sat, 9 Apr 2022 13:12:12 +0300 Subject: [PATCH 3/9] Miscellaneous compile fixes --- imnodes.cpp | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index 509df23..d1762c5 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -682,7 +682,7 @@ void BeginLinkInteraction( // Check the 'click and drag to detach' case. if (hovered_pin_flags & ImNodesAttributeFlags_EnableLinkDetachWithDragClick) { - BeginLinkDetach(editor, link_idx, pin_idx.Value()); + BeginLinkDetach(editor, links, link_idx, pin_idx.Value()); editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; } else @@ -692,7 +692,7 @@ void BeginLinkInteraction( } else { - BeginLinkSelection(editor, link.Id); + BeginLinkSelection(editor, links[link_idx].Id); } } } @@ -830,8 +830,10 @@ ImOptionalIndex FindDuplicateLink( const int start_pin_id, const int end_pin_id) { - for (const ImLink& link : links) + for (int idx = 0; idx < links.size(); ++idx) { + const ImLink& link = links[idx]; + int lhs_start_id = start_pin_id; int lhs_end_id = end_pin_id; int rhs_start_id = link.StartPinId; @@ -990,6 +992,7 @@ void ClickInteractionUpdate( { BeginLinkDetach( editor, + links, GImNodes->SnapLinkIdx.Value(), editor.ClickInteraction.LinkCreation.EndPinIdx.Value()); } @@ -1002,7 +1005,7 @@ void ClickInteractionUpdate( editor, editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]) : GImNodes->MousePos; - const CubicBezier cb = CalcCubicBezier( + const ImCubicBezier cb = CalcCubicBezier( start_pos, end_pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); #if IMGUI_VERSION_NUM < 18000 GImNodes->CanvasDrawList->AddBezierCurve( @@ -1195,7 +1198,7 @@ int ResolveAttachedLink( const ImObjectPool& pins, const int hovered_pin_idx) { - const int pin_id = pins.Pool[hovered_Pin_idx].Id; + const int pin_id = pins.Pool[hovered_pin_idx].Id; for (int idx = 0; idx < links.size(); ++idx) { const ImLinkds& link = links[idx]; @@ -1233,7 +1236,7 @@ ImOptionalIndex ResolveHoveredLink( for (int idx = 0; idx < curves.size(); ++idx) { - const ImBezierCurve& curve = curves[idx]; + const ImCubicBezier& curve = curves[idx]; // The distance test const ImRect curve_bounds = GetContainingRectForCubicBezier(curve); @@ -1534,9 +1537,9 @@ void DrawNode(ImNodesEditorContext& editor, const int node_idx) } void CalcLinkGeometries( - const ObjectPool& pins, - const ImVector& links, - ImVector& curves) + const ImObjectPool& pins, + const ImVector& links, + ImVector& curves) { IM_ASSERT(!link_ids.empty()); @@ -1544,15 +1547,18 @@ void CalcLinkGeometries( for (const ImLink& link : links) { - const ImPinData& start_pin = FindOrCreateObject(pins, link.StartPinId); - const ImPinData& end_pin = FindOrCreateObject(pins, link.EndPinId); + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(pins, link.StartPinId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(pins, link.EndPinId); curves.push_back(CalcCubicBezier( start_pin.Pos, end_pin.Pos, start_pin.Type, link_line_segments_per_length)); } } -void DrawLinks(const ImVector& links, const ImVector& curves) +void DrawLinks( + const ImNodesEditorContext& editor, + const ImVector& links, + const ImVector& curves) { const int num_links = links.size(); IM_ASSERT(num_links == curves.size()); @@ -1561,9 +1567,9 @@ void DrawLinks(const ImVector& links, const ImVector& cur for (int idx = 0; idx < num_links; ++idx) { const ImLink& link = links[idx]; - const ImBezierCurve& curve = curves[idx]; + const ImCubicBezier& curve = curves[idx]; - const bool = link_hovered = GImNodes->HoveredLinkIdx == link_idx; + const bool link_hovered = GImNodes->HoveredLinkIdx == link_idx; ImU32 link_color = link.BaseColor; if (link_hovered) @@ -1787,7 +1793,7 @@ void MiniMapDrawLinks( for (int idx = 0; idx < num_links; ++idx) { const ImLink& link = links[idx]; - const ImBezierCurve& curve = curves[idx]; + const ImCubicBezier& curve = curves[idx]; const int color_idx = editor.SelectedLinkIds.contains(link.Id) ? ImNodesCol_MiniMapLinkSelected @@ -2215,7 +2221,7 @@ void EndNodeEditor() CalcLinkGeometries(editor.Pins, GImNodes->Links, GImNodes->Curves); - IM_ASSERT(GImNodes->Links.size() == GImNodes.Curves.size()); + IM_ASSERT(GImNodes->Links.size() == GImNodes->Curves.size()); bool no_grid_content = editor.GridContentBounds.IsInverted(); if (no_grid_content) From bafa37af26049fd6db931d6417bc6d25ca5d3c4a Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Sun, 10 Apr 2022 13:03:19 +0300 Subject: [PATCH 4/9] Refactor link interaction - Replaces the link creation states with ones which only use pin ids. The fact that snapping and unsnapping from pins used just one state made it very difficult to get the interaction management play nice with just pin ids. - The solution was to reimplement the way interaction states are managed. --- imnodes.cpp | 551 ++++++++++++++++++++++++++------------------- imnodes_internal.h | 65 ++++-- 2 files changed, 361 insertions(+), 255 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index d1762c5..3ac2237 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -18,6 +18,7 @@ #error "Minimum ImGui version requirement not met -- please use a newer version!" #endif +#include #include #include #include @@ -552,17 +553,17 @@ bool MouseInCanvas() GImNodes->CanvasRectScreenSpace.Contains(ImGui::GetMousePos()); } -void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) +inline void PushNodeState(ImVector& interaction_stack) { - // Don't start selecting a node if we are e.g. already creating and dragging - // a new link! New link creation can happen when the mouse is clicked over - // a node, but within the hover radius of a pin. - if (editor.ClickInteraction.Type != ImNodesClickInteractionType_None) - { - return; - } + IM_ASSERT(interaction_stack.size() == 0); + + ImInteractionState state(ImNodesInteractionType_Node); - editor.ClickInteraction.Type = ImNodesClickInteractionType_Node; + interaction_stack.push_back(state); +} + +void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) +{ // If the node is not already contained in the selection, then we want only // the interaction node to be selected, effective immediately. // @@ -573,6 +574,8 @@ void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) // moved at once. if (!editor.SelectedNodeIndices.contains(node_idx)) { + PushNodeState(editor.InteractionStack); + editor.SelectedLinkIds.clear(); if (!GImNodes->MultipleSelectModifier) { @@ -592,9 +595,6 @@ void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) { const int* const node_ptr = editor.SelectedNodeIndices.find(node_idx); editor.SelectedNodeIndices.erase(node_ptr); - - // Don't allow dragging after deselecting - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; } // To support snapping of multiple nodes, we need to store the offset of @@ -612,9 +612,18 @@ void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) } } +inline void PushLinkState(ImVector& interaction_stack) +{ + IM_ASSERT(interaction_stack.size() == 0); + + ImInteractionState state(ImNodesInteractionType_Link); + + interaction_stack.push_back(state); +} + void BeginLinkSelection(ImNodesEditorContext& editor, const int link_id) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_Link; + PushLinkState(editor.InteractionStack); // When a link is selected, clear all other selections, and insert the link // as the sole selection. editor.SelectedNodeIndices.clear(); @@ -622,27 +631,23 @@ void BeginLinkSelection(ImNodesEditorContext& editor, const int link_id) editor.SelectedLinkIds.push_back(link_id); } -void BeginLinkDetach( - ImNodesEditorContext& editor, - const ImVector& links, - const int link_idx, - const int detach_pin_idx) +inline void PushPartialLinkState( + ImVector& interaction_stack, + const int start_pin_id, + const bool from_snap) { - const ImLink& link = links[link_idx]; - ImClickInteractionState& state = editor.ClickInteraction; - state.Type = ImNodesClickInteractionType_LinkCreation; - state.LinkCreation.EndPinIdx.Reset(); - state.LinkCreation.StartPinIdx = - detach_pin_idx == link.StartPinIdx ? link.EndPinIdx : link.StartPinIdx; - GImNodes->DeletedLinkIdx = link_idx; + IM_ASSERT(interaction_stack.size() == 0); + + ImInteractionState state(ImNodesInteractionType_PartialLink); + state.PartialLink.StartPinId = start_pin_id; + state.PartialLink.CreatedFromSnap = from_snap; + + interaction_stack.push_back(state); } void BeginLinkCreation(ImNodesEditorContext& editor, const int hovered_pin_idx) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; - editor.ClickInteraction.LinkCreation.StartPinIdx = hovered_pin_idx; - editor.ClickInteraction.LinkCreation.EndPinIdx.Reset(); - editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_Standard; + PushPartialLinkState(editor.InteractionStack, editor.Pins.Pool[hovered_pin_idx].Id, false); GImNodes->ImNodesUIState |= ImNodesUIState_LinkStarted; } @@ -659,31 +664,34 @@ void BeginLinkInteraction( ? false : *GImNodes->Io.LinkDetachWithModifierClick.Modifier; + const ImLink& link = GImNodes->Links[link_idx]; + if (modifier_pressed) { - const ImLink& link = GImNodes->Links[link_idx]; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); - const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPindId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPinId); const ImVec2& mouse_pos = GImNodes->MousePos; const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); - const int closest_pin_idx = dist_to_start < dist_to_end ? link.StartPinIdx : link.EndPinIdx; + const int closest_pin_id = dist_to_start < dist_to_end ? link.StartPinId : link.EndPinId; - editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; - BeginLinkDetach(editor, links, link_idx, closest_pin_idx); - editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; + PushPartialLinkState(editor.InteractionStack, closest_pin_id, true); + GImNodes->DeletedLinkIdx = link_idx; } else { if (pin_idx.HasValue()) { - const int hovered_pin_flags = editor.Pins.Pool[pin_idx.Value()].Flags; + const ImPinData& hovered_pin = editor.Pins.Pool[pin_idx.Value()]; // Check the 'click and drag to detach' case. - if (hovered_pin_flags & ImNodesAttributeFlags_EnableLinkDetachWithDragClick) + if (hovered_pin.Flags & ImNodesAttributeFlags_EnableLinkDetachWithDragClick) { - BeginLinkDetach(editor, links, link_idx, pin_idx.Value()); - editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; + const int start_id = + hovered_pin.Id == link.StartPinId ? link.EndPinId : link.StartPinId; + PushPartialLinkState(editor.InteractionStack, start_id, true); + GImNodes->DeletedLinkIdx = link_idx; } else { @@ -697,7 +705,49 @@ void BeginLinkInteraction( } } -static inline bool IsMiniMapHovered(); +inline bool IsMiniMapHovered(); + +inline void PushBoxSelectorState( + ImVector& interaction_stack, + const ImVec2& mouse_pos) +{ + IM_ASSERT(interaction_stack.size() == 0); + + ImInteractionState state(ImNodesInteractionType_BoxSelector); + state.BoxSelector.GridSpaceRect.Min = mouse_pos; + + interaction_stack.push_back(state); +} + +inline void PushPanningState(ImVector& interaction_stack) +{ + IM_ASSERT(interaction_stack.size() == 0); + + ImInteractionState state(ImNodesInteractionType_Panning); + + interaction_stack.push_back(state); +} + +inline void PushSnappedLinkState( + ImVector& interaction_stack, + const int start_pin_id, + const int snapped_pin_id) +{ + IM_ASSERT(interaction_stack.size() > 0); + + ImInteractionState state(ImNodesInteractionType_SnappedLink); + state.SnappedLink.StartPinId = start_pin_id; + state.SnappedLink.SnappedPinId = snapped_pin_id; + + interaction_stack.push_back(state); +} + +inline void PushImGuiState(ImVector& interaction_stack) +{ + ImInteractionState state(ImNodesInteractionType_ImGuiItem); + + interaction_stack.push_back(state); +} void BeginCanvasInteraction(ImNodesEditorContext& editor) { @@ -707,8 +757,7 @@ void BeginCanvasInteraction(ImNodesEditorContext& editor) const bool mouse_not_in_canvas = !MouseInCanvas(); - if (editor.ClickInteraction.Type != ImNodesClickInteractionType_None || - any_ui_element_hovered || mouse_not_in_canvas) + if (any_ui_element_hovered || mouse_not_in_canvas) { return; } @@ -717,13 +766,11 @@ void BeginCanvasInteraction(ImNodesEditorContext& editor) if (started_panning) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_Panning; + PushPanningState(editor.InteractionStack); } else if (GImNodes->LeftMouseClicked) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_BoxSelection; - editor.ClickInteraction.BoxSelector.Rect.Min = - ScreenSpaceToGridSpace(editor, GImNodes->MousePos); + PushBoxSelectorState(editor.InteractionStack, GImNodes->MousePos); } } @@ -825,10 +872,9 @@ void TranslateSelectedNodes(ImNodesEditorContext& editor) } ImOptionalIndex FindDuplicateLink( - const ImNodesEditorContext& editor, - const ImVector& links, - const int start_pin_id, - const int end_pin_id) + const ImVector& links, + const int start_pin_id, + const int end_pin_id) { for (int idx = 0; idx < links.size(); ++idx) { @@ -889,19 +935,20 @@ bool ShouldLinkSnapToPin( return true; } -void ClickInteractionUpdate( +void InteractionStackUpdate( ImNodesEditorContext& editor, const ImVector& links, const ImVector& curves) { - switch (editor.ClickInteraction.Type) + ImInteractionState& state = editor.InteractionStack.back(); + + switch (state.Type) { - case ImNodesClickInteractionType_BoxSelection: + case ImNodesInteractionType_BoxSelector: { - editor.ClickInteraction.BoxSelector.Rect.Max = - ScreenSpaceToGridSpace(editor, GImNodes->MousePos); + state.BoxSelector.GridSpaceRect.Max = ScreenSpaceToGridSpace(editor, GImNodes->MousePos); - ImRect box_rect = editor.ClickInteraction.BoxSelector.Rect; + ImRect box_rect = state.BoxSelector.GridSpaceRect; box_rect.Min = GridSpaceToScreenSpace(editor, box_rect.Min); box_rect.Max = GridSpaceToScreenSpace(editor, box_rect.Max); @@ -941,123 +988,94 @@ void ClickInteractionUpdate( } } - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + editor.InteractionStack.pop_back(); } } break; - case ImNodesClickInteractionType_Node: + case ImNodesInteractionType_Node: { TranslateSelectedNodes(editor); if (GImNodes->LeftMouseReleased) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + editor.InteractionStack.pop_back(); } } break; - case ImNodesClickInteractionType_Link: + case ImNodesInteractionType_Link: { if (GImNodes->LeftMouseReleased) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + editor.InteractionStack.pop_back(); } } break; - case ImNodesClickInteractionType_LinkCreation: - { - const ImPinData& start_pin = - editor.Pins.Pool[editor.ClickInteraction.LinkCreation.StartPinIdx]; - - const ImOptionalIndex maybe_duplicate_link_idx = - GImNodes->HoveredPinIdx.HasValue() - ? FindDuplicateLink( - editor, - editor.ClickInteraction.LinkCreation.StartPinIdx, - GImNodes->HoveredPinIdx.Value()) - : ImOptionalIndex(); - - const bool should_snap = - GImNodes->HoveredPinIdx.HasValue() && - ShouldLinkSnapToPin( - editor, start_pin, GImNodes->HoveredPinIdx.Value(), maybe_duplicate_link_idx); + case ImNodesInteractionType_PartialLink: + { + const int start_pin_id = state.PartialLink.StartPinId; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, start_pin_id); - // If we created on snap and the hovered pin is empty or changed, then we need signal that - // the link's state has changed. - const bool snapping_pin_changed = - editor.ClickInteraction.LinkCreation.EndPinIdx.HasValue() && - !(GImNodes->HoveredPinIdx == editor.ClickInteraction.LinkCreation.EndPinIdx); + bool link_was_created = false; - // Detach the link that was created by this link event if it's no longer in snap range - if (snapping_pin_changed && GImNodes->SnapLinkIdx.HasValue()) + if (GImNodes->HoveredPinIdx.HasValue()) { - BeginLinkDetach( - editor, - links, - GImNodes->SnapLinkIdx.Value(), - editor.ClickInteraction.LinkCreation.EndPinIdx.Value()); - } + const ImPinData& hovered_pin = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]; - const ImVec2 start_pos = GetScreenSpacePinCoordinates(editor, start_pin); - // If we are within the hover radius of a receiving pin, snap the link - // endpoint to it - const ImVec2 end_pos = should_snap - ? GetScreenSpacePinCoordinates( - editor, editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]) - : GImNodes->MousePos; + const ImOptionalIndex maybe_duplicate_link_idx = + FindDuplicateLink(links, start_pin_id, hovered_pin.Id); - const ImCubicBezier cb = CalcCubicBezier( - start_pos, end_pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); -#if IMGUI_VERSION_NUM < 18000 - GImNodes->CanvasDrawList->AddBezierCurve( -#else - GImNodes->CanvasDrawList->AddBezierCubic( -#endif - cb.P0, - cb.P1, - cb.P2, - cb.P3, - GImNodes->Style.Colors[ImNodesCol_Link], - GImNodes->Style.LinkThickness, - cb.NumSegments); + const bool should_snap = ShouldLinkSnapToPin( + editor, start_pin, GImNodes->HoveredPinIdx.Value(), maybe_duplicate_link_idx); - const bool link_creation_on_snap = - GImNodes->HoveredPinIdx.HasValue() && - (editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()].Flags & - ImNodesAttributeFlags_EnableLinkCreationOnSnap); + if (should_snap) + { + PushSnappedLinkState(editor.InteractionStack, start_pin_id, hovered_pin.Id); - if (!should_snap) - { - editor.ClickInteraction.LinkCreation.EndPinIdx.Reset(); - } + const bool link_creation_on_snap = + hovered_pin.Flags & ImNodesAttributeFlags_EnableLinkCreationOnSnap; - const bool create_link = - should_snap && (GImNodes->LeftMouseReleased || link_creation_on_snap); + if (link_creation_on_snap) + { + link_was_created = true; + GImNodes->ImNodesUIState |= ImNodesUIState_LinkCreated; + } + } + } - if (create_link && !maybe_duplicate_link_idx.HasValue()) + if (GImNodes->LeftMouseReleased) { - // Avoid send OnLinkCreated() events every frame if the snap link is not saved - // (only applies for EnableLinkCreationOnSnap) - if (!GImNodes->LeftMouseReleased && - editor.ClickInteraction.LinkCreation.EndPinIdx == GImNodes->HoveredPinIdx) + editor.InteractionStack.pop_back(); + + if (!link_was_created) { - break; + GImNodes->ImNodesUIState |= ImNodesUIState_LinkDropped; } - - GImNodes->ImNodesUIState |= ImNodesUIState_LinkCreated; - editor.ClickInteraction.LinkCreation.EndPinIdx = GImNodes->HoveredPinIdx.Value(); } + } + break; + case ImNodesInteractionType_SnappedLink: + { + // TODO: what if the pin id changes? + const bool snapping_pin_changed = !GImNodes->HoveredPinIdx.HasValue(); - if (GImNodes->LeftMouseReleased) + // Detach the link that was created by this link event if it's no longer in snap range + if (snapping_pin_changed) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; - if (!create_link) + editor.InteractionStack.pop_back(); + + if (GImNodes->SnapLinkIdx.HasValue()) { - GImNodes->ImNodesUIState |= ImNodesUIState_LinkDropped; + GImNodes->DeletedLinkIdx = GImNodes->SnapLinkIdx.Value(); } } + + if (GImNodes->LeftMouseReleased) + { + editor.InteractionStack.resize(0); + } } break; - case ImNodesClickInteractionType_Panning: + case ImNodesInteractionType_Panning: { const bool dragging = GImNodes->AltMouseDragging; @@ -1067,19 +1085,18 @@ void ClickInteractionUpdate( } else { - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + editor.InteractionStack.pop_back(); } } break; - case ImNodesClickInteractionType_ImGuiItem: + case ImNodesInteractionType_ImGuiItem: { if (GImNodes->LeftMouseReleased) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + editor.InteractionStack.pop_back(); } } - case ImNodesClickInteractionType_None: - break; + break; default: IM_ASSERT(!"Unreachable code!"); break; @@ -1193,7 +1210,7 @@ ImOptionalIndex ResolveHoveredNode(const ImVector& depth_stack) return ImOptionalIndex(node_idx_on_top); } -int ResolveAttachedLink( +ImOptionalIndex ResolveAttachedLink( const ImVector& links, const ImObjectPool& pins, const int hovered_pin_idx) @@ -1201,15 +1218,14 @@ int ResolveAttachedLink( const int pin_id = pins.Pool[hovered_pin_idx].Id; for (int idx = 0; idx < links.size(); ++idx) { - const ImLinkds& link = links[idx]; + const ImLink& link = links[idx]; if (pin_id == link.StartPinId || pin_id == link.EndPinId) { - return idx; + return ImOptionalIndex(idx); } } - IM_ASSERT(!"unreachable code ResolveAttachedLink"); - return 0; + return ImOptionalIndex(); } ImOptionalIndex ResolveHoveredLink( @@ -1457,9 +1473,7 @@ void DrawNode(ImNodesEditorContext& editor, const int node_idx) const ImNodeData& node = editor.Nodes.Pool[node_idx]; ImGui::SetCursorPos(node.Origin + editor.Panning); - const bool node_hovered = - GImNodes->HoveredNodeIdx == node_idx && - editor.ClickInteraction.Type != ImNodesClickInteractionType_BoxSelection; + const bool node_hovered = GImNodes->HoveredNodeIdx == node_idx; ImU32 node_background = node.ColorStyle.Background; ImU32 titlebar_background = node.ColorStyle.Titlebar; @@ -1537,12 +1551,10 @@ void DrawNode(ImNodesEditorContext& editor, const int node_idx) } void CalcLinkGeometries( - const ImObjectPool& pins, - const ImVector& links, - ImVector& curves) + ImObjectPool& pins, + const ImVector& links, + ImVector& curves) { - IM_ASSERT(!link_ids.empty()); - const float link_line_segments_per_length = GImNodes->Style.LinkLineSegmentsPerLength; for (const ImLink& link : links) @@ -1556,7 +1568,7 @@ void CalcLinkGeometries( } void DrawLinks( - const ImNodesEditorContext& editor, + ImNodesEditorContext& editor, // TODO: const const ImVector& links, const ImVector& curves) { @@ -1569,7 +1581,7 @@ void DrawLinks( const ImLink& link = links[idx]; const ImCubicBezier& curve = curves[idx]; - const bool link_hovered = GImNodes->HoveredLinkIdx == link_idx; + const bool link_hovered = GImNodes->HoveredLinkIdx == idx; ImU32 link_color = link.BaseColor; if (link_hovered) @@ -1588,6 +1600,53 @@ void DrawLinks( #endif curve.P0, curve.P1, curve.P2, curve.P3, link_color, link_thickness, curve.NumSegments); } + + if (std::find_if( + editor.InteractionStack.begin(), + editor.InteractionStack.end(), + [](const ImInteractionState& state) -> bool { + return (state.Type == ImNodesInteractionType_PartialLink) || + (state.Type == ImNodesInteractionType_SnappedLink); + }) != editor.InteractionStack.end()) + { + const ImInteractionState& state = editor.InteractionStack.back(); + + ImVec2 start_pos, end_pos; + ImNodesAttributeType start_pin_type; + + if (state.Type == ImNodesInteractionType_PartialLink) + { + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); + start_pos = start_pin.Pos; + start_pin_type = start_pin.Type; + end_pos = GImNodes->MousePos; + } + else + { + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId); + start_pos = start_pin.Pos; + start_pin_type = start_pin.Type; + end_pos = ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId).Pos; + } + + const ImCubicBezier cb = CalcCubicBezier( + start_pos, end_pos, start_pin_type, GImNodes->Style.LinkLineSegmentsPerLength); + +#if IMGUI_VERSION_NUM < 18000 + GimNodes->CanvasDrawList->AddBezierCurve( +#else + GImNodes->CanvasDrawList->AddBezierCubic( +#endif + cb.P0, + cb.P1, + cb.P2, + cb.P3, + GImNodes->Style.Colors[ImNodesCol_Link], + GImNodes->Style.LinkThickness, + cb.NumSegments); + } } void BeginPinAttribute( @@ -1663,13 +1722,13 @@ void Shutdown(ImNodesContext* ctx) { EditorContextFree(ctx->DefaultEditorCtx); } // [SECTION] minimap -static inline bool IsMiniMapActive() +inline bool IsMiniMapActive() { ImNodesEditorContext& editor = EditorContextGet(); return editor.MiniMapEnabled && editor.MiniMapSizeFraction > 0.0f; } -static inline bool IsMiniMapHovered() +inline bool IsMiniMapHovered() { ImNodesEditorContext& editor = EditorContextGet(); return IsMiniMapActive() && @@ -1694,7 +1753,7 @@ static inline void CalcMiniMapLayout() const ImVec2 grid_content_size = editor.GridContentBounds.IsInverted() ? max_size : ImFloor(editor.GridContentBounds.GetSize()); - const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; + const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; mini_map_size = ImFloor( grid_content_aspect_ratio > max_size_aspect_ratio ? ImVec2(max_size.x, max_size.x / grid_content_aspect_ratio) @@ -1751,7 +1810,7 @@ static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx) ImU32 mini_map_node_background; - if (editor.ClickInteraction.Type == ImNodesClickInteractionType_None && + if (editor.InteractionStack.size() == 0 && ImGui::IsMouseHoveringRect(node_rect.Min, node_rect.Max)) { mini_map_node_background = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; @@ -1795,9 +1854,9 @@ void MiniMapDrawLinks( const ImLink& link = links[idx]; const ImCubicBezier& curve = curves[idx]; - const int color_idx = editor.SelectedLinkIds.contains(link.Id) - ? ImNodesCol_MiniMapLinkSelected - : ImNodesCol_MiniMapLink; + const int color_idx = editor.SelectedLinkIds.contains(link.Id) + ? ImNodesCol_MiniMapLinkSelected + : ImNodesCol_MiniMapLink; const ImU32 link_color = GImNodes->Style.Colors[color_idx]; #if IMGUI_VERSION_NUM < 18000 @@ -1877,7 +1936,7 @@ static void MiniMapUpdate() ImGui::EndChild(); bool center_on_click = mini_map_is_hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left) && - editor.ClickInteraction.Type == ImNodesClickInteractionType_None && + editor.InteractionStack.size() == 0 && !GImNodes->NodeIdxSubmissionOrder.empty(); if (center_on_click) { @@ -2233,7 +2292,7 @@ void EndNodeEditor() if (GImNodes->LeftMouseClicked && ImGui::IsAnyItemActive()) { - editor.ClickInteraction.Type = ImNodesClickInteractionType_ImGuiItem; + PushImGuiState(editor.InteractionStack); } // Detect which UI element is being hovered over. Detection is done in a hierarchical fashion, @@ -2243,9 +2302,14 @@ void EndNodeEditor() // its an *overlay* with its own interaction behavior and must have precedence during mouse // interaction. - if ((editor.ClickInteraction.Type == ImNodesClickInteractionType_None || - editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation) && - MouseInCanvas() && !IsMiniMapHovered()) + if ((editor.InteractionStack.size() == 0 || std::find_if( + editor.InteractionStack.begin(), + editor.InteractionStack.end(), + [](const ImInteractionState& state) -> bool { + return state.Type == + ImNodesInteractionType_PartialLink; + }) != editor.InteractionStack.end() && + MouseInCanvas() && !IsMiniMapHovered())) { // Pins needs some special care. We need to check the depth stack to see which pins are // being occluded by other nodes. @@ -2253,7 +2317,13 @@ void EndNodeEditor() GImNodes->HoveredPinIdx = ResolveHoveredPin(editor.Pins, GImNodes->OccludedPinIndices); - if (!GImNodes->HoveredPinIdx.HasValue()) + if (!GImNodes->HoveredPinIdx.HasValue() && + std::find_if( + editor.InteractionStack.begin(), + editor.InteractionStack.end(), + [](const ImInteractionState& state) -> bool { + return state.Type == ImNodesInteractionType_BoxSelector; + }) != editor.InteractionStack.end()) { // Resolve which node is actually on top and being hovered using the depth stack. GImNodes->HoveredNodeIdx = ResolveHoveredNode(editor.NodeDepthOrder); @@ -2297,11 +2367,12 @@ void EndNodeEditor() // Handle node graph interaction - if (!IsMiniMapHovered()) + if (!IsMiniMapHovered() && editor.InteractionStack.size() == 0) { if (GImNodes->LeftMouseClicked && GImNodes->HoveredLinkIdx.HasValue()) { - BeginLinkInteraction(editor, GImNodes->HoveredLinkIdx.Value(), GImNodes->HoveredPinIdx); + BeginLinkInteraction( + editor, GImNodes->Links, GImNodes->HoveredLinkIdx.Value(), GImNodes->HoveredPinIdx); } else if (GImNodes->LeftMouseClicked && GImNodes->HoveredPinIdx.HasValue()) @@ -2313,7 +2384,6 @@ void EndNodeEditor() { BeginNodeSelection(editor, GImNodes->HoveredNodeIdx.Value()); } - else if ( GImNodes->LeftMouseClicked || GImNodes->LeftMouseReleased || GImNodes->AltMouseClicked || GImNodes->AltMouseScrollDelta != 0.f) @@ -2321,10 +2391,15 @@ void EndNodeEditor() BeginCanvasInteraction(editor); } - bool should_auto_pan = - editor.ClickInteraction.Type == ImNodesClickInteractionType_BoxSelection || - editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation || - editor.ClickInteraction.Type == ImNodesClickInteractionType_Node; + const bool should_auto_pan = + std::find_if( + editor.InteractionStack.begin(), + editor.InteractionStack.end(), + [](const ImInteractionState& state) -> bool { + return state.Type == ImNodesInteractionType_BoxSelector || + ImNodesInteractionType_PartialLink || ImNodesInteractionType_Node; + }) != editor.InteractionStack.end(); + if (should_auto_pan && !MouseInCanvas()) { ImVec2 mouse = ImGui::GetMousePos(); @@ -2337,7 +2412,11 @@ void EndNodeEditor() editor.Panning += editor.AutoPanningDelta; } } - ClickInteractionUpdate(editor, GImNodes->Links, GImNodes->Curves); + + if (editor.InteractionStack.size() > 0) + { + InteractionStackUpdate(editor, GImNodes->Links, GImNodes->Curves); + } // At this point, draw commands have been issued for all nodes (and pins). Update the node pool // to detect unused node slots and remove those indices from the depth stack before sorting the @@ -2536,21 +2615,26 @@ void Link(const int id, const int start_attr_id, const int end_attr_id) { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); - const int link_idx = GImNodes->Links.Ids.size(); - GImNodes->Links.push_back(ImLink(id, start_attr_id, end_attr_id, GImNodes->Style.Colors)); + const int link_idx = GImNodes->Links.size(); + const ImLink link(id, start_attr_id, end_attr_id, GImNodes->Style.Colors); + GImNodes->Links.push_back(link); - const ImNodesEditorContext& editor = EditorContextGet(); - const ImPinData& end_pin = FindOrCreateObject(editor.Pins, link.EndPinId); + ImNodesEditorContext& editor = EditorContextGet(); // Check if this link was created by the current link event - if ((editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation && - editor.Pins.Pool[link.EndPinIdx].Flags & ImNodesAttributeFlags_EnableLinkCreationOnSnap && - editor.ClickInteraction.LinkCreation.StartPinIdx == link.StartPinIdx && - editor.ClickInteraction.LinkCreation.EndPinIdx == link.EndPinIdx) || - (editor.ClickInteraction.LinkCreation.StartPinIdx == link.EndPinIdx && - editor.ClickInteraction.LinkCreation.EndPinIdx == link.StartPinIdx)) + + if (editor.InteractionStack.size() > 0) { - GImNodes->SnapLinkIdx = ObjectPoolFindOrCreateIndex(editor.Links, id); + const ImInteractionState& state = editor.InteractionStack.back(); + + if (state.Type == ImNodesInteractionType_SnappedLink && + (state.SnappedLink.StartPinId == start_attr_id && + state.SnappedLink.SnappedPinId == end_attr_id) || + (state.SnappedLink.StartPinId == end_attr_id && + state.SnappedLink.SnappedPinId == start_attr_id)) + { + GImNodes->SnapLinkIdx = link_idx; + } } } @@ -2750,7 +2834,7 @@ bool IsLinkHovered(int* const link_id) const bool is_hovered = GImNodes->HoveredLinkIdx.HasValue(); if (is_hovered) { - *link_id = GImNodes->Links[GImNodes->HoveredLinkIdx.Value].Id; + *link_id = GImNodes->Links[GImNodes->HoveredLinkIdx.Value()].Id; } return is_hovered; } @@ -2802,7 +2886,7 @@ void GetSelectedLinks(int* link_ids) const ImNodesEditorContext& editor = EditorContextGet(); for (int i = 0; i < editor.SelectedLinkIds.size(); ++i) { - link_idx[i] = editor.SelectedLinkIds[i]; + link_ids[i] = editor.SelectedLinkIds[i]; } } @@ -2893,8 +2977,9 @@ bool IsLinkStarted(int* const started_at_id) if (is_started) { const ImNodesEditorContext& editor = EditorContextGet(); - const int pin_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; - *started_at_id = editor.Pins.Pool[pin_idx].Id; + IM_ASSERT(editor.InteractionStack.size() > 0); + IM_ASSERT(editor.InteractionStack.back().Type == ImNodesInteractionType_PartialLink); + *started_at_id = editor.InteractionStack.back().PartialLink.StartPinId; } return is_started; @@ -2906,16 +2991,15 @@ bool IsLinkDropped(int* const started_at_id, const bool including_detached_links IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); const ImNodesEditorContext& editor = EditorContextGet(); + const ImInteractionState& state = editor.InteractionStack.back(); - const bool link_dropped = - (GImNodes->ImNodesUIState & ImNodesUIState_LinkDropped) != 0 && - (including_detached_links || - editor.ClickInteraction.LinkCreation.Type != ImNodesLinkCreationType_FromDetach); + IM_ASSERT(state.Type == ImNodesInteractionType_PartialLink); + const bool link_dropped = (GImNodes->ImNodesUIState & ImNodesUIState_LinkDropped) != 0 && + (including_detached_links || !state.PartialLink.CreatedFromSnap); if (link_dropped && started_at_id) { - const int pin_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; - *started_at_id = editor.Pins.Pool[pin_idx].Id; + *started_at_id = state.PartialLink.StartPinId; } return link_dropped; @@ -2934,27 +3018,30 @@ bool IsLinkCreated( if (is_created) { - const ImNodesEditorContext& editor = EditorContextGet(); - const int start_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; - const int end_idx = editor.ClickInteraction.LinkCreation.EndPinIdx.Value(); - const ImPinData& start_pin = editor.Pins.Pool[start_idx]; - const ImPinData& end_pin = editor.Pins.Pool[end_idx]; + ImNodesEditorContext& editor = EditorContextGet(); // TODO: const + IM_ASSERT(editor.InteractionStack.size() > 0); + const ImInteractionState& state = editor.InteractionStack.back(); + IM_ASSERT( + state.Type == ImNodesInteractionType_PartialLink || + state.Type == ImNodesInteractionType_SnappedLink); - if (start_pin.Type == ImNodesAttributeType_Output) + if (state.Type == ImNodesInteractionType_PartialLink) { - *started_at_pin_id = start_pin.Id; - *ended_at_pin_id = end_pin.Id; + *started_at_pin_id = + ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId).Id; + *ended_at_pin_id = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()].Id; } else { - *started_at_pin_id = end_pin.Id; - *ended_at_pin_id = start_pin.Id; + *started_at_pin_id = + ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.StartPinId).Id; + *ended_at_pin_id = + ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId).Id; } if (created_from_snap) { - *created_from_snap = - editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation; + *created_from_snap = (state.Type == ImNodesInteractionType_SnappedLink); } } @@ -2978,33 +3065,38 @@ bool IsLinkCreated( if (is_created) { - const ImNodesEditorContext& editor = EditorContextGet(); - const int start_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; - const int end_idx = editor.ClickInteraction.LinkCreation.EndPinIdx.Value(); - const ImPinData& start_pin = editor.Pins.Pool[start_idx]; - const ImNodeData& start_node = editor.Nodes.Pool[start_pin.ParentNodeIdx]; - const ImPinData& end_pin = editor.Pins.Pool[end_idx]; - const ImNodeData& end_node = editor.Nodes.Pool[end_pin.ParentNodeIdx]; + ImNodesEditorContext& editor = EditorContextGet(); // TODO: const + IM_ASSERT(editor.InteractionStack.size() > 0); + const ImInteractionState& state = editor.InteractionStack.back(); + IM_ASSERT( + state.Type == ImNodesInteractionType_PartialLink || + state.Type == ImNodesInteractionType_SnappedLink); - if (start_pin.Type == ImNodesAttributeType_Output) + if (state.Type == ImNodesInteractionType_PartialLink) { + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); + const ImPinData& hovered_pin = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]; + *started_at_node_id = editor.Nodes.Pool[start_pin.ParentNodeIdx].Id; *started_at_pin_id = start_pin.Id; - *started_at_node_id = start_node.Id; - *ended_at_pin_id = end_pin.Id; - *ended_at_node_id = end_node.Id; + *ended_at_node_id = editor.Nodes.Pool[hovered_pin.ParentNodeIdx].Id; + *ended_at_pin_id = hovered_pin.Id; } else { - *started_at_pin_id = end_pin.Id; - *started_at_node_id = end_node.Id; - *ended_at_pin_id = start_pin.Id; - *ended_at_node_id = start_node.Id; + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.StartPinId); + const ImPinData& snapped_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId); + *started_at_node_id = editor.Nodes.Pool[start_pin.ParentNodeIdx].Id; + *started_at_pin_id = start_pin.Id; + *ended_at_node_id = editor.Nodes.Pool[snapped_pin.ParentNodeIdx].Id; + *ended_at_pin_id = snapped_pin.Id; } if (created_from_snap) { - *created_from_snap = - editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation; + *created_from_snap = (state.Type == ImNodesInteractionType_SnappedLink); } } @@ -3018,9 +3110,8 @@ bool IsLinkDestroyed(int* const link_id) const bool link_destroyed = GImNodes->DeletedLinkIdx.HasValue(); if (link_destroyed) { - const ImNodesEditorContext& editor = EditorContextGet(); - const int link_idx = GImNodes->DeletedLinkIdx.Value(); - *link_id = GImNodes->Links[link_ids].Id; + const int link_idx = GImNodes->DeletedLinkIdx.Value(); + *link_id = GImNodes->Links[link_idx].Id; } return link_destroyed; diff --git a/imnodes_internal.h b/imnodes_internal.h index a34402e..a202249 100644 --- a/imnodes_internal.h +++ b/imnodes_internal.h @@ -26,6 +26,7 @@ typedef int ImNodesAttributeType; typedef int ImNodesUIState; typedef int ImNodesClickInteractionType; typedef int ImNodesLinkCreationType; +typedef int ImNodesInteractionType; enum ImNodesScope_ { @@ -50,17 +51,6 @@ enum ImNodesUIState_ ImNodesUIState_LinkCreated = 1 << 2 }; -enum ImNodesClickInteractionType_ -{ - ImNodesClickInteractionType_Node, - ImNodesClickInteractionType_Link, - ImNodesClickInteractionType_LinkCreation, - ImNodesClickInteractionType_Panning, - ImNodesClickInteractionType_BoxSelection, - ImNodesClickInteractionType_ImGuiItem, - ImNodesClickInteractionType_None -}; - enum ImNodesLinkCreationType_ { ImNodesLinkCreationType_Standard, @@ -208,23 +198,48 @@ struct ImCubicBezier int NumSegments; }; -struct ImClickInteractionState +struct ImBoxSelector { - ImNodesClickInteractionType Type; + ImRect GridSpaceRect; +}; - struct - { - int StartPinIdx; - ImOptionalIndex EndPinIdx; - ImNodesLinkCreationType Type; - } LinkCreation; +// A link which is connected to the mouse cursor at the other end +struct ImPartialLink +{ + int StartPinId; + bool CreatedFromSnap; +}; - struct +struct ImSnappedLink +{ + int StartPinId; + int SnappedPinId; +}; + +enum ImNodesInteractionType_ +{ + ImNodesInteractionType_BoxSelector, + ImNodesInteractionType_Panning, + ImNodesInteractionType_Link, + ImNodesInteractionType_PartialLink, + ImNodesInteractionType_SnappedLink, + ImNodesInteractionType_Node, + ImNodesInteractionType_ImGuiItem, + ImNodesInteractionType_None +}; + +struct ImInteractionState +{ + ImNodesInteractionType Type; + + union { - ImRect Rect; // Coordinates in grid space - } BoxSelector; + ImBoxSelector BoxSelector; + ImPartialLink PartialLink; + ImSnappedLink SnappedLink; + }; - ImClickInteractionState() : Type(ImNodesClickInteractionType_None) {} + ImInteractionState(const ImNodesInteractionType type) : Type(type) {} }; struct ImNodesColElement @@ -276,7 +291,7 @@ struct ImNodesEditorContext // Offset of the primary node origin relative to the mouse cursor. ImVec2 PrimaryNodeOffset; - ImClickInteractionState ClickInteraction; + ImVector InteractionStack; // Mini-map state set by MiniMap() @@ -294,7 +309,7 @@ struct ImNodesEditorContext ImNodesEditorContext() : Nodes(), Pins(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIds(), - SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), ClickInteraction(), + SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), InteractionStack(), MiniMapEnabled(false), MiniMapSizeFraction(0.0f), MiniMapNodeHoveringCallback(NULL), MiniMapNodeHoveringCallbackUserData(NULL), MiniMapScaling(0.0f) { From fab5070a4171d4e1cb2ce96b848897474fa6eb3b Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Sat, 28 May 2022 10:16:51 +0300 Subject: [PATCH 5/9] Second link interaction refactor - A state stack wasn't necessary and introduced needless complexity - This change removes EnableLinKDetachWithDragClick from the code entirely. --- example/multi_editor.cpp | 1 - example/save_load.cpp | 1 - imnodes.cpp | 1389 ++++++++++++++++++-------------------- imnodes.h | 4 - imnodes_internal.h | 87 ++- 5 files changed, 703 insertions(+), 779 deletions(-) diff --git a/example/multi_editor.cpp b/example/multi_editor.cpp index cbe0a23..a5903f4 100644 --- a/example/multi_editor.cpp +++ b/example/multi_editor.cpp @@ -117,7 +117,6 @@ void NodeEditorInitialize() { editor1.context = ImNodes::EditorContextCreate(); editor2.context = ImNodes::EditorContextCreate(); - ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick); ImNodesIO& io = ImNodes::GetIO(); io.LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; diff --git a/example/save_load.cpp b/example/save_load.cpp index 7a21376..8240baf 100644 --- a/example/save_load.cpp +++ b/example/save_load.cpp @@ -192,7 +192,6 @@ static SaveLoadEditor editor; void NodeEditorInitialize() { ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; - ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick); editor.load(); } diff --git a/imnodes.cpp b/imnodes.cpp index 3ac2237..04f79c5 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -19,6 +19,7 @@ #endif #include +#include #include #include #include @@ -538,296 +539,19 @@ ImVec2 GetScreenSpacePinCoordinates( return ImVec2(x, 0.5f * (attribute_rect.Min.y + attribute_rect.Max.y)); } -ImVec2 GetScreenSpacePinCoordinates(const ImNodesEditorContext& editor, const ImPinData& pin) -{ - const ImRect& parent_node_rect = editor.Nodes.Pool[pin.ParentNodeIdx].Rect; - return GetScreenSpacePinCoordinates(parent_node_rect, pin.AttributeRect, pin.Type); -} - -bool MouseInCanvas() +bool MouseInCanvas(const ImNodesContext& context) { // This flag should be true either when hovering or clicking something in the canvas. - const bool is_window_hovered_or_focused = ImGui::IsWindowHovered() || ImGui::IsWindowFocused(); + const bool is_window_hovered_or_focused = + ImGui::IsWindowHovered(ImGuiFocusedFlags_RootAndChildWindows) || + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); return is_window_hovered_or_focused && - GImNodes->CanvasRectScreenSpace.Contains(ImGui::GetMousePos()); -} - -inline void PushNodeState(ImVector& interaction_stack) -{ - IM_ASSERT(interaction_stack.size() == 0); - - ImInteractionState state(ImNodesInteractionType_Node); - - interaction_stack.push_back(state); -} - -void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) -{ - // If the node is not already contained in the selection, then we want only - // the interaction node to be selected, effective immediately. - // - // If the multiple selection modifier is active, we want to add this node - // to the current list of selected nodes. - // - // Otherwise, we want to allow for the possibility of multiple nodes to be - // moved at once. - if (!editor.SelectedNodeIndices.contains(node_idx)) - { - PushNodeState(editor.InteractionStack); - - editor.SelectedLinkIds.clear(); - if (!GImNodes->MultipleSelectModifier) - { - editor.SelectedNodeIndices.clear(); - } - editor.SelectedNodeIndices.push_back(node_idx); - - // Ensure that individually selected nodes get rendered on top - ImVector& depth_stack = editor.NodeDepthOrder; - const int* const elem = depth_stack.find(node_idx); - IM_ASSERT(elem != depth_stack.end()); - depth_stack.erase(elem); - depth_stack.push_back(node_idx); - } - // Deselect a previously-selected node - else if (GImNodes->MultipleSelectModifier) - { - const int* const node_ptr = editor.SelectedNodeIndices.find(node_idx); - editor.SelectedNodeIndices.erase(node_ptr); - } - - // To support snapping of multiple nodes, we need to store the offset of - // each node in the selection to the origin of the dragged node. - const ImVec2 ref_origin = editor.Nodes.Pool[node_idx].Origin; - editor.PrimaryNodeOffset = - ref_origin + GImNodes->CanvasOriginScreenSpace + editor.Panning - GImNodes->MousePos; - - editor.SelectedNodeOffsets.clear(); - for (int idx = 0; idx < editor.SelectedNodeIndices.Size; idx++) - { - const int node = editor.SelectedNodeIndices[idx]; - const ImVec2 node_origin = editor.Nodes.Pool[node].Origin - ref_origin; - editor.SelectedNodeOffsets.push_back(node_origin); - } -} - -inline void PushLinkState(ImVector& interaction_stack) -{ - IM_ASSERT(interaction_stack.size() == 0); - - ImInteractionState state(ImNodesInteractionType_Link); - - interaction_stack.push_back(state); -} - -void BeginLinkSelection(ImNodesEditorContext& editor, const int link_id) -{ - PushLinkState(editor.InteractionStack); - // When a link is selected, clear all other selections, and insert the link - // as the sole selection. - editor.SelectedNodeIndices.clear(); - editor.SelectedLinkIds.clear(); - editor.SelectedLinkIds.push_back(link_id); -} - -inline void PushPartialLinkState( - ImVector& interaction_stack, - const int start_pin_id, - const bool from_snap) -{ - IM_ASSERT(interaction_stack.size() == 0); - - ImInteractionState state(ImNodesInteractionType_PartialLink); - state.PartialLink.StartPinId = start_pin_id; - state.PartialLink.CreatedFromSnap = from_snap; - - interaction_stack.push_back(state); -} - -void BeginLinkCreation(ImNodesEditorContext& editor, const int hovered_pin_idx) -{ - PushPartialLinkState(editor.InteractionStack, editor.Pins.Pool[hovered_pin_idx].Id, false); - GImNodes->ImNodesUIState |= ImNodesUIState_LinkStarted; -} - -void BeginLinkInteraction( - ImNodesEditorContext& editor, - const ImVector& links, - const int link_idx, - const ImOptionalIndex pin_idx = ImOptionalIndex()) -{ - // Check if we are clicking the link with the modifier pressed. - // This will in a link detach via clicking. - - const bool modifier_pressed = GImNodes->Io.LinkDetachWithModifierClick.Modifier == NULL - ? false - : *GImNodes->Io.LinkDetachWithModifierClick.Modifier; - - const ImLink& link = GImNodes->Links[link_idx]; - - if (modifier_pressed) - { - - const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); - const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPinId); - const ImVec2& mouse_pos = GImNodes->MousePos; - const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); - const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); - const int closest_pin_id = dist_to_start < dist_to_end ? link.StartPinId : link.EndPinId; - - PushPartialLinkState(editor.InteractionStack, closest_pin_id, true); - GImNodes->DeletedLinkIdx = link_idx; - } - else - { - if (pin_idx.HasValue()) - { - const ImPinData& hovered_pin = editor.Pins.Pool[pin_idx.Value()]; - - // Check the 'click and drag to detach' case. - if (hovered_pin.Flags & ImNodesAttributeFlags_EnableLinkDetachWithDragClick) - { - const int start_id = - hovered_pin.Id == link.StartPinId ? link.EndPinId : link.StartPinId; - PushPartialLinkState(editor.InteractionStack, start_id, true); - GImNodes->DeletedLinkIdx = link_idx; - } - else - { - BeginLinkCreation(editor, pin_idx.Value()); - } - } - else - { - BeginLinkSelection(editor, links[link_idx].Id); - } - } + context.CanvasRectScreenSpace.Contains(ImGui::GetMousePos()); } inline bool IsMiniMapHovered(); -inline void PushBoxSelectorState( - ImVector& interaction_stack, - const ImVec2& mouse_pos) -{ - IM_ASSERT(interaction_stack.size() == 0); - - ImInteractionState state(ImNodesInteractionType_BoxSelector); - state.BoxSelector.GridSpaceRect.Min = mouse_pos; - - interaction_stack.push_back(state); -} - -inline void PushPanningState(ImVector& interaction_stack) -{ - IM_ASSERT(interaction_stack.size() == 0); - - ImInteractionState state(ImNodesInteractionType_Panning); - - interaction_stack.push_back(state); -} - -inline void PushSnappedLinkState( - ImVector& interaction_stack, - const int start_pin_id, - const int snapped_pin_id) -{ - IM_ASSERT(interaction_stack.size() > 0); - - ImInteractionState state(ImNodesInteractionType_SnappedLink); - state.SnappedLink.StartPinId = start_pin_id; - state.SnappedLink.SnappedPinId = snapped_pin_id; - - interaction_stack.push_back(state); -} - -inline void PushImGuiState(ImVector& interaction_stack) -{ - ImInteractionState state(ImNodesInteractionType_ImGuiItem); - - interaction_stack.push_back(state); -} - -void BeginCanvasInteraction(ImNodesEditorContext& editor) -{ - const bool any_ui_element_hovered = - GImNodes->HoveredNodeIdx.HasValue() || GImNodes->HoveredLinkIdx.HasValue() || - GImNodes->HoveredPinIdx.HasValue() || ImGui::IsAnyItemHovered(); - - const bool mouse_not_in_canvas = !MouseInCanvas(); - - if (any_ui_element_hovered || mouse_not_in_canvas) - { - return; - } - - const bool started_panning = GImNodes->AltMouseClicked; - - if (started_panning) - { - PushPanningState(editor.InteractionStack); - } - else if (GImNodes->LeftMouseClicked) - { - PushBoxSelectorState(editor.InteractionStack, GImNodes->MousePos); - } -} - -void BoxSelectorUpdateSelection( - ImNodesEditorContext& editor, - const ImVector& links, - const ImVector& curves, - ImRect box_rect) -{ - // Invert box selector coordinates as needed - - if (box_rect.Min.x > box_rect.Max.x) - { - ImSwap(box_rect.Min.x, box_rect.Max.x); - } - - if (box_rect.Min.y > box_rect.Max.y) - { - ImSwap(box_rect.Min.y, box_rect.Max.y); - } - - // Update node selection - - editor.SelectedNodeIndices.clear(); - - // Test for overlap against node rectangles - - for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) - { - if (editor.Nodes.InUse[node_idx]) - { - ImNodeData& node = editor.Nodes.Pool[node_idx]; - if (box_rect.Overlaps(node.Rect)) - { - editor.SelectedNodeIndices.push_back(node_idx); - } - } - } - - // Update link selection - - editor.SelectedLinkIds.clear(); - - // Test for overlap against links - - for (int idx = 0; idx < links.size(); ++idx) - { - const ImLink& link = links[idx]; - const ImCubicBezier& cb = curves[idx]; - - if (RectangleOverlapsLink(box_rect, cb)) - { - editor.SelectedLinkIds.push_back(link.Id); - } - } -} - ImVec2 SnapOriginToGrid(ImVec2 origin) { if (GImNodes->Style.Flags & ImNodesStyleFlags_GridSnapping) @@ -927,7 +651,7 @@ bool ShouldLinkSnapToPin( // The link to be created must not be a duplicate, unless it is the link which was created on // snap. In that case we want to snap, since we want it to appear visually as if the created // link remains snapped to the pin. - if (duplicate_link.HasValue() && !(duplicate_link == GImNodes->SnapLinkIdx)) + if (duplicate_link.HasValue()) { return false; } @@ -935,341 +659,715 @@ bool ShouldLinkSnapToPin( return true; } -void InteractionStackUpdate( - ImNodesEditorContext& editor, - const ImVector& links, - const ImVector& curves) +void ResolveOccludedPins(const ImNodesEditorContext& editor, ImVector& occluded_pin_indices) { - ImInteractionState& state = editor.InteractionStack.back(); - - switch (state.Type) - { - case ImNodesInteractionType_BoxSelector: - { - state.BoxSelector.GridSpaceRect.Max = ScreenSpaceToGridSpace(editor, GImNodes->MousePos); + const ImVector& depth_stack = editor.NodeDepthOrder; - ImRect box_rect = state.BoxSelector.GridSpaceRect; - box_rect.Min = GridSpaceToScreenSpace(editor, box_rect.Min); - box_rect.Max = GridSpaceToScreenSpace(editor, box_rect.Max); + occluded_pin_indices.resize(0); - BoxSelectorUpdateSelection(editor, links, curves, box_rect); + if (depth_stack.Size < 2) + { + return; + } - const ImU32 box_selector_color = GImNodes->Style.Colors[ImNodesCol_BoxSelector]; - const ImU32 box_selector_outline = GImNodes->Style.Colors[ImNodesCol_BoxSelectorOutline]; - GImNodes->CanvasDrawList->AddRectFilled(box_rect.Min, box_rect.Max, box_selector_color); - GImNodes->CanvasDrawList->AddRect(box_rect.Min, box_rect.Max, box_selector_outline); + // For each node in the depth stack + for (int depth_idx = 0; depth_idx < (depth_stack.Size - 1); ++depth_idx) + { + const ImNodeData& node_below = editor.Nodes.Pool[depth_stack[depth_idx]]; - if (GImNodes->LeftMouseReleased) + // Iterate over the rest of the depth stack to find nodes overlapping the pins + for (int next_depth_idx = depth_idx + 1; next_depth_idx < depth_stack.Size; + ++next_depth_idx) { - ImVector& depth_stack = editor.NodeDepthOrder; - const ImVector& selected_idxs = editor.SelectedNodeIndices; - - // Bump the selected node indices, in order, to the top of the depth stack. - // NOTE: this algorithm has worst case time complexity of O(N^2), if the node selection - // is ~ N (due to selected_idxs.contains()). + const ImRect& rect_above = editor.Nodes.Pool[depth_stack[next_depth_idx]].Rect; - if ((selected_idxs.Size > 0) && (selected_idxs.Size < depth_stack.Size)) + // Iterate over each pin + for (int idx = 0; idx < node_below.PinIndices.Size; ++idx) { - int num_moved = 0; // The number of indices moved. Stop after selected_idxs.Size - for (int i = 0; i < depth_stack.Size - selected_idxs.Size; ++i) + const int pin_idx = node_below.PinIndices[idx]; + const ImVec2& pin_pos = editor.Pins.Pool[pin_idx].Pos; + + if (rect_above.Contains(pin_pos)) { - for (int node_idx = depth_stack[i]; selected_idxs.contains(node_idx); - node_idx = depth_stack[i]) - { - depth_stack.erase(depth_stack.begin() + static_cast(i)); - depth_stack.push_back(node_idx); - ++num_moved; - } - - if (num_moved == selected_idxs.Size) - { - break; - } + occluded_pin_indices.push_back(pin_idx); } } - - editor.InteractionStack.pop_back(); } } - break; - case ImNodesInteractionType_Node: +} + +ImOptionalIndex ResolveHoveredPin( + const ImObjectPool& pins, + const ImVector& occluded_pin_indices) +{ + float smallest_distance = FLT_MAX; + ImOptionalIndex pin_idx_with_smallest_distance; + + const float hover_radius_sqr = GImNodes->Style.PinHoverRadius * GImNodes->Style.PinHoverRadius; + + for (int idx = 0; idx < pins.Pool.Size; ++idx) { - TranslateSelectedNodes(editor); + if (!pins.InUse[idx]) + { + continue; + } - if (GImNodes->LeftMouseReleased) + if (occluded_pin_indices.contains(idx)) { - editor.InteractionStack.pop_back(); + continue; + } + + const ImVec2& pin_pos = pins.Pool[idx].Pos; + const float distance_sqr = ImLengthSqr(pin_pos - GImNodes->MousePos); + + // TODO: GImNodes->Style.PinHoverRadius needs to be copied into pin data and the pin-local + // value used here. This is no longer called in BeginAttribute/EndAttribute scope and the + // detected pin might have a different hover radius than what the user had when calling + // BeginAttribute/EndAttribute. + if (distance_sqr < hover_radius_sqr && distance_sqr < smallest_distance) + { + smallest_distance = distance_sqr; + pin_idx_with_smallest_distance = idx; } } - break; - case ImNodesInteractionType_Link: + + return pin_idx_with_smallest_distance; +} + +ImOptionalIndex ResolveHoveredNode(const ImVector& depth_stack) +{ + if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 0) { - if (GImNodes->LeftMouseReleased) + return ImOptionalIndex(); + } + + if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 1) + { + return ImOptionalIndex(GImNodes->NodeIndicesOverlappingWithMouse[0]); + } + + int largest_depth_idx = -1; + int node_idx_on_top = -1; + + for (int i = 0; i < GImNodes->NodeIndicesOverlappingWithMouse.size(); ++i) + { + const int node_idx = GImNodes->NodeIndicesOverlappingWithMouse[i]; + for (int depth_idx = 0; depth_idx < depth_stack.size(); ++depth_idx) { - editor.InteractionStack.pop_back(); + if (depth_stack[depth_idx] == node_idx && (depth_idx > largest_depth_idx)) + { + largest_depth_idx = depth_idx; + node_idx_on_top = node_idx; + } } } - break; - case ImNodesInteractionType_PartialLink: - { - const int start_pin_id = state.PartialLink.StartPinId; - const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, start_pin_id); - bool link_was_created = false; + IM_ASSERT(node_idx_on_top != -1); + return ImOptionalIndex(node_idx_on_top); +} - if (GImNodes->HoveredPinIdx.HasValue()) +ImOptionalIndex ResolveAttachedLink( + const ImVector& links, + const ImObjectPool& pins, + const int hovered_pin_idx) +{ + const int pin_id = pins.Pool[hovered_pin_idx].Id; + for (int idx = 0; idx < links.size(); ++idx) + { + const ImLink& link = links[idx]; + if (pin_id == link.StartPinId || pin_id == link.EndPinId) { - const ImPinData& hovered_pin = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]; + return ImOptionalIndex(idx); + } + } - const ImOptionalIndex maybe_duplicate_link_idx = - FindDuplicateLink(links, start_pin_id, hovered_pin.Id); + return ImOptionalIndex(); +} - const bool should_snap = ShouldLinkSnapToPin( - editor, start_pin, GImNodes->HoveredPinIdx.Value(), maybe_duplicate_link_idx); +ImOptionalIndex ResolveHoveredLink( + const ImVector& links, + const ImVector& curves, + const ImObjectPool& pins) +{ + float smallest_distance = FLT_MAX; + ImOptionalIndex link_idx_with_smallest_distance; - if (should_snap) - { - PushSnappedLinkState(editor.InteractionStack, start_pin_id, hovered_pin.Id); + // There are two ways a link can be detected as "hovered". + // 1. The link is within hover distance to the mouse. The closest such link is selected as being + // hovered over. + // 2. If the link is connected to the currently hovered pin. + // + // The latter is a requirement for link detaching with drag click to work, as both a link and + // pin are required to be hovered over for the feature to work. - const bool link_creation_on_snap = - hovered_pin.Flags & ImNodesAttributeFlags_EnableLinkCreationOnSnap; + const bool is_pin_hovered = GImNodes->HoveredPinIdx.HasValue(); + if (is_pin_hovered) + { + return ResolveAttachedLink(links, pins, GImNodes->HoveredPinIdx.Value()); + } - if (link_creation_on_snap) - { - link_was_created = true; - GImNodes->ImNodesUIState |= ImNodesUIState_LinkCreated; - } - } - } + for (int idx = 0; idx < curves.size(); ++idx) + { + const ImCubicBezier& curve = curves[idx]; + + // The distance test + const ImRect curve_bounds = GetContainingRectForCubicBezier(curve); - if (GImNodes->LeftMouseReleased) + if (curve_bounds.Contains(GImNodes->MousePos)) { - editor.InteractionStack.pop_back(); + const float distance = GetDistanceToCubicBezier(GImNodes->MousePos, curve); - if (!link_was_created) + if (distance < GImNodes->Style.LinkHoverDistance && distance < smallest_distance) { - GImNodes->ImNodesUIState |= ImNodesUIState_LinkDropped; + smallest_distance = distance; + link_idx_with_smallest_distance = idx; } } } - break; - case ImNodesInteractionType_SnappedLink: - { - // TODO: what if the pin id changes? - const bool snapping_pin_changed = !GImNodes->HoveredPinIdx.HasValue(); - // Detach the link that was created by this link event if it's no longer in snap range - if (snapping_pin_changed) + return link_idx_with_smallest_distance; +} + +void EnterNodeState(const ImNodesContext& context, ImNodesEditorContext& editor, const int node_idx) +{ + editor.InteractionState.Type = ImNodesInteractionType_Node; + + // Clear the existing node selection if the user clicked an unselected node. + const int* const node_ptr = editor.SelectedNodeIndices.find(node_idx); + + if (node_ptr == editor.SelectedNodeIndices.end()) + { + if (!context.MultipleSelectModifier) { - editor.InteractionStack.pop_back(); + editor.SelectedLinkIds.resize(0); + editor.SelectedNodeIndices.resize(0); + } + editor.SelectedNodeIndices.push_back(node_idx); - if (GImNodes->SnapLinkIdx.HasValue()) - { - GImNodes->DeletedLinkIdx = GImNodes->SnapLinkIdx.Value(); - } + // Ensure that individually selected nodes get rendered on top + ImVector& depth_stack = editor.NodeDepthOrder; + const int* const elem = depth_stack.find(node_idx); + IM_ASSERT(elem != depth_stack.end()); + depth_stack.erase(elem); + depth_stack.push_back(node_idx); + } + // Otherwise, deselect a previously-selected node + else if (context.MultipleSelectModifier) + { + editor.SelectedNodeIndices.erase_unsorted(node_ptr); + } + + // To support snapping of multiple nodes, we need to store the offset of + // each node in the selection to the origin of the dragged node. + const ImVec2 ref_origin = editor.Nodes.Pool[node_idx].Origin; + editor.PrimaryNodeOffset = + ref_origin + context.CanvasOriginScreenSpace + editor.Panning - context.MousePos; + + editor.SelectedNodeOffsets.clear(); + for (int idx = 0; idx < editor.SelectedNodeIndices.Size; idx++) + { + const int node = editor.SelectedNodeIndices[idx]; + const ImVec2 node_origin = editor.Nodes.Pool[node].Origin - ref_origin; + editor.SelectedNodeOffsets.push_back(node_origin); + } +} + +void EnterLinkState( + const ImNodesContext& context, + ImNodesEditorContext& editor, + const int hovered_link_id) +{ + editor.InteractionState.Type = ImNodesInteractionType_Link; + + const int* const link_ptr = editor.SelectedLinkIds.find(hovered_link_id); + + if (link_ptr == editor.SelectedLinkIds.end()) + { + // Clicking an unselected link + if (!context.MultipleSelectModifier) + { + editor.SelectedLinkIds.resize(0); + editor.SelectedNodeIndices.resize(0); } + editor.SelectedLinkIds.push_back(hovered_link_id); + } + else if (context.MultipleSelectModifier) + { + // Clicking a previously selected link with multiple select modifier should remove the + // selection + editor.SelectedLinkIds.erase_unsorted(link_ptr); + } + else + { + // Click a previously selected link without the multiple select modifier. Clear all + // selections, append link selection. + editor.SelectedLinkIds.resize(0); + editor.SelectedNodeIndices.resize(0); + editor.SelectedLinkIds.push_back(hovered_link_id); + } +} + +void EnterImGuiState(ImNodesEditorContext& editor) +{ + editor.InteractionState.Type = ImNodesInteractionType_ImGui; +} - if (GImNodes->LeftMouseReleased) +void EnterBoxSelectorState(ImNodesEditorContext& editor) +{ + editor.InteractionState.Type = ImNodesInteractionType_BoxSelector; + editor.InteractionState.BoxSelector.GridSpaceRect.Min = + ScreenSpaceToGridSpace(editor, ImGui::GetIO().MousePos); + editor.InteractionState.BoxSelector.GridSpaceRect.Max = ImVec2(-FLT_MAX, -FLT_MAX); +} + +void EnterPanningState(ImNodesEditorContext& editor) +{ + editor.InteractionState.Type = ImNodesInteractionType_Panning; +} + +void EnterPartialLinkState( + ImNodesContext& context, + ImNodesEditorContext& editor, + const int start_pin_id, + const bool created_from_detach) +{ + if (!created_from_detach) + { + context.Event.Type = ImNodesEventType_LinkStarted; + context.Event.LinkStarted.StartPinId = start_pin_id; + } + + editor.InteractionState.Type = ImNodesInteractionType_PartialLink; + editor.InteractionState.PartialLink.StartPinId = start_pin_id; + editor.InteractionState.PartialLink.CreatedFromDetach = created_from_detach; +} + +void EnterPendingState(ImNodesEditorContext& editor) +{ + editor.InteractionState.Type = ImNodesInteractionType_Pending; +} + +void EnterSnappedLinkState( + ImNodesEditorContext& editor, + const int start_pin_id, + const int end_pin_id) +{ + editor.InteractionState.Type = ImNodesInteractionType_SnappedLink; + editor.InteractionState.SnappedLink.StartPinId = start_pin_id; + editor.InteractionState.SnappedLink.SnappedPinId = end_pin_id; +} + +void PartialLinkStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) +{ + const ImInteractionState& state = editor.InteractionState; + const int start_pin_id = state.PartialLink.StartPinId; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, start_pin_id); + + // Hovered pin detection + + { + ResolveOccludedPins(editor, context.OccludedPinIndices); + context.HoveredPinIdx = ResolveHoveredPin(editor.Pins, context.OccludedPinIndices); + } + + // Partial link state + bool link_snapped_to_pin = false; + bool create_link_on_snap = false; + + if (context.HoveredPinIdx.HasValue()) + { + const ImPinData& hovered_pin = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]; + + const ImOptionalIndex maybe_duplicate_link_idx = + FindDuplicateLink(context.Links, start_pin_id, hovered_pin.Id); + + link_snapped_to_pin = ShouldLinkSnapToPin( + editor, start_pin, context.HoveredPinIdx.Value(), maybe_duplicate_link_idx); + if (link_snapped_to_pin) { - editor.InteractionStack.resize(0); + create_link_on_snap = + hovered_pin.Flags & ImNodesAttributeFlags_EnableLinkCreationOnSnap; } } - break; - case ImNodesInteractionType_Panning: + + // Render the bezier curve + { - const bool dragging = GImNodes->AltMouseDragging; + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); + const ImVec2 start_pos = start_pin.Pos; + const ImNodesAttributeType start_pin_type = start_pin.Type; - if (dragging) + ImVec2 end_pos; + if (link_snapped_to_pin) { - editor.Panning += ImGui::GetIO().MouseDelta; + end_pos = editor.Pins.Pool[context.HoveredPinIdx.Value()].Pos; } else { - editor.InteractionStack.pop_back(); + end_pos = context.MousePos; } + + const ImCubicBezier cb = CalcCubicBezier( + start_pos, end_pos, start_pin_type, GImNodes->Style.LinkLineSegmentsPerLength); + +#if IMGUI_VERSION_NUM < 18000 + GimNodes->CanvasDrawList->AddBezierCurve( +#else + GImNodes->CanvasDrawList->AddBezierCubic( +#endif + cb.P0, + cb.P1, + cb.P2, + cb.P3, + GImNodes->Style.Colors[ImNodesCol_Link], + GImNodes->Style.LinkThickness, + cb.NumSegments); } - break; - case ImNodesInteractionType_ImGuiItem: + + if (context.LeftMouseReleased) { - if (GImNodes->LeftMouseReleased) + if (link_snapped_to_pin) + { + context.Event.Type = ImNodesEventType_LinkCreatedOnMouseRelease; + context.Event.LinkCreated.StartPinId = start_pin_id; + context.Event.LinkCreated.EndPinId = editor.Pins.Pool[context.HoveredPinIdx.Value()].Id; + } + else { - editor.InteractionStack.pop_back(); + context.Event.Type = ImNodesEventType_LinkDropped; + context.Event.LinkDropped.StartPinId = start_pin_id; + context.Event.LinkDropped.IsFromDetach = state.PartialLink.CreatedFromDetach; } + + EnterPendingState(editor); + return; } - break; - default: - IM_ASSERT(!"Unreachable code!"); - break; + + if (create_link_on_snap) + { + context.Event.Type = ImNodesEventType_LinkCreatedOnSnap; + context.Event.LinkCreated.StartPinId = start_pin_id; + context.Event.LinkCreated.EndPinId = editor.Pins.Pool[context.HoveredPinIdx.Value()].Id; + EnterSnappedLinkState( + editor, start_pin_id, editor.Pins.Pool[context.HoveredPinIdx.Value()].Id); + return; } } -void ResolveOccludedPins(const ImNodesEditorContext& editor, ImVector& occluded_pin_indices) +void ImGuiStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) { - const ImVector& depth_stack = editor.NodeDepthOrder; + if (context.LeftMouseReleased) + { + EnterPendingState(editor); + } +} - occluded_pin_indices.resize(0); +void SnappedLinkStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) +{ + // Entering this state requires HoveredPinIdx to have a value. + + // Hovered pin detection - if (depth_stack.Size < 2) { - return; + ResolveOccludedPins(editor, context.OccludedPinIndices); + context.HoveredPinIdx = ResolveHoveredPin(editor.Pins, context.OccludedPinIndices); } - // For each node in the depth stack - for (int depth_idx = 0; depth_idx < (depth_stack.Size - 1); ++depth_idx) + const ImInteractionState& state = editor.InteractionState; + + const bool snapping_pin_changed = + !(context.HoveredPinIdx.HasValue() && + editor.Pins.Pool[context.HoveredPinIdx.Value()].Id == state.SnappedLink.SnappedPinId); + + if (snapping_pin_changed) { - const ImNodeData& node_below = editor.Nodes.Pool[depth_stack[depth_idx]]; + // Pre-condition: the user created a link after this state was entered + // FindDuplicateLink should return a valid link idx. + const ImOptionalIndex snapped_link = FindDuplicateLink( + context.Links, state.SnappedLink.StartPinId, state.SnappedLink.SnappedPinId); - // Iterate over the rest of the depth stack to find nodes overlapping the pins - for (int next_depth_idx = depth_idx + 1; next_depth_idx < depth_stack.Size; - ++next_depth_idx) - { - const ImRect& rect_above = editor.Nodes.Pool[depth_stack[next_depth_idx]].Rect; + context.Event.Type = ImNodesEventType_LinkDestroyed; + context.Event.LinkDestroyed.LinkIdx = snapped_link.Value(); - // Iterate over each pin - for (int idx = 0; idx < node_below.PinIndices.Size; ++idx) - { - const int pin_idx = node_below.PinIndices[idx]; - const ImVec2& pin_pos = editor.Pins.Pool[pin_idx].Pos; + EnterPartialLinkState(context, editor, state.SnappedLink.StartPinId, true); + return; + } - if (rect_above.Contains(pin_pos)) - { - occluded_pin_indices.push_back(pin_idx); - } - } - } + if (context.LeftMouseReleased) + { + EnterPendingState(editor); + return; } } -ImOptionalIndex ResolveHoveredPin( - const ImObjectPool& pins, - const ImVector& occluded_pin_indices) +void PendingStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) { - float smallest_distance = FLT_MAX; - ImOptionalIndex pin_idx_with_smallest_distance; + if (IsMiniMapHovered() || !MouseInCanvas(context)) + { + return; + } - const float hover_radius_sqr = GImNodes->Style.PinHoverRadius * GImNodes->Style.PinHoverRadius; + // Detect ImGui interaction first, because it blocks interaction with the rest of the UI. - for (int idx = 0; idx < pins.Pool.Size; ++idx) + if (context.LeftMouseClicked && (ImGui::IsAnyItemActive() || ImGui::IsAnyItemHovered())) { - if (!pins.InUse[idx]) + EnterImGuiState(editor); + return; + } + + ResolveOccludedPins(editor, context.OccludedPinIndices); + context.HoveredPinIdx = ResolveHoveredPin(editor.Pins, context.OccludedPinIndices); + + if (!context.HoveredPinIdx.HasValue()) + { + // Resolve which node is actually on top and being hovered using the depth stack. + context.HoveredNodeIdx = ResolveHoveredNode(editor.NodeDepthOrder); + } + + if (!(context.HoveredPinIdx.HasValue() || context.HoveredNodeIdx.HasValue())) + { + context.HoveredLinkIdx = ResolveHoveredLink(context.Links, context.Curves, editor.Pins); + } + + // This function should replicate BeginCanvasInteraction() + + if (context.LeftMouseClicked) + { + if (context.HoveredPinIdx.HasValue()) { - continue; + EnterPartialLinkState( + context, editor, editor.Pins.Pool[context.HoveredPinIdx.Value()].Id, false); + return; } - - if (occluded_pin_indices.contains(idx)) + else if (context.HoveredNodeIdx.HasValue()) { - continue; + EnterNodeState(context, editor, context.HoveredNodeIdx.Value()); + return; } + else if (context.HoveredLinkIdx.HasValue()) + { + const bool detach_modifier_pressed = + context.Io.LinkDetachWithModifierClick.Modifier == nullptr + ? false + : *context.Io.LinkDetachWithModifierClick.Modifier; - const ImVec2& pin_pos = pins.Pool[idx].Pos; - const float distance_sqr = ImLengthSqr(pin_pos - GImNodes->MousePos); + if (detach_modifier_pressed) + { + const int link_idx = context.HoveredLinkIdx.Value(); - // TODO: GImNodes->Style.PinHoverRadius needs to be copied into pin data and the pin-local - // value used here. This is no longer called in BeginAttribute/EndAttribute scope and the - // detected pin might have a different hover radius than what the user had when calling - // BeginAttribute/EndAttribute. - if (distance_sqr < hover_radius_sqr && distance_sqr < smallest_distance) + // Detach the link from the pin which is closest to the mouse cursor. + const ImLink& link = context.Links[link_idx]; + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPinId); + const float dist_to_start = ImLengthSqr(start_pin.Pos - context.MousePos); + const float dist_to_end = ImLengthSqr(end_pin.Pos - context.MousePos); + const int closest_pin_id = dist_to_start <= dist_to_end ? end_pin.Id : start_pin.Id; + + context.Event.Type = ImNodesEventType_LinkDestroyed; + context.Event.LinkDestroyed.LinkIdx = link_idx; + + EnterPartialLinkState(context, editor, closest_pin_id, true); + } + else + { + EnterLinkState(context, editor, context.HoveredLinkIdx.Value()); + } + return; + } + else if (context.AltMouseClicked) { - smallest_distance = distance_sqr; - pin_idx_with_smallest_distance = idx; + EnterPanningState(editor); + return; + } + else + { + EnterBoxSelectorState(editor); + return; } } - - return pin_idx_with_smallest_distance; } -ImOptionalIndex ResolveHoveredNode(const ImVector& depth_stack) +void BoxSelectorUpdateSelection( + ImNodesEditorContext& editor, + const ImVector& links, + const ImVector& curves, + ImRect box_rect) { - if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 0) + // Invert box selector coordinates as needed + + if (box_rect.Min.x > box_rect.Max.x) { - return ImOptionalIndex(); + ImSwap(box_rect.Min.x, box_rect.Max.x); } - if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 1) + if (box_rect.Min.y > box_rect.Max.y) { - return ImOptionalIndex(GImNodes->NodeIndicesOverlappingWithMouse[0]); + ImSwap(box_rect.Min.y, box_rect.Max.y); } - int largest_depth_idx = -1; - int node_idx_on_top = -1; + // Update node selection - for (int i = 0; i < GImNodes->NodeIndicesOverlappingWithMouse.size(); ++i) + editor.SelectedNodeIndices.clear(); + + // Test for overlap against node rectangles + + for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) { - const int node_idx = GImNodes->NodeIndicesOverlappingWithMouse[i]; - for (int depth_idx = 0; depth_idx < depth_stack.size(); ++depth_idx) + if (editor.Nodes.InUse[node_idx]) { - if (depth_stack[depth_idx] == node_idx && (depth_idx > largest_depth_idx)) + ImNodeData& node = editor.Nodes.Pool[node_idx]; + if (box_rect.Overlaps(node.Rect)) { - largest_depth_idx = depth_idx; - node_idx_on_top = node_idx; + editor.SelectedNodeIndices.push_back(node_idx); } } } - IM_ASSERT(node_idx_on_top != -1); - return ImOptionalIndex(node_idx_on_top); -} + // Update link selection + + editor.SelectedLinkIds.clear(); + + // Test for overlap against links -ImOptionalIndex ResolveAttachedLink( - const ImVector& links, - const ImObjectPool& pins, - const int hovered_pin_idx) -{ - const int pin_id = pins.Pool[hovered_pin_idx].Id; for (int idx = 0; idx < links.size(); ++idx) { - const ImLink& link = links[idx]; - if (pin_id == link.StartPinId || pin_id == link.EndPinId) + const ImLink& link = links[idx]; + const ImCubicBezier& cb = curves[idx]; + + if (RectangleOverlapsLink(box_rect, cb)) { - return ImOptionalIndex(idx); + editor.SelectedLinkIds.push_back(link.Id); } } +} - return ImOptionalIndex(); +void AutoPan(const ImNodesContext& context, ImNodesEditorContext& editor) +{ + if (MouseInCanvas(context)) + { + return; + } + + ImVec2 mouse = ImGui::GetMousePos(); + ImVec2 center = GImNodes->CanvasRectScreenSpace.GetCenter(); + ImVec2 direction = (center - mouse); + direction = direction * ImInvLength(direction, 0.0); + + editor.AutoPanningDelta = direction * ImGui::GetIO().DeltaTime * GImNodes->Io.AutoPanningSpeed; + editor.Panning += editor.AutoPanningDelta; } -ImOptionalIndex ResolveHoveredLink( - const ImVector& links, - const ImVector& curves, - const ImObjectPool& pins) +void BoxSelectorUpdate(ImNodesContext& context, ImNodesEditorContext& editor) { - float smallest_distance = FLT_MAX; - ImOptionalIndex link_idx_with_smallest_distance; + AutoPan(context, editor); - // There are two ways a link can be detected as "hovered". - // 1. The link is within hover distance to the mouse. The closest such link is selected as being - // hovered over. - // 2. If the link is connected to the currently hovered pin. - // - // The latter is a requirement for link detaching with drag click to work, as both a link and - // pin are required to be hovered over for the feature to work. + ImBoxSelector& box_selector = editor.InteractionState.BoxSelector; - const bool is_pin_hovered = GImNodes->HoveredPinIdx.HasValue(); - if (is_pin_hovered) - { - return ResolveAttachedLink(links, pins, GImNodes->HoveredPinIdx.Value()); - } + box_selector.GridSpaceRect.Max = ScreenSpaceToGridSpace(editor, context.MousePos); - for (int idx = 0; idx < curves.size(); ++idx) + ImRect box_rect = box_selector.GridSpaceRect; + box_rect.Min = GridSpaceToScreenSpace(editor, box_rect.Min); + box_rect.Max = GridSpaceToScreenSpace(editor, box_rect.Max); + + BoxSelectorUpdateSelection(editor, context.Links, context.Curves, box_rect); + + const ImU32 box_selector_color = context.Style.Colors[ImNodesCol_BoxSelector]; + const ImU32 box_selector_outline = context.Style.Colors[ImNodesCol_BoxSelectorOutline]; + context.CanvasDrawList->AddRectFilled(box_rect.Min, box_rect.Max, box_selector_color); + context.CanvasDrawList->AddRect(box_rect.Min, box_rect.Max, box_selector_outline); + + if (context.LeftMouseReleased) { - const ImCubicBezier& curve = curves[idx]; + ImVector& depth_stack = editor.NodeDepthOrder; + const ImVector& selected_idxs = editor.SelectedNodeIndices; - // The distance test - const ImRect curve_bounds = GetContainingRectForCubicBezier(curve); + // Bump the selected node indices, in order, to the top of the depth stack. + // NOTE: this algorithm has worst case time complexity of O(N^2), if the node + // selection is ~ N (due to selected_idxs.contains()). - if (curve_bounds.Contains(GImNodes->MousePos)) + if ((selected_idxs.Size > 0) && (selected_idxs.Size < depth_stack.Size)) { - const float distance = GetDistanceToCubicBezier(GImNodes->MousePos, curve); - - if (distance < GImNodes->Style.LinkHoverDistance && distance < smallest_distance) + int num_moved = 0; // The number of indices moved. Stop after selected_idxs.Size + for (int i = 0; i < depth_stack.Size - selected_idxs.Size; ++i) { - smallest_distance = distance; - link_idx_with_smallest_distance = idx; + for (int node_idx = depth_stack[i]; selected_idxs.contains(node_idx); + node_idx = depth_stack[i]) + { + depth_stack.erase(depth_stack.begin() + static_cast(i)); + depth_stack.push_back(node_idx); + ++num_moved; + } + + if (num_moved == selected_idxs.Size) + { + break; + } } } + + EnterPendingState(editor); + return; + } +} + +void InteractionStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) +{ + switch (editor.InteractionState.Type) + { + case ImNodesInteractionType_Pending: + PendingStateUpdate(context, editor); + break; + case ImNodesInteractionType_BoxSelector: + BoxSelectorUpdate(context, editor); + break; + case ImNodesInteractionType_ImGui: + { + ImGuiStateUpdate(context, editor); + } + break; + case ImNodesInteractionType_Link: + { + if (context.LeftMouseReleased) + { + EnterPendingState(editor); + } } + break; + case ImNodesInteractionType_Node: + { + AutoPan(context, editor); - return link_idx_with_smallest_distance; + TranslateSelectedNodes(editor); + + if (context.LeftMouseReleased) + { + EnterPendingState(editor); + } + } + break; + case ImNodesInteractionType_Panning: + { + if (context.AltMouseDragging) + { + editor.Panning += ImGui::GetIO().MouseDelta; + } + else + { + EnterPendingState(editor); + } + } + break; + case ImNodesInteractionType_PartialLink: + PartialLinkStateUpdate(context, editor); + break; + case ImNodesInteractionType_SnappedLink: + SnappedLinkStateUpdate(context, editor); + break; + default: + break; + } } // [SECTION] render helpers @@ -1600,53 +1698,6 @@ void DrawLinks( #endif curve.P0, curve.P1, curve.P2, curve.P3, link_color, link_thickness, curve.NumSegments); } - - if (std::find_if( - editor.InteractionStack.begin(), - editor.InteractionStack.end(), - [](const ImInteractionState& state) -> bool { - return (state.Type == ImNodesInteractionType_PartialLink) || - (state.Type == ImNodesInteractionType_SnappedLink); - }) != editor.InteractionStack.end()) - { - const ImInteractionState& state = editor.InteractionStack.back(); - - ImVec2 start_pos, end_pos; - ImNodesAttributeType start_pin_type; - - if (state.Type == ImNodesInteractionType_PartialLink) - { - const ImPinData& start_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); - start_pos = start_pin.Pos; - start_pin_type = start_pin.Type; - end_pos = GImNodes->MousePos; - } - else - { - const ImPinData& start_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId); - start_pos = start_pin.Pos; - start_pin_type = start_pin.Type; - end_pos = ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId).Pos; - } - - const ImCubicBezier cb = CalcCubicBezier( - start_pos, end_pos, start_pin_type, GImNodes->Style.LinkLineSegmentsPerLength); - -#if IMGUI_VERSION_NUM < 18000 - GimNodes->CanvasDrawList->AddBezierCurve( -#else - GImNodes->CanvasDrawList->AddBezierCubic( -#endif - cb.P0, - cb.P1, - cb.P2, - cb.P3, - GImNodes->Style.Colors[ImNodesCol_Link], - GImNodes->Style.LinkThickness, - cb.NumSegments); - } } void BeginPinAttribute( @@ -1753,7 +1804,7 @@ static inline void CalcMiniMapLayout() const ImVec2 grid_content_size = editor.GridContentBounds.IsInverted() ? max_size : ImFloor(editor.GridContentBounds.GetSize()); - const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; + const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; mini_map_size = ImFloor( grid_content_aspect_ratio > max_size_aspect_ratio ? ImVec2(max_size.x, max_size.x / grid_content_aspect_ratio) @@ -1810,7 +1861,9 @@ static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx) ImU32 mini_map_node_background; - if (editor.InteractionStack.size() == 0 && + // TODO: it should not be possible to interact with the minimap if an interaction is already in + // progress + if (editor.InteractionState.Type == ImNodesInteractionType_Pending && ImGui::IsMouseHoveringRect(node_rect.Min, node_rect.Max)) { mini_map_node_background = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; @@ -1854,9 +1907,9 @@ void MiniMapDrawLinks( const ImLink& link = links[idx]; const ImCubicBezier& curve = curves[idx]; - const int color_idx = editor.SelectedLinkIds.contains(link.Id) - ? ImNodesCol_MiniMapLinkSelected - : ImNodesCol_MiniMapLink; + const int color_idx = editor.SelectedLinkIds.contains(link.Id) + ? ImNodesCol_MiniMapLinkSelected + : ImNodesCol_MiniMapLink; const ImU32 link_color = GImNodes->Style.Colors[color_idx]; #if IMGUI_VERSION_NUM < 18000 @@ -1935,8 +1988,10 @@ static void MiniMapUpdate() ImGui::EndChild(); + // TODO: it should not be possible to interact with the minimap if an interaction is already in + // progress bool center_on_click = mini_map_is_hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left) && - editor.InteractionStack.size() == 0 && + editor.InteractionState.Type == ImNodesInteractionType_Pending && !GImNodes->NodeIdxSubmissionOrder.empty(); if (center_on_click) { @@ -2034,9 +2089,9 @@ void SetCurrentContext(ImNodesContext* ctx) { GImNodes = ctx; } ImNodesEditorContext* EditorContextCreate() { - void* mem = ImGui::MemAlloc(sizeof(ImNodesEditorContext)); - new (mem) ImNodesEditorContext(); - return (ImNodesEditorContext*)mem; + void* mem = ImGui::MemAlloc(sizeof(ImNodesEditorContext)); + ImNodesEditorContext* editor_ctx = new (mem) ImNodesEditorContext(); + return editor_ctx; } void EditorContextFree(ImNodesEditorContext* ctx) @@ -2213,12 +2268,10 @@ void BeginNodeEditor() GImNodes->HoveredNodeIdx.Reset(); GImNodes->HoveredLinkIdx.Reset(); GImNodes->HoveredPinIdx.Reset(); - GImNodes->DeletedLinkIdx.Reset(); - GImNodes->SnapLinkIdx.Reset(); GImNodes->NodeIndicesOverlappingWithMouse.clear(); - GImNodes->ImNodesUIState = ImNodesUIState_None; + GImNodes->Event.Type = ImNodesEventType_None; GImNodes->MousePos = ImGui::GetIO().MousePos; GImNodes->LeftMouseClicked = ImGui::IsMouseClicked(0); @@ -2288,54 +2341,15 @@ void EndNodeEditor() editor.GridContentBounds = ScreenSpaceToGridSpace(editor, GImNodes->CanvasRectScreenSpace); } - // Detect ImGui interaction first, because it blocks interaction with the rest of the UI - - if (GImNodes->LeftMouseClicked && ImGui::IsAnyItemActive()) - { - PushImGuiState(editor.InteractionStack); - } - - // Detect which UI element is being hovered over. Detection is done in a hierarchical fashion, - // because a UI element being hovered excludes any other as being hovered over. - - // Don't do hovering detection for nodes/links/pins when interacting with the mini-map, since - // its an *overlay* with its own interaction behavior and must have precedence during mouse - // interaction. - - if ((editor.InteractionStack.size() == 0 || std::find_if( - editor.InteractionStack.begin(), - editor.InteractionStack.end(), - [](const ImInteractionState& state) -> bool { - return state.Type == - ImNodesInteractionType_PartialLink; - }) != editor.InteractionStack.end() && - MouseInCanvas() && !IsMiniMapHovered())) { - // Pins needs some special care. We need to check the depth stack to see which pins are - // being occluded by other nodes. - ResolveOccludedPins(editor, GImNodes->OccludedPinIndices); - - GImNodes->HoveredPinIdx = ResolveHoveredPin(editor.Pins, GImNodes->OccludedPinIndices); + // Render the click interaction UI elements (partial links, box selector) on top of + // everything + // else. - if (!GImNodes->HoveredPinIdx.HasValue() && - std::find_if( - editor.InteractionStack.begin(), - editor.InteractionStack.end(), - [](const ImInteractionState& state) -> bool { - return state.Type == ImNodesInteractionType_BoxSelector; - }) != editor.InteractionStack.end()) - { - // Resolve which node is actually on top and being hovered using the depth stack. - GImNodes->HoveredNodeIdx = ResolveHoveredNode(editor.NodeDepthOrder); - } + DrawListAppendClickInteractionChannel(); + DrawListActivateClickInteractionChannel(); - // We don't check for hovered pins here, because if we want to detach a link by clicking and - // dragging, we need to have both a link and pin hovered. - if (!GImNodes->HoveredNodeIdx.HasValue()) - { - GImNodes->HoveredLinkIdx = - ResolveHoveredLink(GImNodes->Links, GImNodes->Curves, editor.Pins); - } + InteractionStateUpdate(*GImNodes, editor); } for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) @@ -2353,74 +2367,15 @@ void EndNodeEditor() DrawLinks(editor, GImNodes->Links, GImNodes->Curves); - // Render the click interaction UI elements (partial links, box selector) on top of everything - // else. - - DrawListAppendClickInteractionChannel(); - DrawListActivateClickInteractionChannel(); - if (IsMiniMapActive()) { CalcMiniMapLayout(); MiniMapUpdate(); } - // Handle node graph interaction - - if (!IsMiniMapHovered() && editor.InteractionStack.size() == 0) - { - if (GImNodes->LeftMouseClicked && GImNodes->HoveredLinkIdx.HasValue()) - { - BeginLinkInteraction( - editor, GImNodes->Links, GImNodes->HoveredLinkIdx.Value(), GImNodes->HoveredPinIdx); - } - - else if (GImNodes->LeftMouseClicked && GImNodes->HoveredPinIdx.HasValue()) - { - BeginLinkCreation(editor, GImNodes->HoveredPinIdx.Value()); - } - - else if (GImNodes->LeftMouseClicked && GImNodes->HoveredNodeIdx.HasValue()) - { - BeginNodeSelection(editor, GImNodes->HoveredNodeIdx.Value()); - } - else if ( - GImNodes->LeftMouseClicked || GImNodes->LeftMouseReleased || - GImNodes->AltMouseClicked || GImNodes->AltMouseScrollDelta != 0.f) - { - BeginCanvasInteraction(editor); - } - - const bool should_auto_pan = - std::find_if( - editor.InteractionStack.begin(), - editor.InteractionStack.end(), - [](const ImInteractionState& state) -> bool { - return state.Type == ImNodesInteractionType_BoxSelector || - ImNodesInteractionType_PartialLink || ImNodesInteractionType_Node; - }) != editor.InteractionStack.end(); - - if (should_auto_pan && !MouseInCanvas()) - { - ImVec2 mouse = ImGui::GetMousePos(); - ImVec2 center = GImNodes->CanvasRectScreenSpace.GetCenter(); - ImVec2 direction = (center - mouse); - direction = direction * ImInvLength(direction, 0.0); - - editor.AutoPanningDelta = - direction * ImGui::GetIO().DeltaTime * GImNodes->Io.AutoPanningSpeed; - editor.Panning += editor.AutoPanningDelta; - } - } - - if (editor.InteractionStack.size() > 0) - { - InteractionStackUpdate(editor, GImNodes->Links, GImNodes->Curves); - } - - // At this point, draw commands have been issued for all nodes (and pins). Update the node pool - // to detect unused node slots and remove those indices from the depth stack before sorting the - // node draw commands by depth. + // At this point, draw commands have been issued for all nodes (and pins). Update the node + // pool to detect unused node slots and remove those indices from the depth stack before + // sorting the node draw commands by depth. ObjectPoolUpdate(editor.Nodes); ObjectPoolUpdate(editor.Pins); @@ -2615,27 +2570,9 @@ void Link(const int id, const int start_attr_id, const int end_attr_id) { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); - const int link_idx = GImNodes->Links.size(); + // const int link_idx = GImNodes->Links.size(); const ImLink link(id, start_attr_id, end_attr_id, GImNodes->Style.Colors); GImNodes->Links.push_back(link); - - ImNodesEditorContext& editor = EditorContextGet(); - - // Check if this link was created by the current link event - - if (editor.InteractionStack.size() > 0) - { - const ImInteractionState& state = editor.InteractionStack.back(); - - if (state.Type == ImNodesInteractionType_SnappedLink && - (state.SnappedLink.StartPinId == start_attr_id && - state.SnappedLink.SnappedPinId == end_attr_id) || - (state.SnappedLink.StartPinId == end_attr_id && - state.SnappedLink.SnappedPinId == start_attr_id)) - { - GImNodes->SnapLinkIdx = link_idx; - } - } } void PushColorStyle(const ImNodesCol item, unsigned int color) @@ -2810,7 +2747,7 @@ void SnapNodeToGrid(int node_id) node.Origin = SnapOriginToGrid(node.Origin); } -bool IsEditorHovered() { return MouseInCanvas(); } +bool IsEditorHovered() { return MouseInCanvas(*GImNodes); } bool IsNodeHovered(int* const node_id) { @@ -2973,13 +2910,10 @@ bool IsLinkStarted(int* const started_at_id) IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); IM_ASSERT(started_at_id != NULL); - const bool is_started = (GImNodes->ImNodesUIState & ImNodesUIState_LinkStarted) != 0; + const bool is_started = GImNodes->Event.Type == ImNodesEventType_LinkStarted; if (is_started) { - const ImNodesEditorContext& editor = EditorContextGet(); - IM_ASSERT(editor.InteractionStack.size() > 0); - IM_ASSERT(editor.InteractionStack.back().Type == ImNodesInteractionType_PartialLink); - *started_at_id = editor.InteractionStack.back().PartialLink.StartPinId; + *started_at_id = GImNodes->Event.LinkStarted.StartPinId; } return is_started; @@ -2990,16 +2924,13 @@ bool IsLinkDropped(int* const started_at_id, const bool including_detached_links // Call this function after EndNodeEditor()! IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); - const ImNodesEditorContext& editor = EditorContextGet(); - const ImInteractionState& state = editor.InteractionStack.back(); - - IM_ASSERT(state.Type == ImNodesInteractionType_PartialLink); - const bool link_dropped = (GImNodes->ImNodesUIState & ImNodesUIState_LinkDropped) != 0 && - (including_detached_links || !state.PartialLink.CreatedFromSnap); + const bool link_dropped = + (GImNodes->Event.Type == ImNodesEventType_LinkDropped) && + (including_detached_links || !GImNodes->Event.LinkDropped.IsFromDetach); if (link_dropped && started_at_id) { - *started_at_id = state.PartialLink.StartPinId; + *started_at_id = GImNodes->Event.LinkDropped.StartPinId; } return link_dropped; @@ -3014,34 +2945,22 @@ bool IsLinkCreated( IM_ASSERT(started_at_pin_id != NULL); IM_ASSERT(ended_at_pin_id != NULL); - const bool is_created = (GImNodes->ImNodesUIState & ImNodesUIState_LinkCreated) != 0; + const ImNodesEvent& event = GImNodes->Event; + + const bool is_created = (event.Type == ImNodesEventType_LinkCreatedOnMouseRelease) || + (event.Type == ImNodesEventType_LinkCreatedOnSnap); if (is_created) { ImNodesEditorContext& editor = EditorContextGet(); // TODO: const - IM_ASSERT(editor.InteractionStack.size() > 0); - const ImInteractionState& state = editor.InteractionStack.back(); - IM_ASSERT( - state.Type == ImNodesInteractionType_PartialLink || - state.Type == ImNodesInteractionType_SnappedLink); - if (state.Type == ImNodesInteractionType_PartialLink) - { - *started_at_pin_id = - ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId).Id; - *ended_at_pin_id = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()].Id; - } - else - { - *started_at_pin_id = - ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.StartPinId).Id; - *ended_at_pin_id = - ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId).Id; - } + *started_at_pin_id = + ObjectPoolFindOrCreateObject(editor.Pins, event.LinkCreated.StartPinId).Id; + *ended_at_pin_id = ObjectPoolFindOrCreateObject(editor.Pins, event.LinkCreated.EndPinId).Id; if (created_from_snap) { - *created_from_snap = (state.Type == ImNodesInteractionType_SnappedLink); + *created_from_snap = (event.Type == ImNodesEventType_LinkCreatedOnSnap); } } @@ -3061,42 +2980,27 @@ bool IsLinkCreated( IM_ASSERT(ended_at_node_id != NULL); IM_ASSERT(ended_at_pin_id != NULL); - const bool is_created = (GImNodes->ImNodesUIState & ImNodesUIState_LinkCreated) != 0; + const ImNodesEvent& event = GImNodes->Event; + + const bool is_created = (event.Type == ImNodesEventType_LinkCreatedOnMouseRelease) || + (event.Type == ImNodesEventType_LinkCreatedOnSnap); if (is_created) { ImNodesEditorContext& editor = EditorContextGet(); // TODO: const - IM_ASSERT(editor.InteractionStack.size() > 0); - const ImInteractionState& state = editor.InteractionStack.back(); - IM_ASSERT( - state.Type == ImNodesInteractionType_PartialLink || - state.Type == ImNodesInteractionType_SnappedLink); - if (state.Type == ImNodesInteractionType_PartialLink) - { - const ImPinData& start_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); - const ImPinData& hovered_pin = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]; - *started_at_node_id = editor.Nodes.Pool[start_pin.ParentNodeIdx].Id; - *started_at_pin_id = start_pin.Id; - *ended_at_node_id = editor.Nodes.Pool[hovered_pin.ParentNodeIdx].Id; - *ended_at_pin_id = hovered_pin.Id; - } - else - { - const ImPinData& start_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.StartPinId); - const ImPinData& snapped_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.SnappedLink.SnappedPinId); - *started_at_node_id = editor.Nodes.Pool[start_pin.ParentNodeIdx].Id; - *started_at_pin_id = start_pin.Id; - *ended_at_node_id = editor.Nodes.Pool[snapped_pin.ParentNodeIdx].Id; - *ended_at_pin_id = snapped_pin.Id; - } + const ImPinData& start_pin = + ObjectPoolFindOrCreateObject(editor.Pins, event.LinkCreated.StartPinId); + const ImPinData& end_pin = + ObjectPoolFindOrCreateObject(editor.Pins, event.LinkCreated.EndPinId); + *started_at_node_id = editor.Nodes.Pool[start_pin.ParentNodeIdx].Id; + *started_at_pin_id = start_pin.Id; + *ended_at_node_id = editor.Nodes.Pool[end_pin.ParentNodeIdx].Id; + *ended_at_pin_id = end_pin.Id; if (created_from_snap) { - *created_from_snap = (state.Type == ImNodesInteractionType_SnappedLink); + *created_from_snap = (event.Type == ImNodesEventType_LinkCreatedOnSnap); } } @@ -3107,11 +3011,12 @@ bool IsLinkDestroyed(int* const link_id) { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); - const bool link_destroyed = GImNodes->DeletedLinkIdx.HasValue(); + const ImNodesEvent& event = GImNodes->Event; + const bool link_destroyed = event.Type == ImNodesEventType_LinkDestroyed; + if (link_destroyed) { - const int link_idx = GImNodes->DeletedLinkIdx.Value(); - *link_id = GImNodes->Links[link_idx].Id; + *link_id = GImNodes->Links[event.LinkDestroyed.LinkIdx].Id; } return link_destroyed; diff --git a/imnodes.h b/imnodes.h index 6368893..1217601 100644 --- a/imnodes.h +++ b/imnodes.h @@ -95,10 +95,6 @@ enum ImNodesPinShape_ enum ImNodesAttributeFlags_ { ImNodesAttributeFlags_None = 0, - // Allow detaching a link by left-clicking and dragging the link at a pin it is connected to. - // NOTE: the user has to actually delete the link for this to work. A deleted link can be - // detected by calling IsLinkDestroyed() after EndNodeEditor(). - ImNodesAttributeFlags_EnableLinkDetachWithDragClick = 1 << 0, // Visual snapping of an in progress link will trigger IsLink Created/Destroyed events. Allows // for previewing the creation of a link while dragging it across attributes. See here for demo: // https://github.com/Nelarius/imnodes/issues/41#issuecomment-647132113 NOTE: the user has to diff --git a/imnodes_internal.h b/imnodes_internal.h index a202249..3c4c59b 100644 --- a/imnodes_internal.h +++ b/imnodes_internal.h @@ -23,10 +23,9 @@ extern ImNodesContext* GImNodes; typedef int ImNodesScope; typedef int ImNodesAttributeType; -typedef int ImNodesUIState; typedef int ImNodesClickInteractionType; -typedef int ImNodesLinkCreationType; typedef int ImNodesInteractionType; +typedef int ImNodesEventType; enum ImNodesScope_ { @@ -43,20 +42,6 @@ enum ImNodesAttributeType_ ImNodesAttributeType_Output }; -enum ImNodesUIState_ -{ - ImNodesUIState_None = 0, - ImNodesUIState_LinkStarted = 1 << 0, - ImNodesUIState_LinkDropped = 1 << 1, - ImNodesUIState_LinkCreated = 1 << 2 -}; - -enum ImNodesLinkCreationType_ -{ - ImNodesLinkCreationType_Standard, - ImNodesLinkCreationType_FromDetach -}; - // [SECTION] internal data structures // The object T must have the following interface: @@ -203,11 +188,10 @@ struct ImBoxSelector ImRect GridSpaceRect; }; -// A link which is connected to the mouse cursor at the other end struct ImPartialLink { int StartPinId; - bool CreatedFromSnap; + bool CreatedFromDetach; }; struct ImSnappedLink @@ -218,13 +202,14 @@ struct ImSnappedLink enum ImNodesInteractionType_ { + ImNodesInteractionType_Pending, ImNodesInteractionType_BoxSelector, - ImNodesInteractionType_Panning, + ImNodesInteractionType_ImGui, ImNodesInteractionType_Link, + ImNodesInteractionType_Node, + ImNodesInteractionType_Panning, ImNodesInteractionType_PartialLink, ImNodesInteractionType_SnappedLink, - ImNodesInteractionType_Node, - ImNodesInteractionType_ImGuiItem, ImNodesInteractionType_None }; @@ -239,7 +224,53 @@ struct ImInteractionState ImSnappedLink SnappedLink; }; - ImInteractionState(const ImNodesInteractionType type) : Type(type) {} + ImInteractionState() : Type(ImNodesInteractionType_Pending) {} +}; + +enum ImNodesEventType_ +{ + ImNodesEventType_LinkStarted, + ImNodesEventType_LinkCreatedOnMouseRelease, + ImNodesEventType_LinkCreatedOnSnap, + ImNodesEventType_LinkDropped, + ImNodesEventType_LinkDestroyed, + ImNodesEventType_None, +}; + +struct ImLinkStarted +{ + int StartPinId; +}; + +struct ImLinkCreated +{ + int StartPinId, EndPinId; +}; + +struct ImLinkDropped +{ + int StartPinId; + bool IsFromDetach; +}; + +struct ImLinkDestroyed +{ + int LinkIdx; +}; + +struct ImNodesEvent +{ + ImNodesEventType Type; + + union + { + ImLinkStarted LinkStarted; + ImLinkCreated LinkCreated; + ImLinkDestroyed LinkDestroyed; + ImLinkDropped LinkDropped; + }; + + ImNodesEvent() : Type(ImNodesEventType_None) {} }; struct ImNodesColElement @@ -291,7 +322,7 @@ struct ImNodesEditorContext // Offset of the primary node origin relative to the mouse cursor. ImVec2 PrimaryNodeOffset; - ImVector InteractionStack; + ImInteractionState InteractionState; // Mini-map state set by MiniMap() @@ -309,7 +340,7 @@ struct ImNodesEditorContext ImNodesEditorContext() : Nodes(), Pins(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIds(), - SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), InteractionStack(), + SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), InteractionState(), MiniMapEnabled(false), MiniMapSizeFraction(0.0f), MiniMapNodeHoveringCallback(NULL), MiniMapNodeHoveringCallbackUserData(NULL), MiniMapScaling(0.0f) { @@ -361,13 +392,7 @@ struct ImNodesContext ImOptionalIndex HoveredLinkIdx; ImOptionalIndex HoveredPinIdx; - ImOptionalIndex DeletedLinkIdx; - ImOptionalIndex SnapLinkIdx; - - // Event helper state - // TODO: this should be a part of a state machine, and not a member of the global struct. - // Unclear what parts of the code this relates to. - int ImNodesUIState; + ImNodesEvent Event; int ActiveAttributeId; bool ActiveAttribute; From 1b5841b993c71aec9c6b5299da9c2512938c9c03 Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Thu, 26 May 2022 21:41:23 +0300 Subject: [PATCH 6/9] Fix multi-link select --- imnodes.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imnodes.cpp b/imnodes.cpp index 04f79c5..bd27a8d 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -1172,7 +1172,7 @@ void PendingStateUpdate(ImNodesContext& context, ImNodesEditorContext& editor) } else { - EnterLinkState(context, editor, context.HoveredLinkIdx.Value()); + EnterLinkState(context, editor, context.Links[context.HoveredLinkIdx.Value()].Id); } return; } From 1c3ad55a8eb2d53de7503bfeeaaba3128919cedb Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Fri, 27 May 2022 08:21:22 +0300 Subject: [PATCH 7/9] Fix shadowed variable --- imnodes.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index bd27a8d..9a55691 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -993,8 +993,6 @@ void PartialLinkStateUpdate(ImNodesContext& context, ImNodesEditorContext& edito // Render the bezier curve { - const ImPinData& start_pin = - ObjectPoolFindOrCreateObject(editor.Pins, state.PartialLink.StartPinId); const ImVec2 start_pos = start_pin.Pos; const ImNodesAttributeType start_pin_type = start_pin.Type; From d4d7459b212695ec5c94141d378e82fbc667ed33 Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Sat, 28 May 2022 10:27:16 +0300 Subject: [PATCH 8/9] Code cleanup - reset unneeded changes in EditorContextCreate() - remove unused include --- imnodes.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index 9a55691..28fa8c9 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -18,7 +18,6 @@ #error "Minimum ImGui version requirement not met -- please use a newer version!" #endif -#include #include #include #include @@ -2087,9 +2086,9 @@ void SetCurrentContext(ImNodesContext* ctx) { GImNodes = ctx; } ImNodesEditorContext* EditorContextCreate() { - void* mem = ImGui::MemAlloc(sizeof(ImNodesEditorContext)); - ImNodesEditorContext* editor_ctx = new (mem) ImNodesEditorContext(); - return editor_ctx; + void* mem = ImGui::MemAlloc(sizeof(ImNodesEditorContext)); + new (mem) ImNodesEditorContext(); + return (ImNodesEditorContext*)mem; } void EditorContextFree(ImNodesEditorContext* ctx) @@ -2568,7 +2567,6 @@ void Link(const int id, const int start_attr_id, const int end_attr_id) { IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); - // const int link_idx = GImNodes->Links.size(); const ImLink link(id, start_attr_id, end_attr_id, GImNodes->Style.Colors); GImNodes->Links.push_back(link); } From fd04987b0f0d9f19191188b6912722a6eeec56ef Mon Sep 17 00:00:00 2001 From: Johann Muszynski Date: Mon, 30 May 2022 20:58:05 +0300 Subject: [PATCH 9/9] MiniMap fixes - render minimap on top of other node editor elements - solve the wiggling minimap link issue --- imnodes.cpp | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/imnodes.cpp b/imnodes.cpp index 28fa8c9..ffc438f 100644 --- a/imnodes.cpp +++ b/imnodes.cpp @@ -1889,20 +1889,23 @@ static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx) node_rect.Min, node_rect.Max, mini_map_node_outline, mini_map_node_rounding); } -void MiniMapDrawLinks( - const ImNodesEditorContext& editor, - const ImVector& links, - const ImVector& curves) +void MiniMapDrawLinks(ImNodesEditorContext& editor, const ImVector& links) { - const int num_links = links.size(); - IM_ASSERT(num_links == curves.size()); - const float link_thickness = GImNodes->Style.LinkThickness * editor.MiniMapScaling; - for (int idx = 0; idx < num_links; ++idx) + for (const ImLink& link : links) { - const ImLink& link = links[idx]; - const ImCubicBezier& curve = curves[idx]; + const ImPinData& start_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.StartPinId); + const ImPinData& end_pin = ObjectPoolFindOrCreateObject(editor.Pins, link.EndPinId); + + // TODO: this should use the pre-calculated curves, but pins are updated in the draw code + // after we generate the links. Using pre-calculated curves causes a strong link wiggling + // effect, as the link's position drags the nodes' position by one frame. + const ImCubicBezier cubic_bezier = CalcCubicBezier( + ScreenSpaceToMiniMapSpace(editor, start_pin.Pos), + ScreenSpaceToMiniMapSpace(editor, end_pin.Pos), + start_pin.Type, + GImNodes->Style.LinkLineSegmentsPerLength / editor.MiniMapScaling); const int color_idx = editor.SelectedLinkIds.contains(link.Id) ? ImNodesCol_MiniMapLinkSelected @@ -1914,13 +1917,13 @@ void MiniMapDrawLinks( #else GImNodes->CanvasDrawList->AddBezierCubic( #endif - ScreenSpaceToMiniMapSpace(editor, curve.P0), - ScreenSpaceToMiniMapSpace(editor, curve.P1), - ScreenSpaceToMiniMapSpace(editor, curve.P2), - ScreenSpaceToMiniMapSpace(editor, curve.P3), + cubic_bezier.P0, + cubic_bezier.P1, + cubic_bezier.P2, + cubic_bezier.P3, link_color, link_thickness, - curve.NumSegments); + cubic_bezier.NumSegments); } } @@ -1958,7 +1961,7 @@ static void MiniMapUpdate() mini_map_rect.Min, mini_map_rect.Max, true /* intersect with editor clip-rect */); // Draw links first so they appear under nodes, and we can use the same draw channel - MiniMapDrawLinks(editor, GImNodes->Links, GImNodes->Curves); + MiniMapDrawLinks(editor, GImNodes->Links); for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) { @@ -1985,8 +1988,8 @@ static void MiniMapUpdate() ImGui::EndChild(); - // TODO: it should not be possible to interact with the minimap if an interaction is already in - // progress + // TODO: it should not be possible to interact with the minimap if an interaction is already + // in progress bool center_on_click = mini_map_is_hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left) && editor.InteractionState.Type == ImNodesInteractionType_Pending && !GImNodes->NodeIdxSubmissionOrder.empty(); @@ -2364,12 +2367,6 @@ void EndNodeEditor() DrawLinks(editor, GImNodes->Links, GImNodes->Curves); - if (IsMiniMapActive()) - { - CalcMiniMapLayout(); - MiniMapUpdate(); - } - // At this point, draw commands have been issued for all nodes (and pins). Update the node // pool to detect unused node slots and remove those indices from the depth stack before // sorting the node draw commands by depth. @@ -2381,6 +2378,12 @@ void EndNodeEditor() // Finally, merge the draw channels GImNodes->CanvasDrawList->ChannelsMerge(); + if (IsMiniMapActive()) + { + CalcMiniMapLayout(); + MiniMapUpdate(); + } + // pop style ImGui::EndChild(); // end scrolling region ImGui::PopStyleColor(); // pop child window background color