313 lines
9.9 KiB
C++
313 lines
9.9 KiB
C++
// NodeEx.cpp - Extended Node Builder for Precise Pin Placement
|
|
// Implementation of PinEx function
|
|
|
|
# define IMGUI_DEFINE_MATH_OPERATORS
|
|
# include "NodeEx.h"
|
|
# include <vector>
|
|
# include <map>
|
|
# include <cstring>
|
|
|
|
namespace ax {
|
|
namespace NodeEditor {
|
|
|
|
// Internal storage for pin tooltips
|
|
struct PinTooltipData
|
|
{
|
|
PinId pinId;
|
|
ImRect bounds;
|
|
const char* tooltip;
|
|
};
|
|
|
|
static std::vector<PinTooltipData> s_PinTooltips;
|
|
|
|
// Helper function to get colors based on pin kind and state
|
|
void GetPinColors(PinKind kind, PinState state, ImU32& fillColor, ImU32& borderColor)
|
|
{
|
|
borderColor = IM_COL32(0, 0, 0, 0); // No border
|
|
|
|
switch (state)
|
|
{
|
|
case PinState::Normal:
|
|
fillColor = kind == PinKind::Input
|
|
? IM_COL32(120, 120, 150, 255) // Muted blue for inputs
|
|
: IM_COL32(150, 120, 120, 255); // Muted red for outputs
|
|
break;
|
|
|
|
case PinState::Running:
|
|
fillColor = IM_COL32(80, 160, 80, 255); // Muted green for running
|
|
break;
|
|
|
|
case PinState::Deactivated:
|
|
fillColor = IM_COL32(80, 80, 80, 200); // Dark gray for deactivated
|
|
break;
|
|
|
|
case PinState::Error:
|
|
fillColor = IM_COL32(200, 80, 80, 255); // Muted red for error
|
|
break;
|
|
|
|
case PinState::Warning:
|
|
fillColor = IM_COL32(200, 160, 80, 255); // Muted orange for warning
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Default renderer: Small rectangle for parameter pins
|
|
void RenderPinCircle(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize, ImU32 fillColor, ImU32 borderColor, PinState state)
|
|
{
|
|
// Small rectangle - about half the height of flow pins
|
|
float width = 8.0f;
|
|
float height = 4.0f;
|
|
ImVec2 halfSize(width * 0.5f, height * 0.5f);
|
|
ImVec2 boxMin = center - halfSize;
|
|
ImVec2 boxMax = center + halfSize;
|
|
|
|
// Simple filled rectangle, no border
|
|
drawList->AddRectFilled(boxMin, boxMax, fillColor, 0.0f);
|
|
}
|
|
|
|
// Default renderer: Small square for flow pins
|
|
void RenderPinBox(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize, ImU32 fillColor, ImU32 borderColor, PinState state)
|
|
{
|
|
// Small square - 8x8 pixels
|
|
float boxSize = 8.0f;
|
|
ImVec2 boxHalfSize(boxSize * 0.5f, boxSize * 0.5f);
|
|
ImVec2 boxMin = center - boxHalfSize;
|
|
ImVec2 boxMax = center + boxHalfSize;
|
|
|
|
// Simple filled square, no border, no rounding
|
|
drawList->AddRectFilled(boxMin, boxMax, fillColor, 0.0f);
|
|
}
|
|
|
|
ImRect PinEx(
|
|
PinId pinId,
|
|
PinKind kind,
|
|
PinEdge edge,
|
|
float offset,
|
|
float direction,
|
|
const ImRect& nodeRect,
|
|
PinState state,
|
|
const char* tooltip,
|
|
PinRenderer renderer)
|
|
{
|
|
// Default pin size
|
|
const ImVec2 pinSize(16.0f, 16.0f);
|
|
ImVec2 halfSize = pinSize * 0.5f;
|
|
|
|
// Calculate pin center position based on edge
|
|
ImVec2 pinCenter;
|
|
|
|
switch (edge)
|
|
{
|
|
case PinEdge::Top:
|
|
{
|
|
float x = ImLerp(nodeRect.Min.x, nodeRect.Max.x, offset);
|
|
float y = nodeRect.Min.y - direction;
|
|
pinCenter = ImVec2(x, y - halfSize.y);
|
|
break;
|
|
}
|
|
|
|
case PinEdge::Bottom:
|
|
{
|
|
float x = ImLerp(nodeRect.Min.x, nodeRect.Max.x, offset);
|
|
float y = nodeRect.Max.y + direction;
|
|
pinCenter = ImVec2(x, y + halfSize.y);
|
|
break;
|
|
}
|
|
|
|
case PinEdge::Left:
|
|
{
|
|
float x = nodeRect.Min.x - direction;
|
|
float y = ImLerp(nodeRect.Min.y, nodeRect.Max.y, offset);
|
|
pinCenter = ImVec2(x - halfSize.x, y);
|
|
break;
|
|
}
|
|
|
|
case PinEdge::Right:
|
|
{
|
|
float x = nodeRect.Max.x + direction;
|
|
float y = ImLerp(nodeRect.Min.y, nodeRect.Max.y, offset);
|
|
pinCenter = ImVec2(x + halfSize.x, y);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate pin rectangle
|
|
ImRect pinRect = ImRect(
|
|
pinCenter - halfSize,
|
|
pinCenter + halfSize
|
|
);
|
|
|
|
// Calculate pivot point at the edge for link attachment
|
|
ImVec2 pivotPoint;
|
|
switch (edge)
|
|
{
|
|
case PinEdge::Top:
|
|
pivotPoint = ImVec2(pinRect.GetCenter().x, pinRect.Min.y);
|
|
break;
|
|
|
|
case PinEdge::Bottom:
|
|
pivotPoint = ImVec2(pinRect.GetCenter().x, pinRect.Max.y);
|
|
break;
|
|
|
|
case PinEdge::Left:
|
|
pivotPoint = ImVec2(pinRect.Min.x, pinRect.GetCenter().y);
|
|
break;
|
|
|
|
case PinEdge::Right:
|
|
pivotPoint = ImVec2(pinRect.Max.x, pinRect.GetCenter().y);
|
|
break;
|
|
}
|
|
|
|
// Set pin direction BEFORE BeginPin (direction is captured during BeginPin)
|
|
// This overrides the node-level style scope
|
|
switch (edge)
|
|
{
|
|
case PinEdge::Top:
|
|
// Pins on top: links flow upward (negative Y)
|
|
if (kind == PinKind::Input)
|
|
PushStyleVar(StyleVar_TargetDirection, ImVec2(0.0f, -1.0f));
|
|
else
|
|
PushStyleVar(StyleVar_SourceDirection, ImVec2(0.0f, -1.0f));
|
|
break;
|
|
|
|
case PinEdge::Bottom:
|
|
// Pins on bottom: links flow downward (positive Y)
|
|
if (kind == PinKind::Input)
|
|
PushStyleVar(StyleVar_TargetDirection, ImVec2(0.0f, 1.0f));
|
|
else
|
|
PushStyleVar(StyleVar_SourceDirection, ImVec2(0.0f, 1.0f));
|
|
break;
|
|
|
|
case PinEdge::Left:
|
|
// Pins on left: links flow LEFT (negative X)
|
|
if (kind == PinKind::Input)
|
|
PushStyleVar(StyleVar_TargetDirection, ImVec2(-1.0f, 0.0f));
|
|
else
|
|
PushStyleVar(StyleVar_SourceDirection, ImVec2(-1.0f, 0.0f));
|
|
break;
|
|
|
|
case PinEdge::Right:
|
|
// Pins on right: links flow RIGHT (positive X)
|
|
if (kind == PinKind::Input)
|
|
PushStyleVar(StyleVar_TargetDirection, ImVec2(1.0f, 0.0f));
|
|
else
|
|
PushStyleVar(StyleVar_SourceDirection, ImVec2(1.0f, 0.0f));
|
|
break;
|
|
}
|
|
|
|
// Begin pin (direction is captured here from the style we just pushed)
|
|
BeginPin(pinId, kind);
|
|
|
|
// Save current cursor position so we can restore it
|
|
ImVec2 savedCursorPos = ImGui::GetCursorScreenPos();
|
|
|
|
// Draw visual indicator using renderer function
|
|
auto drawList = ImGui::GetWindowDrawList();
|
|
ImVec2 center = pinRect.GetCenter();
|
|
|
|
// Get colors based on pin kind and state
|
|
ImU32 fillColor, borderColor;
|
|
GetPinColors(kind, state, fillColor, borderColor);
|
|
|
|
// Use custom renderer if provided, otherwise use default based on edge
|
|
if (renderer != nullptr)
|
|
{
|
|
renderer(drawList, center, pinSize, fillColor, borderColor, state);
|
|
}
|
|
else
|
|
{
|
|
// Default: boxes for top/bottom, circles for left/right
|
|
if (edge == PinEdge::Top || edge == PinEdge::Bottom)
|
|
{
|
|
RenderPinBox(drawList, center, pinSize, fillColor, borderColor, state);
|
|
}
|
|
else // Left or Right
|
|
{
|
|
RenderPinCircle(drawList, center, pinSize, fillColor, borderColor, state);
|
|
}
|
|
}
|
|
|
|
// Set precise pin rectangle (this prevents automatic bounds resolution)
|
|
PinRect(pinRect.Min, pinRect.Max);
|
|
|
|
// Set pivot point at edge for proper link connection
|
|
PinPivotRect(pivotPoint, pivotPoint);
|
|
|
|
// Restore cursor position so we don't affect node size calculation
|
|
ImGui::SetCursorScreenPos(savedCursorPos);
|
|
|
|
// End pin (now it won't try to resolve bounds from cursor/item rect)
|
|
EndPin();
|
|
|
|
// Pop the direction style var we pushed
|
|
PopStyleVar();
|
|
|
|
// Store tooltip data if provided
|
|
if (tooltip != nullptr && strlen(tooltip) > 0)
|
|
{
|
|
PinTooltipData tooltipData;
|
|
tooltipData.pinId = pinId;
|
|
tooltipData.bounds = pinRect;
|
|
tooltipData.tooltip = tooltip;
|
|
s_PinTooltips.push_back(tooltipData);
|
|
}
|
|
|
|
return pinRect;
|
|
}
|
|
|
|
// Overload: PinEx with nodeId (for use after EndNode, when node bounds are finalized)
|
|
ImRect PinEx(
|
|
PinId pinId,
|
|
PinKind kind,
|
|
PinEdge edge,
|
|
float offset,
|
|
float direction,
|
|
NodeId nodeId,
|
|
PinState state,
|
|
const char* tooltip,
|
|
PinRenderer renderer)
|
|
{
|
|
// Get node bounds from node ID
|
|
ImVec2 nodePos = GetNodePosition(nodeId);
|
|
ImVec2 nodeSize = GetNodeSize(nodeId);
|
|
ImRect nodeRect = ImRect(nodePos, nodePos + nodeSize);
|
|
|
|
// Use the main implementation
|
|
return PinEx(pinId, kind, edge, offset, direction, nodeRect, state, tooltip, renderer);
|
|
}
|
|
|
|
// Helper: Process tooltips for pins (call this after EndNode, typically in a deferred section)
|
|
// Must be called inside Suspend() block (outside canvas coordinate space)
|
|
void ProcessPinTooltips()
|
|
{
|
|
if (s_PinTooltips.empty())
|
|
return;
|
|
|
|
// Get mouse position in screen space (we're in Suspend, so mouse is in screen space)
|
|
ImVec2 mouseScreenPos = ImGui::GetMousePos();
|
|
|
|
// Convert screen position to canvas space
|
|
// Pin bounds are stored in canvas space (from PinRect), so we need to convert mouse pos
|
|
ImVec2 mouseCanvasPos = ScreenToCanvas(mouseScreenPos);
|
|
|
|
// Find hovered pin
|
|
for (const auto& tooltipData : s_PinTooltips)
|
|
{
|
|
// Check if mouse is within pin bounds (bounds are in canvas space)
|
|
if (tooltipData.bounds.Contains(mouseCanvasPos))
|
|
{
|
|
ImGui::SetTooltip("%s", tooltipData.tooltip);
|
|
break; // Only show one tooltip at a time
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear tooltip data (call at start of frame or after processing)
|
|
void ClearPinTooltips()
|
|
{
|
|
s_PinTooltips.clear();
|
|
}
|
|
|
|
} // namespace NodeEditor
|
|
} // namespace ax
|