From 038450a1ca2eb9c90a1ad69c5a27ab13a52e15fd Mon Sep 17 00:00:00 2001 From: indigo Date: Wed, 8 Jul 2026 22:35:23 +0800 Subject: [PATCH] Vendor node editor in-tree; add material presets; debug drag regression Move the imgui-node-editor subset the build actually compiles from third_party/imgui-node-editor to src/ui/NodeEditor (version-controlled, MIT LICENSE included). FindImguiNodeEditor.cmake points at the new location; CMakeLists excludes src/ui/NodeEditor from UI_SOURCES so it isn't compiled twice. Material editor presets: a Presets menu (disabled with no open material) builds a UsdPreviewSurface + UsdUVTexture graph fed by an st primvar reader, or a MaterialX standard_surface + image graph fed by a texcoord node. Each is one idempotent, undoable command that re-normalizes layout. Create Material becomes an icon button. New nodes route through FindFreeCanvasSpot so a creation never lands on top of an existing node (overlapping nodes fight over the editor hit test and become undraggable). Shader-ball preview: pick a previewable output (terminal or 3/4-component color-like) instead of always the first output, so scalar-only nodes keep the whole-material preview rather than failing Storm codegen. Per-shape camera frame-fit margins and auto-clip framing. Fixes: DeletePrimCommand::Undo recreates missing destination ancestors before SdfCopySpec (parent material may have been deleted after the command ran). ConfigWindowsMoveFromTitleBarOnly stops a content-area drag in the node canvas from moving the whole Material Editor window. Temporary (marked for removal once the node-editor drag regression is diagnosed): main.cpp mirrors LOG_INFO to %APPDATA%\UsdLayerManager\ debug.log; a g_AxNodeEditorDebugLog hook in the vendored editor plus a [NodeGraph] event-trace block in RenderNodeGraphCanvas dump click/drag/ selection/position-save state. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 5 + cmake/modules/FindImguiNodeEditor.cmake | 18 +- src/core/commands/DeletePrimCommand.cpp | 8 + src/main.cpp | 12 +- src/ui/ImGuiContext.cpp | 10 + src/ui/MaterialEditorPanel.cpp | 459 +- src/ui/MaterialEditorPanel.h | 20 + src/ui/MaterialPreviewRenderer.cpp | 27 +- .../BlueprintWidgets/Include/ax/Drawing.h | 12 + .../BlueprintWidgets/Include/ax/Math2D.h | 1256 ++++ .../BlueprintWidgets/Include/ax/Math2D.inl | 642 ++ .../BlueprintWidgets/Include/ax/Widgets.h | 13 + .../BlueprintWidgets/Source/Interop.h | 69 + .../BlueprintWidgets/Source/ax/Drawing.cpp | 207 + .../BlueprintWidgets/Source/ax/Widgets.cpp | 15 + src/ui/NodeEditor/Include/imgui_node_editor.h | 442 ++ src/ui/NodeEditor/LICENSE | 21 + src/ui/NodeEditor/Source/crude_json.cpp | 814 +++ src/ui/NodeEditor/Source/crude_json.h | 223 + src/ui/NodeEditor/Source/imgui_bezier_math.h | 142 + .../NodeEditor/Source/imgui_bezier_math.inl | 668 ++ src/ui/NodeEditor/Source/imgui_canvas.cpp | 452 ++ src/ui/NodeEditor/Source/imgui_canvas.h | 250 + src/ui/NodeEditor/Source/imgui_extra_math.h | 76 + src/ui/NodeEditor/Source/imgui_extra_math.inl | 191 + .../NodeEditor/Source/imgui_node_editor.cpp | 5445 +++++++++++++++++ .../Source/imgui_node_editor_api.cpp | 637 ++ .../Source/imgui_node_editor_internal.h | 1496 +++++ .../Source/imgui_node_editor_internal.inl | 55 + 29 files changed, 13658 insertions(+), 27 deletions(-) create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Include/ax/Drawing.h create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.h create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.inl create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Include/ax/Widgets.h create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Source/Interop.h create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Source/ax/Drawing.cpp create mode 100644 src/ui/NodeEditor/BlueprintWidgets/Source/ax/Widgets.cpp create mode 100644 src/ui/NodeEditor/Include/imgui_node_editor.h create mode 100644 src/ui/NodeEditor/LICENSE create mode 100644 src/ui/NodeEditor/Source/crude_json.cpp create mode 100644 src/ui/NodeEditor/Source/crude_json.h create mode 100644 src/ui/NodeEditor/Source/imgui_bezier_math.h create mode 100644 src/ui/NodeEditor/Source/imgui_bezier_math.inl create mode 100644 src/ui/NodeEditor/Source/imgui_canvas.cpp create mode 100644 src/ui/NodeEditor/Source/imgui_canvas.h create mode 100644 src/ui/NodeEditor/Source/imgui_extra_math.h create mode 100644 src/ui/NodeEditor/Source/imgui_extra_math.inl create mode 100644 src/ui/NodeEditor/Source/imgui_node_editor.cpp create mode 100644 src/ui/NodeEditor/Source/imgui_node_editor_api.cpp create mode 100644 src/ui/NodeEditor/Source/imgui_node_editor_internal.h create mode 100644 src/ui/NodeEditor/Source/imgui_node_editor_internal.inl diff --git a/CMakeLists.txt b/CMakeLists.txt index 18227b9..98fdd4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,11 @@ endif() # Collect source files file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp") file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp") +# src/ui/NodeEditor is the in-tree vendored imgui-node-editor subset — already +# compiled into the imgui_node_editor_impl target by FindImguiNodeEditor.cmake +# (with its own required include dirs/defines); excluded here so it isn't +# compiled a second time, incorrectly, as part of this target's own sources. +list(FILTER UI_SOURCES EXCLUDE REGEX "/src/ui/NodeEditor/") file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp") set(MAIN_SOURCE "src/main.cpp") diff --git a/cmake/modules/FindImguiNodeEditor.cmake b/cmake/modules/FindImguiNodeEditor.cmake index 40e20e5..3da85ec 100644 --- a/cmake/modules/FindImguiNodeEditor.cmake +++ b/cmake/modules/FindImguiNodeEditor.cmake @@ -1,9 +1,13 @@ # FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor # -# This module looks for imgui-node-editor in the third_party/imgui-node-editor -# directory (vendored, docking branch) and compiles its sources directly -# against this project's own Imgui::Imgui target so it shares the exact same -# ImGui headers/defines (no second copy of ImGui is pulled in). +# This module looks for imgui-node-editor at src/ui/NodeEditor (an in-tree, +# version-controlled subset of the upstream library — MIT licensed, see +# src/ui/NodeEditor/LICENSE — containing only the files this project's build +# actually compiles; the full upstream drop, including the standalone example +# app used to reproduce library-level bugs against a pure reference build, +# stays vendored separately at third_party/imgui-node-editor) and compiles its +# sources directly against this project's own Imgui::Imgui target so it shares +# the exact same ImGui headers/defines (no second copy of ImGui is pulled in). # # Inputs: # IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root @@ -13,12 +17,12 @@ # ImguiNodeEditor::ImguiNodeEditor (imported STATIC target) if(NOT IMGUI_NODE_EDITOR_DIR) - set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/NodeEditor") + set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor") endif() if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR) set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR - "${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/Examples/Common/BlueprintUtilities") + "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor/BlueprintWidgets") endif() find_path(ImguiNodeEditor_INCLUDE_DIR @@ -48,7 +52,7 @@ endforeach() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(ImguiNodeEditor REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR - FAIL_MESSAGE "imgui-node-editor not found — expected it vendored at third_party/imgui-node-editor/NodeEditor" + FAIL_MESSAGE "imgui-node-editor not found — expected it at src/ui/NodeEditor" ) if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING) diff --git a/src/core/commands/DeletePrimCommand.cpp b/src/core/commands/DeletePrimCommand.cpp index 3f77d63..e2bec45 100644 --- a/src/core/commands/DeletePrimCommand.cpp +++ b/src/core/commands/DeletePrimCommand.cpp @@ -59,6 +59,14 @@ void DeletePrimCommand::Undo() { if (!rootLayer) return; try { + // The destination ancestors may be gone by now (e.g. the parent + // material was itself deleted after this command ran) — SdfCopySpec + // fails with "No spec ... when trying to set field 'primChildren'" + // when the parent spec is missing, so recreate the chain first. + pxr::SdfPath parentPath = m_primPath.GetParentPath(); + if (!parentPath.IsAbsoluteRootPath() && !rootLayer->GetPrimAtPath(parentPath)) + pxr::SdfCreatePrimInLayer(rootLayer, parentPath); + if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath)) LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString()); } catch (const std::exception& e) { diff --git a/src/main.cpp b/src/main.cpp index 189d471..bd8f450 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include static void SetUsdPluginPath() { char exePath[MAX_PATH]; @@ -98,7 +99,16 @@ static void SetUsdPluginPath() { int main(int /*argc*/, char* /*argv*/[]) { try { UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info); - + // Temporary: capture LOG_INFO to a file so the [NodeGraph]/[ed] debug + // traces are readable after the fact (this is a WIN32-subsystem app, + // so stdout isn't visible when launched normally). Remove once the + // node-editor drag regression is diagnosed. + if (const char* appData = getenv("APPDATA")) { + std::filesystem::path logDir = std::filesystem::path(appData) / "UsdLayerManager"; + std::filesystem::create_directories(logDir); + UsdLayerManager::Logger::Instance().SetLogFile((logDir / "debug.log").string()); + } + LOG_INFO("=== USD Layer Manager Starting ==="); SetUsdPluginPath(); diff --git a/src/ui/ImGuiContext.cpp b/src/ui/ImGuiContext.cpp index a5b8a06..fa3c99a 100644 --- a/src/ui/ImGuiContext.cpp +++ b/src/ui/ImGuiContext.cpp @@ -98,6 +98,16 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei // position overlay appears then); a plain drag just moves the window, // so casual rearranging can't accidentally re-dock panels. io.ConfigDockingWithShift = true; + // Windows move only via their title bar/tab, never by dragging empty + // content. Without this, a press-drag that a content widget doesn't + // capture falls through to ImGui's "drag body to move window" behaviour — + // which, in the node-editor canvas (whose background button doesn't + // reliably claim the drag in this nested-child embedding), meant dragging + // in the graph moved the whole Material Editor window. Title-bar dragging + // and Shift-to-dock are unaffected; this is also the desired behaviour for + // every other content area (viewports, timelines) where a content drag + // must never move the host window. + io.ConfigWindowsMoveFromTitleBarOnly = true; // Store imgui.ini in %APPDATA%\UsdLayerManager\ so it doesn't litter the CWD. if (const char* appData = getenv("APPDATA")) { diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index dfdf83b..ed823ff 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -26,6 +26,11 @@ #include #include +// UsdLayerManager local patch hook in the vendored node editor (see +// imgui_node_editor.cpp): when set, BuildControl logs its click-time +// hit-test state through it. Temporary debugging aid. +extern void (*g_AxNodeEditorDebugLog)(const char*); + namespace UsdLayerManager { namespace NE = ax::NodeEditor; @@ -221,6 +226,10 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) { } // namespace MaterialEditorPanel::MaterialEditorPanel() { + // Route the vendored editor's click-time hit-test dump into our log so + // it interleaves with the [NodeGraph] event trace. + g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); }; + NE::Config config; config.SettingsFile = nullptr; // layout persists as USD uiPosition custom data, not an on-disk .json config.UserPointer = this; @@ -303,12 +312,30 @@ void MaterialEditorPanel::RenderPreviewPanel() { bool outputIsTerminal = false; if (!m_selectedNodePath.IsEmpty()) { for (const auto& node : m_graph.nodes) { - if (node.path == m_selectedNodePath && !node.outputs.empty()) { - previewNode = node.path; - previewOutput = node.outputs.front().name; - outputIsTerminal = (node.outputs.front().typeName == pxr::SdfValueTypeNames->Token); - break; + if (node.path != m_selectedNodePath) + continue; + // Pick a previewable output: a terminal (surface) or a + // 3/4-component value the wrapper can plug into diffuseColor. + // Scalar outputs (e.g. UsdUVTexture's Sdr-first output "r") + // make Storm's codegen swizzle .xyz off a float and fail to + // compile — nodes with only scalar outputs keep the + // whole-material preview instead. + const auto& tn = pxr::SdfValueTypeNames; + for (const auto& out : node.outputs) { + const pxr::SdfValueTypeName& t = out.typeName; + const bool terminal = (t == tn->Token); + const bool colorLike = + t == tn->Color3f || t == tn->Float3 || t == tn->Vector3f || + t == tn->Normal3f || t == tn->Point3f || + t == tn->Color4f || t == tn->Float4; + if (terminal || colorLike) { + previewNode = node.path; + previewOutput = out.name; + outputIsTerminal = terminal; + break; + } } + break; } } m_preview.SetMaterial(m_stage, m_materialPath, ComputeGraphRevision(m_stage, m_graph), @@ -585,9 +612,47 @@ void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const Sha } void MaterialEditorPanel::RenderToolbar() { - if (ImGui::Button("Create Material")) + // Create Material icon button + bool createClicked; + if (m_iconManager) { + createClicked = ImGui::ImageButton("##createMaterial", + ImTextureRef(m_iconManager->Get(Icon::FilePlus)), + ImVec2(18.f, 18.f)); + } else { + createClicked = ImGui::Button("Create Material"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Create Material"); + if (createClicked) CreateNewMaterial(); + // Presets menu: entries appear per node source when the required shader + // definitions are registered, and need an open material to apply to. + ImGui::SameLine(); + ImGui::BeginDisabled(m_materialPath.IsEmpty()); + if (ImGui::Button("Presets")) + ImGui::OpenPopup("MaterialPresets"); + ImGui::EndDisabled(); + if (ImGui::BeginPopup("MaterialPresets")) { + auto& sdr = pxr::SdrRegistry::GetInstance(); + if (ImGui::BeginMenu("USD")) { + ImGui::BeginDisabled(!sdr.GetShaderNodeByIdentifier(pxr::TfToken("UsdPreviewSurface"))); + if (ImGui::MenuItem("Default Shader Graph")) + CreateUsdPresetGraph(); + ImGui::EndDisabled(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("MaterialX")) { + ImGui::BeginDisabled( + !sdr.GetShaderNodeByIdentifier(pxr::TfToken("ND_standard_surface_surfaceshader"))); + if (ImGui::MenuItem("Standard Surface Graph")) + CreateMaterialXPresetGraph(); + ImGui::EndDisabled(); + ImGui::EndMenu(); + } + ImGui::EndPopup(); + } + if (!m_targetPrimPath.IsEmpty()) { ImGui::SameLine(); ImGui::Text("Selected: %s", m_targetPrimPath.GetText()); @@ -731,6 +796,159 @@ void MaterialEditorPanel::CreateNewMaterial() { OpenOrCreateMaterial(materialsScope.AppendChild(pxr::TfToken(finalName)).GetString()); } +namespace { +// Shared by the preset builders below. +pxr::UsdShadeShader DefinePresetShader(const pxr::UsdStageRefPtr& stage, + const pxr::SdfPath& materialPath, + const char* name, const char* shaderId, + const pxr::GfVec2f& uiPos) { + pxr::UsdShadeShader shader = + pxr::UsdShadeShader::Define(stage, materialPath.AppendChild(pxr::TfToken(name))); + shader.CreateIdAttr(pxr::VtValue(pxr::TfToken(shaderId))); + shader.GetPrim().SetCustomDataByKey(pxr::TfToken("uiPosition"), pxr::VtValue(uiPos)); + return shader; +} +} // namespace + +void MaterialEditorPanel::CreateUsdPresetGraph() { + if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return; + // Re-applying onto an existing preset graph is allowed: the build is + // idempotent (same prims/connections re-authored, user-set values like + // texture file paths survive) and re-normalizes the node layout. + + pxr::UsdStageRefPtr stage = m_stage; + pxr::SdfPath matPath = m_materialPath; + + auto build = [stage, matPath]() { + // Column/row spacing must exceed the rendered node sizes (full Sdr + // pin sets make these ~400px tall): overlapping nodes steal each + // other's clicks in the editor's hit test and become undraggable. + pxr::UsdShadeShader surf = DefinePresetShader( + stage, matPath, "UsdPreviewSurface", "UsdPreviewSurface", pxr::GfVec2f(0.f, 400.f)); + pxr::UsdShadeShader st = DefinePresetShader( + stage, matPath, "stReader", "UsdPrimvarReader_float2", pxr::GfVec2f(-840.f, 600.f)); + st.CreateInput(pxr::TfToken("varname"), pxr::SdfValueTypeNames->Token) + .Set(pxr::TfToken("st")); + + struct TexPreset { + const char* name; + const char* texOutput; pxr::SdfValueTypeName outType; + const char* destInput; pxr::SdfValueTypeName destType; + bool rawColorSpace; + }; + const TexPreset textures[] = { + {"diffuseTexture", "rgb", pxr::SdfValueTypeNames->Float3, + "diffuseColor", pxr::SdfValueTypeNames->Color3f, false}, + {"metallicTexture", "r", pxr::SdfValueTypeNames->Float, + "metallic", pxr::SdfValueTypeNames->Float, true}, + {"roughnessTexture", "r", pxr::SdfValueTypeNames->Float, + "roughness", pxr::SdfValueTypeNames->Float, true}, + {"normalTexture", "rgb", pxr::SdfValueTypeNames->Float3, + "normal", pxr::SdfValueTypeNames->Normal3f, true}, + }; + float y = 0.f; + for (const TexPreset& t : textures) { + pxr::UsdShadeShader tex = DefinePresetShader( + stage, matPath, t.name, "UsdUVTexture", pxr::GfVec2f(-420.f, y)); + y += 440.f; + tex.CreateInput(pxr::TfToken("st"), pxr::SdfValueTypeNames->Float2) + .ConnectToSource(st.ConnectableAPI(), pxr::TfToken("result")); + if (t.rawColorSpace) + tex.CreateInput(pxr::TfToken("sourceColorSpace"), pxr::SdfValueTypeNames->Token) + .Set(pxr::TfToken("raw")); + surf.CreateInput(pxr::TfToken(t.destInput), t.destType) + .ConnectToSource(tex.ConnectableAPI(), pxr::TfToken(t.texOutput)); + } + // Normal maps need the [0,1] texture range remapped to [-1,1]. + pxr::UsdShadeShader normalTex( + stage->GetPrimAtPath(matPath.AppendChild(pxr::TfToken("normalTexture")))); + normalTex.CreateInput(pxr::TfToken("scale"), pxr::SdfValueTypeNames->Float4) + .Set(pxr::GfVec4f(2.f, 2.f, 2.f, 1.f)); + normalTex.CreateInput(pxr::TfToken("bias"), pxr::SdfValueTypeNames->Float4) + .Set(pxr::GfVec4f(-1.f, -1.f, -1.f, 0.f)); + + pxr::UsdShadeMaterial material(stage->GetPrimAtPath(matPath)); + if (material) + material.CreateSurfaceOutput().ConnectToSource( + surf.ConnectableAPI(), pxr::TfToken("surface")); + }; + auto remove = [stage, matPath]() { + for (const char* name : {"UsdPreviewSurface", "stReader", "diffuseTexture", + "metallicTexture", "roughnessTexture", "normalTexture"}) + stage->RemovePrim(matPath.AppendChild(pxr::TfToken(name))); + if (pxr::UsdPrim mat = stage->GetPrimAtPath(matPath)) + mat.RemoveProperty(pxr::TfToken("outputs:surface")); + }; + + m_commandHistory->Push(std::make_unique( + "Create USD Preset Graph", build, remove)); + // Force a position reseed: on a re-apply the nodes are already known to + // the editor at their old canvas positions. + m_nodeIdToPath.clear(); + SyncFromUsd(); +} + +void MaterialEditorPanel::CreateMaterialXPresetGraph() { + if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return; + // Re-applying is allowed; see CreateUsdPresetGraph. + + pxr::UsdStageRefPtr stage = m_stage; + pxr::SdfPath matPath = m_materialPath; + + auto build = [stage, matPath]() { + // See the USD preset: spacing must exceed rendered node sizes or + // overlapping nodes steal each other's drag hit-tests. + pxr::UsdShadeShader surf = DefinePresetShader( + stage, matPath, "standard_surface", "ND_standard_surface_surfaceshader", + pxr::GfVec2f(0.f, 300.f)); + pxr::UsdShadeShader texcoord = DefinePresetShader( + stage, matPath, "texcoord", "ND_texcoord_vector2", pxr::GfVec2f(-840.f, 400.f)); + + struct ImagePreset { + const char* name; + const char* shaderId; pxr::SdfValueTypeName outType; + const char* destInput; pxr::SdfValueTypeName destType; + }; + const ImagePreset images[] = { + {"base_color_image", "ND_image_color3", pxr::SdfValueTypeNames->Color3f, + "base_color", pxr::SdfValueTypeNames->Color3f}, + {"specular_roughness_image", "ND_image_float", pxr::SdfValueTypeNames->Float, + "specular_roughness", pxr::SdfValueTypeNames->Float}, + {"metalness_image", "ND_image_float", pxr::SdfValueTypeNames->Float, + "metalness", pxr::SdfValueTypeNames->Float}, + }; + float y = 0.f; + for (const ImagePreset& img : images) { + pxr::UsdShadeShader tex = DefinePresetShader( + stage, matPath, img.name, img.shaderId, pxr::GfVec2f(-420.f, y)); + y += 400.f; + tex.CreateInput(pxr::TfToken("texcoord"), pxr::SdfValueTypeNames->Float2) + .ConnectToSource(texcoord.ConnectableAPI(), pxr::TfToken("out")); + surf.CreateInput(pxr::TfToken(img.destInput), img.destType) + .ConnectToSource(tex.ConnectableAPI(), pxr::TfToken("out")); + } + + // MaterialX networks terminate on the mtlx render-context output + // (matches usdMtlx-imported materials). + pxr::UsdShadeMaterial material(stage->GetPrimAtPath(matPath)); + if (material) + material.CreateSurfaceOutput(pxr::TfToken("mtlx")).ConnectToSource( + surf.ConnectableAPI(), pxr::TfToken("out")); + }; + auto remove = [stage, matPath]() { + for (const char* name : {"standard_surface", "texcoord", "base_color_image", + "specular_roughness_image", "metalness_image"}) + stage->RemovePrim(matPath.AppendChild(pxr::TfToken(name))); + if (pxr::UsdPrim mat = stage->GetPrimAtPath(matPath)) + mat.RemoveProperty(pxr::TfToken("outputs:mtlx:surface")); + }; + + m_commandHistory->Push(std::make_unique( + "Create MaterialX Preset Graph", build, remove)); + m_nodeIdToPath.clear(); // reseed positions (see CreateUsdPresetGraph) + SyncFromUsd(); +} + void MaterialEditorPanel::OpenOrCreateMaterial(const std::string& pathStr) { if (!m_stage) return; if (!pxr::SdfPath::IsValidPathString(pathStr, nullptr)) { @@ -883,6 +1101,8 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { NE::Begin("MaterialEditorCanvas", ImVec2(0.0f, 0.0f)); if (!m_pendingCreateShaderId.empty()) { + // Browser-list creations land at the view center; CreateShaderNode + // nudges the spot clear of existing nodes. CreateShaderNode(m_pendingCreateShaderId, NE::ScreenToCanvas(viewCenterScreen)); m_pendingCreateShaderId.clear(); } @@ -1001,6 +1221,164 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { NE::End(); + // --- Debug tracing of node interaction events. Edge-triggered so each + // event logs once, not per frame. Event order in the editor is: + // LMB press -> node becomes active (and is brought to front) -> drag + // moves it -> release -> "click" -> selection updates -> position save. + { + // Logs the mouse position and every node's editor rect with a hit + // verdict, and returns the hit paths. Topmost is unknown to us, but + // the full table makes overlap fights directly visible in the log. + // hitIds, when given, collects the editor ids of the hit nodes. + auto nodeRectsAtMouse = [this](std::vector* hitIds) { + std::string hits; + const ImVec2 screen = ImGui::GetMousePos(); + const ImVec2 mouse = NE::ScreenToCanvas(screen); + char buf[512]; + std::snprintf(buf, sizeof(buf), + "[NodeGraph] hit test: mouse screen (%.1f, %.1f) canvas (%.1f, %.1f), %zu node(s)", + screen.x, screen.y, mouse.x, mouse.y, m_graph.nodes.size()); + LOG_INFO(std::string(buf)); + for (const auto& node : m_graph.nodes) { + NE::NodeId nodeId(HashId(node.path.GetString())); + const ImVec2 pos = NE::GetNodePosition(nodeId); + const ImVec2 size = NE::GetNodeSize(nodeId); + if (pos.x == FLT_MAX || size.x <= 0.0f) { + LOG_INFO("[NodeGraph] " + node.path.GetString() + + ": no editor rect yet (not rendered)"); + continue; + } + const bool hit = mouse.x >= pos.x && mouse.x <= pos.x + size.x && + mouse.y >= pos.y && mouse.y <= pos.y + size.y; + std::snprintf(buf, sizeof(buf), + "[NodeGraph] %s: rect min (%.1f, %.1f) size (%.1f x %.1f)%s", + node.path.GetText(), pos.x, pos.y, size.x, size.y, + hit ? " <-- HIT" : ""); + LOG_INFO(std::string(buf)); + if (hit) { + hits += (hits.empty() ? "" : ", ") + node.path.GetString(); + if (hitIds) + hitIds->push_back(nodeId.Get()); + } + } + return hits; + }; + + // Editor NodeIds of the currently selected nodes. + auto selectedNodeIds = []() { + std::vector ids; + const int count = NE::GetSelectedObjectCount(); + if (count > 0) { + std::vector sel(static_cast(count)); + const int nodeCount = NE::GetSelectedNodes(sel.data(), count); + ids.reserve(static_cast(nodeCount)); + for (int i = 0; i < nodeCount; ++i) + ids.push_back(sel[static_cast(i)].Get()); + } + return ids; + }; + auto nodeNames = [this](const std::vector& ids) { + std::string names; + for (uintptr_t idValue : ids) { + auto it = m_nodeIdToPath.find(idValue); + names += (names.empty() ? "" : ", "); + names += (it != m_nodeIdToPath.end()) ? it->second.GetName() : ""; + } + return names; + }; + auto containsAnyOf = [](const std::vector& haystack, + const std::vector& needles) { + for (uintptr_t needle : needles) + if (std::find(haystack.begin(), haystack.end(), needle) != haystack.end()) + return true; + return false; + }; + + if (canvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + m_debugDragNodeIds.clear(); + const std::string hits = nodeRectsAtMouse(&m_debugDragNodeIds); + m_debugPressOnNode = !hits.empty(); + m_debugDragLogged = false; + if (m_debugPressOnNode) { + // Pressing an already-selected node keeps the selection (no + // clear, no rect-select) so a drag moves the whole group — + // the editor only updates selection on release. + const std::vector selected = selectedNodeIds(); + std::string msg = "[NodeGraph] LMB press over node rect(s): " + hits; + if (selected.size() > 1 && containsAnyOf(selected, m_debugDragNodeIds)) + msg += " [in current selection of " + std::to_string(selected.size()) + + " -> drag moves all selected]"; + LOG_INFO(msg); + } else { + LOG_INFO("[NodeGraph] LMB press on empty canvas (rect select / clear selection on release)"); + } + } + if (m_debugPressOnNode && !m_debugDragLogged && + ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + m_debugDragLogged = true; + // Mirror DragAction::Accept: dragging a selected node moves the + // whole selection, so trace every group member from here on. + const std::vector selected = selectedNodeIds(); + if (selected.size() > 1 && containsAnyOf(selected, m_debugDragNodeIds)) { + m_debugDragNodeIds = selected; + LOG_INFO("[NodeGraph] drag start: group move of " + + std::to_string(selected.size()) + " selected node(s): " + + nodeNames(m_debugDragNodeIds)); + } else { + LOG_INFO("[NodeGraph] drag start (ready to move), cursor over: " + nodeRectsAtMouse(nullptr)); + } + } + // Per-move trace while a drag is live, only on frames the mouse + // actually moved: mouse position, accumulated drag delta, and the + // live editor position of every tracked node — the whole selection + // for a group drag, else the node(s) hit at press time (a node that + // stops tracking the delta shows up immediately). + if (m_debugDragLogged && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && + (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) { + const ImVec2 screen = ImGui::GetMousePos(); + const ImVec2 canvas = NE::ScreenToCanvas(screen); + const ImVec2 delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0f); + char buf[512]; + std::snprintf(buf, sizeof(buf), + "[NodeGraph] drag move: mouse screen (%.1f, %.1f) canvas (%.1f, %.1f) dragDelta (%.1f, %.1f)", + screen.x, screen.y, canvas.x, canvas.y, delta.x, delta.y); + std::string msg = buf; + for (uintptr_t idValue : m_debugDragNodeIds) { + const ImVec2 nodePos = NE::GetNodePosition(NE::NodeId(idValue)); + auto it = m_nodeIdToPath.find(idValue); + std::snprintf(buf, sizeof(buf), "; %s at (%.1f, %.1f)", + it != m_nodeIdToPath.end() ? it->second.GetName().c_str() : "", + nodePos.x, nodePos.y); + msg += buf; + } + LOG_INFO(msg); + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && + (m_debugPressOnNode || m_debugDragLogged)) { + LOG_INFO(std::string("[NodeGraph] LMB release") + + (m_debugDragLogged ? " (ends drag; position save should follow)" : " (click, no drag)")); + m_debugPressOnNode = false; + m_debugDragLogged = false; + m_debugDragNodeIds.clear(); + } + + // Covers single-click selection and rubber-band rect multi-select + // alike; fires on the frame the editor's selection list changes. + if (NE::HasSelectionChanged()) + { + const int count = NE::GetSelectedObjectCount(); + std::vector selected(static_cast(std::max(count, 1))); + const int nodeCount = NE::GetSelectedNodes(selected.data(), count); + std::string msg = "[NodeGraph] selection changed: " + std::to_string(nodeCount) + " node(s)"; + for (int i = 0; i < nodeCount; ++i) { + auto it = m_nodeIdToPath.find(selected[static_cast(i)].Get()); + msg += (i == 0 ? ": " : ", "); + msg += (it != m_nodeIdToPath.end()) ? it->second.GetString() : ""; + } + LOG_INFO(msg); + } + } + // Mirror the editor's node selection (first selected node) for the // properties section under the preview. m_selectedNodePath = pxr::SdfPath(); @@ -1124,6 +1502,54 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) { } } +ImVec2 MaterialEditorPanel::FindFreeCanvasSpot(ImVec2 desired, const std::string& shaderId) const { + // Estimated footprint of the not-yet-rendered node: same height model as + // ApplyFallbackLayout, width covering typical icon+label pin rows. + auto estimateSize = [](size_t pinCount) { + return ImVec2(340.0f, static_cast(pinCount) * 24.0f + 80.0f); + }; + size_t newPinCount = 6; + if (pxr::SdrShaderNodeConstPtr sdrNode = + pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(pxr::TfToken(shaderId))) + newPinCount = sdrNode->GetShaderInputNames().size() + sdrNode->GetShaderOutputNames().size(); + const ImVec2 newSize = estimateSize(newPinCount); + + struct Rect { ImVec2 pos, size; }; + std::vector rects; + rects.reserve(m_graph.nodes.size()); + for (const auto& node : m_graph.nodes) { + Rect r{ImVec2(node.uiPosition[0], node.uiPosition[1]), + estimateSize(node.inputs.size() + node.outputs.size())}; + // Nodes the editor has already rendered report their real rectangle. + NE::NodeId nodeId(HashId(node.path.GetString())); + const ImVec2 livePos = NE::GetNodePosition(nodeId); + const ImVec2 liveSize = NE::GetNodeSize(nodeId); + if (livePos.x != FLT_MAX && liveSize.x > 0.0f) { + r.pos = livePos; + r.size = liveSize; + } + rects.push_back(r); + } + + const float margin = 24.0f; + bool moved = true; + while (moved) { + moved = false; + for (const Rect& r : rects) { + const bool overlaps = + desired.x < r.pos.x + r.size.x + margin && r.pos.x < desired.x + newSize.x + margin && + desired.y < r.pos.y + r.size.y + margin && r.pos.y < desired.y + newSize.y + margin; + if (overlaps) { + // March right past the blocker; x only grows, so the scan + // terminates once it clears the rightmost overlapping node. + desired.x = r.pos.x + r.size.x + margin; + moved = true; + } + } + } + return desired; +} + void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos) { if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return; @@ -1136,7 +1562,8 @@ void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const Im finalName = baseName + "_" + std::to_string(++suffix); pxr::SdfPath path = m_materialPath.AppendChild(pxr::TfToken(finalName)); - pxr::GfVec2f pos(canvasPos.x, canvasPos.y); + const ImVec2 freePos = FindFreeCanvasSpot(canvasPos, shaderId); + pxr::GfVec2f pos(freePos.x, freePos.y); m_commandHistory->Push(std::make_unique(m_stage, path, shaderId, pos)); SyncFromUsd(); @@ -1183,7 +1610,11 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) { // auto-layout). Only a real drag diverges from it and should author. for (const auto& node : m_graph.nodes) { if (node.path == path) { - if (node.uiPosition == newValue) return; + if (node.uiPosition == newValue) { + LOG_INFO("[NodeGraph] position save for " + path.GetString() + + " ignored (echo of snapshot seed)"); + return; + } break; } } @@ -1193,7 +1624,17 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) { ? oldValueVt.UncheckedGet() : pxr::GfVec2f(0.0f, 0.0f); - if (oldValue == newValue) return; // e.g. redundant Save call right after our own seed + if (oldValue == newValue) { + LOG_INFO("[NodeGraph] position save for " + path.GetString() + + " ignored (customData already matches)"); + return; + } + + char moveMsg[256]; + std::snprintf(moveMsg, sizeof(moveMsg), + "[NodeGraph] move committed: %s (%.1f, %.1f) -> (%.1f, %.1f)", + path.GetText(), oldValue[0], oldValue[1], newValue[0], newValue[1]); + LOG_INFO(std::string(moveMsg)); pxr::UsdStageRefPtr stage = m_stage; m_commandHistory->Push(std::make_unique( diff --git a/src/ui/MaterialEditorPanel.h b/src/ui/MaterialEditorPanel.h index 97632c1..bafe4e5 100644 --- a/src/ui/MaterialEditorPanel.h +++ b/src/ui/MaterialEditorPanel.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace UsdLayerManager { @@ -82,6 +83,11 @@ private: void RenderMaterialBrowser(); /// Creates a uniquely-named empty material under /Materials and opens it. void CreateNewMaterial(); + /// Preset graphs for the open material (one undoable command each): + /// UsdPreviewSurface + UsdUVTextures fed by an st primvar reader, or a + /// MaterialX standard_surface + image nodes fed by a texcoord node. + void CreateUsdPresetGraph(); + void CreateMaterialXPresetGraph(); void RenderNodeGraphCanvas(); void RenderPreviewPanel(); void HandleCreateAndDelete(); @@ -89,6 +95,11 @@ private: /// creates the highlighted node at canvasPos. void RenderNodeSearchMenu(const ImVec2& canvasPos); void CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos); + /// Nudges a creation point right until the new node's estimated rectangle + /// clears every existing node's rendered rectangle. Overlapping nodes + /// fight over the editor's hit test: the buried one becomes unmovable and + /// its pins unhoverable. Must be called with the editor context current. + ImVec2 FindFreeCanvasSpot(ImVec2 desired, const std::string& shaderId) const; /// Binds an existing material to targetPath, undoably, restoring whatever /// direct binding (if any) targetPath had before. void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath); @@ -153,6 +164,15 @@ private: /// is only valid inside the editor's Begin/End). std::string m_pendingCreateShaderId; + /// Debug event-trace state (see the logging block at the end of + /// RenderNodeGraphCanvas): whether the last LMB press landed on a node + /// rect, whether that press's drag-start has been logged yet, and the + /// editor NodeIds hit at press time (their live positions are logged on + /// every drag-move so a node that stops following the mouse is visible). + bool m_debugPressOnNode = false; + bool m_debugDragLogged = false; + std::vector m_debugDragNodeIds; + /// First node currently selected in the graph editor (empty = none); /// drives the properties section under the preview. pxr::SdfPath m_selectedNodePath; diff --git a/src/ui/MaterialPreviewRenderer.cpp b/src/ui/MaterialPreviewRenderer.cpp index 57b6e98..d2e1aaa 100644 --- a/src/ui/MaterialPreviewRenderer.cpp +++ b/src/ui/MaterialPreviewRenderer.cpp @@ -37,17 +37,21 @@ const pxr::SdfPath kDomeLightPath("/Preview/DomeLight"); /// Selectable preview geometry; prim name is a child of kShapesRootPath. /// Shapes backed by an asset file are only created when the file exists. +/// frameFit: camera framing margin — the camera frames the shape's bounding +/// box, and a cube fills its box completely while a sphere only inscribes it, +/// so boxy shapes need a larger margin to read at a similar visual size. struct ShapePreset { const char* label; const char* primName; + double frameFit; }; const ShapePreset kShapePresets[] = { - {"Sphere", "Sphere"}, - {"Cube", "Cube"}, - {"Cylinder", "Cylinder"}, - {"Teapot", "Teapot"}, - {"Hair", "Hair"}, - {"Cloud Volume", "Cloud"}, + {"Sphere", "Sphere", 1.6}, + {"Cube", "Cube", 2.1}, + {"Cylinder", "Cylinder", 1.8}, + {"Teapot", "Teapot", 1.6}, + {"Hair", "Hair", 1.6}, + {"Cloud Volume", "Cloud", 1.6}, }; constexpr int kShapePresetCount = static_cast(sizeof(kShapePresets) / sizeof(kShapePresets[0])); @@ -155,7 +159,8 @@ void MaterialPreviewRenderer::EnsureInitialized() { // ~25° down instead of the dead-on front view (subsequent shape switches // keep whatever orientation the user has orbited to). m_camera.Tumble(-45.0, 25.0); - m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); + m_renderer.SetCameraStateFromGfCamera( + m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true)); m_initialized = true; m_dirty = true; @@ -307,8 +312,9 @@ void MaterialPreviewRenderer::ApplyPreviewShape(int index) { // Shapes differ wildly in size (the cloud VDB spans hundreds of units) — // reframe on the active shape's bounds; inactive prims don't contribute. m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds()); - m_camera.FrameSelection(m_previewBBox, 1.6); // margin so the shape doesn't touch the frame edge - m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); + m_camera.FrameSelection(m_previewBBox, kShapePresets[index].frameFit); + m_renderer.SetCameraStateFromGfCamera( + m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true)); m_dirty = true; } @@ -402,7 +408,8 @@ uint32_t MaterialPreviewRenderer::Render(int width, int height) { void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) { if (!m_initialized) return; m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity - m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); + m_renderer.SetCameraStateFromGfCamera( + m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true)); m_dirty = true; } diff --git a/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Drawing.h b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Drawing.h new file mode 100644 index 0000000..57b0cd1 --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Drawing.h @@ -0,0 +1,12 @@ +# pragma once +#include "imgui.h" + +namespace ax { +namespace Drawing { + +enum class IconType { Flow, Circle, Square, Grid, RoundSquare, Diamond }; + +void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor); + +} // namespace Drawing +} // namespace ax \ No newline at end of file diff --git a/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.h b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.h new file mode 100644 index 0000000..161cac4 --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.h @@ -0,0 +1,1256 @@ +//------------------------------------------------------------------------------ +// 2D friendly math library +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# if !defined(__AX_MATH_2D_H__) +# define __AX_MATH_2D_H__ +# pragma once + + +//------------------------------------------------------------------------------ +# if defined(__cplusplus) + + +//------------------------------------------------------------------------------ +# include +# include +# include +# include +# include + + +//------------------------------------------------------------------------------ +namespace ax { + + +//------------------------------------------------------------------------------ +constexpr float AX_PI = 3.14159265358979323846f; + + +//------------------------------------------------------------------------------ +enum class matrix_order +{ + prepend, + append, + set +}; + + +//------------------------------------------------------------------------------ +template +struct basic_size; + + +//------------------------------------------------------------------------------ +template +struct basic_point +{ + typedef T value_type; + + union + { + struct { T x, y; }; + T values[2]; + }; + + basic_point(): x(0), y(0) {} + basic_point(T x, T y): x(x), y(y) {} + + template + explicit basic_point(const basic_point

& p) { x = static_cast(p.x); y = static_cast(p.y); } + + basic_point(const basic_point&) = default; + basic_point(basic_point&&) = default; + basic_point& operator=(const basic_point&) = default; + basic_point& operator=(basic_point&&) = default; + + template + explicit operator basic_size

() const { return basic_size

(static_cast

(x), static_cast

(y)); } + + friend inline value_type dot(const basic_point& lhs, const basic_point& rhs) { return lhs.x * rhs.x + lhs.y * rhs.y; } + + inline T length_sq() const + { + return dot(*this, *this); + } + + inline T length() const + { + return static_cast(sqrtf(static_cast(length_sq()))); + } + + inline basic_point normalized() const + { + const auto self = static_cast>(*this); + const auto lenght_sq = dot(self, self); + if (lenght_sq == 1) return *this; + if (lenght_sq == 0) return basic_point(0, 0); + const auto inv_length_sq = 1.0f / sqrtf(lenght_sq); + return static_cast(self * inv_length_sq); + } + + inline void normalize() { *this = normalized(); } + + inline basic_point followed(const basic_point& target, T distance) const + { + return *this + static_cast(static_cast>(target - *this).normalized() * static_cast(distance)); + } + + inline void follow(const basic_point& target, T distance) + { + *this = followed(target, distance); + } + + inline basic_point cwise_min(const basic_point& rhs) const { return basic_point(std::min(x, rhs.x), std::min(y, rhs.y)); } + inline basic_point cwise_max(const basic_point& rhs) const { return basic_point(std::max(x, rhs.x), std::max(y, rhs.y)); } + inline basic_point cwise_product(const basic_point& rhs) const { return basic_point(x * rhs.x, y * rhs.y); } + inline basic_point cwise_quotient(const basic_point& rhs) const { return basic_point(x / rhs.x, y / rhs.y); } + inline basic_point cwise_safe_quotient(const basic_point& rhs, const basic_point& alt = basic_point()) const { return basic_point(rhs.x ? x / rhs.x : alt.x, rhs.y ? y / rhs.y : alt.x); } + inline basic_point cwise_abs() const { return basic_point(fabsf(x), fabsf(y)); } + inline basic_point cwise_sqrt() const { return basic_point(sqrtf(x), sqrtf(y)); } + inline basic_point cwise_floor() const { return basic_point(floorf(x), floorf(y)); } + inline basic_point cwise_ceil() const { return basic_point(ceilf(x), ceilf(y)); } + inline basic_point cwise_round() const { return basic_point(roundf(x), roundf(y)); } + + friend inline bool operator == (const basic_point& lhs, const basic_point& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } + friend inline bool operator != (const basic_point& lhs, const basic_point& rhs) { return !(lhs == rhs); } + + T& operator[](size_t i) { return values[i]; } + const T& operator[](size_t i) const { return values[i]; } + + friend inline basic_point operator + (const basic_point& lhs) { return lhs; } + friend inline basic_point operator - (const basic_point& lhs) { return basic_point(-lhs.x, -lhs.y); } + + friend inline basic_point operator + (const basic_point& lhs, const basic_point& rhs) { return basic_point(lhs.x + rhs.x, lhs.y + rhs.y); } + friend inline basic_point operator - (const basic_point& lhs, const basic_point& rhs) { return basic_point(lhs.x - rhs.x, lhs.y - rhs.y); } + + template friend basic_point operator + (const basic_size

& lhs, const basic_point& rhs); + template friend basic_point operator - (const basic_point& lhs, const basic_size

& rhs); + + friend inline basic_point operator * (T lhs, const basic_point& rhs) { return basic_point(lhs * rhs.x, lhs * rhs.y); } + friend inline basic_point operator * (const basic_point& lhs, T rhs) { return basic_point(lhs.x * rhs, lhs.y * rhs); } + + basic_point& operator += (const basic_point& rhs) { *this = *this + rhs; return *this; } + basic_point& operator -= (const basic_point& rhs) { *this = *this - rhs; return *this; } +}; + +typedef basic_point point; +typedef basic_point pointf; + + +//------------------------------------------------------------------------------ +template +struct basic_size +{ + T w, h; + + basic_size(): w(0), h(0) {} + basic_size(T w, T h): w(w), h(h) {} + + template + explicit basic_size(const basic_size

& p) { w = static_cast(p.w); h = static_cast(p.h); } + + basic_size(const basic_size&) = default; + basic_size(basic_size&&) = default; + basic_size& operator=(const basic_size&) = default; + basic_size& operator=(basic_size&&) = default; + + template + explicit operator basic_point

() const { return basic_point

(static_cast

(w), static_cast

(h)); } + + friend inline bool operator == (const basic_size& lhs, const basic_size& rhs) { return lhs.w == rhs.w && lhs.h == rhs.h; } + friend inline bool operator != (const basic_size& lhs, const basic_size& rhs) { return !(lhs == rhs); } + + bool is_empty() const { return w <= 0 || h <= 0; } +}; + +typedef basic_size size; +typedef basic_size sizef; + + +//------------------------------------------------------------------------------ +template +struct basic_line +{ + typedef basic_point point_t; + + union + { + struct { point_t a; point_t b; }; + struct { T x0, y0, x1, y1; }; + }; + + basic_line(): x0(0), y0(0), x1(0), y1(0) {} + basic_line(const point_t& a, const point_t& b): a(a), b(b) {} + basic_line(const point_t& p, const size_t& s): a(p), b(p + s) {} + basic_line(T x0, T y0, T x1, T y1): x0(x0), y0(y0), x1(x1), y1(y1) {} + + template + explicit basic_line(const basic_line

& l) + { + a = static_cast>(l.a); + b = static_cast>(l.b); + } + + basic_line(const basic_line&) = default; + basic_line(basic_line&&) = default; + basic_line& operator=(const basic_line&) = default; + basic_line& operator=(basic_line&&) = default; + + template + explicit operator basic_line

() const + { + return basic_line

( + static_cast>(this->l.a), + static_cast>(this->l.b)); + } +}; + +typedef basic_line line; +typedef basic_line line_f; + + +//------------------------------------------------------------------------------ +enum class rect_region +{ + center = 0x00000, + top = 0x00001, + bottom = 0x00002, + left = 0x00004, + right = 0x00008, + top_left = top | left, + top_right = top | right, + bottom_left = bottom | left, + bottom_right = bottom | right, +}; + +inline rect_region operator |(rect_region lhs, rect_region rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } +inline rect_region operator &(rect_region lhs, rect_region rhs) { return static_cast(static_cast(lhs) & static_cast(rhs)); } + + +//------------------------------------------------------------------------------ +template +struct basic_rect +{ + typedef basic_point point_t; + typedef basic_size size_t; + + union + { + struct { point_t location; size_t size; }; + struct { T x, y, w, h; }; + }; + + basic_rect(): x(0), y(0), w(0), h(0) {} + basic_rect(const point_t& tl, const point_t& br): location(tl), size(br.x - tl.x, br.y - tl.y) {} + basic_rect(const point_t& l, const size_t& s): location(l), size(s) {} + basic_rect(T x, T y, T w, T h): x(x), y(y), w(w), h(h) {} + + template + explicit basic_rect(const basic_rect

& r) + { + const auto tl = static_cast>(r.top_left()); + const auto br = static_cast>(r.bottom_right()); + + location = tl; + size = size_t(br - tl); + } + + basic_rect(const basic_rect&) = default; + basic_rect(basic_rect&&) = default; + basic_rect& operator=(const basic_rect&) = default; + basic_rect& operator=(basic_rect&&) = default; + + template + explicit operator basic_rect

() const + { + return basic_rect

( + static_cast>(top_left()), + static_cast>(bottom_right())); + } + + friend inline bool operator == (const basic_rect& lhs, const basic_rect& rhs) { return lhs.location == rhs.location && lhs.size == rhs.size; } + friend inline bool operator != (const basic_rect& lhs, const basic_rect& rhs) { return !(lhs == rhs); } + + point_t top_left() const { return point_t(x, y); } + point_t top_right() const { return point_t(x + w, y); } + point_t bottom_left() const { return point_t(x, y + h); } + point_t bottom_right() const { return point_t(x + w, y + h); } + + T left() const { return x; } + T right() const { return x + w; } + T top() const { return y; } + T bottom() const { return y + h; } + + point_t center() const { return point_t(center_x(), center_y()); } + T center_x() const { return x + w / 2; } + T center_y() const { return y + h / 2; } + + pointf centerf() const { return pointf(centerf_x(), centerf_y()); } + float centerf_x() const { return x + w / 2.0f; } + float centerf_y() const { return y + h / 2.0f; } + + bool is_empty() const { return size.is_empty(); } + + template + bool contains(const basic_point

& p) const + { + return p.x >= x && p.y >= y && p.x < right() && p.y < bottom(); + } + + template + bool contains(const basic_rect

& r) const + { + return r.x >= x && r.y >= y && r.right() <= right() && r.bottom() <= bottom(); + } + + template + bool intersects(const basic_rect

& r) const + { + return left() < r.right() && right() > r.left() && top() < r.bottom() && bottom() > r.top(); + } + + template + void expand(P extent) + { + expand(extent, extent); + } + + template + void expand(P hotizontal, P vertical) + { + expand(hotizontal, vertical, hotizontal, vertical); + } + + template + void expand(P left, P top, P right, P bottom) + { + x -= left; + y -= top; + w += left + right; + h += top + bottom; + } + + template + basic_rect expand(P hotizontal, P vertical) const + { + auto result = basic_rect(*this); + result.expand(hotizontal, vertical); + return result; + } + + template + basic_rect expand(P left, P top, P right, P bottom) const + { + auto result = basic_rect(*this); + result.expand(left, top, right, bottom); + return result; + } + + template + basic_rect expanded(P extent) const + { + auto result = basic_rect(*this); + result.expand(extent); + return result; + } + + template + basic_rect expanded(P hotizontal, P vertical) const + { + auto result = basic_rect(*this); + result.expand(hotizontal, vertical); + return result; + } + + template + basic_rect expanded(P left, P top, P right, P bottom) const + { + auto result = basic_rect(*this); + result.expand(left, top, right, bottom); + return result; + } + + point_t get_closest_point(const point_t& p, bool on_edge) const + { + if (!on_edge && contains(p)) + return p; + + return point_t( + (p.x > right()) ? right() : (p.x < left() ? left() : p.x), + (p.y > bottom()) ? bottom() : (p.y < top() ? top() : p.y)); + } + + point_t get_closest_point(const point_t& p, bool on_edge, float radius) const; + + point_t get_closest_point(const basic_rect& r) const; + point_t get_closest_point_hollow(const point_t& p, T rounding = 0, rect_region* region = nullptr) const; + + basic_line get_closest_line(const basic_rect& r) const; + basic_line get_closest_line(const basic_rect& r, T radius) const; + basic_line get_closest_line(const basic_rect& r, T radius_a, T radius_b) const; +}; + +template +static inline basic_rect make_intersection(const basic_rect& lhs, const basic_rect& rhs) +{ + if (lhs.is_empty()) + return lhs; + else if (rhs.is_empty()) + return rhs; + + const auto tl = lhs.top_left().cwise_max(rhs.top_left()); + const auto br = lhs.bottom_right().cwise_min(rhs.bottom_right()); + + return basic_rect(tl, br); +} + +template +static inline basic_rect make_union(const basic_rect& lhs, const basic_rect& rhs) +{ + if (lhs.is_empty()) + return rhs; + else if (rhs.is_empty()) + return lhs; + + const auto tl = lhs.top_left().cwise_min(rhs.top_left()); + const auto br = lhs.bottom_right().cwise_max(rhs.bottom_right()); + + return basic_rect(tl, br); +} + +template +static inline basic_rect make_union(const basic_rect& lhs, const basic_point& p) +{ + if (lhs.is_empty()) + return basic_rect(p, p); + + const auto tl = lhs.top_left().cwise_min(p); + const auto br = lhs.bottom_right().cwise_max(p); + + return basic_rect(tl, br); +} + +typedef basic_rect rect; +typedef basic_rect rectf; + + +//------------------------------------------------------------------------------ +struct matrix +{ + float m11, m12, m21, m22, m31, m32; + + matrix(): m11(1), m12(0), m21(0), m22(1), m31(0), m32(0) {} + matrix(float m11, float m12, float m21, float m22, float m31, float m32): + m11(m11), m12(m12), m21(m21), m22(m22), m31(m31), m32(m32) {} + + void zero(); + void reset(); + + bool invert(); + + void translate(float x, float y, matrix_order order = matrix_order::prepend); + void rotate(float angle, matrix_order order = matrix_order::prepend); + void rotate_at(float angle, float cx, float cy, matrix_order order = matrix_order::prepend); + void scale(float x, float y, matrix_order order = matrix_order::prepend); + void shear(float x, float y, matrix_order order = matrix_order::prepend); + + void combine(const matrix& matrix, matrix_order order = matrix_order::prepend); + + matrix inverted() const; +}; + +struct matrix4 +{ + float m11, m12, m13, m14; + float m21, m22, m23, m24; + float m31, m32, m33, m34; + float m41, m42, m43, m44; + + matrix4(): + m11(1), m12(0), m13(0), m14(0), + m21(0), m22(1), m23(0), m24(0), + m31(0), m32(0), m33(1), m34(0), + m41(0), m42(0), m43(0), m44(1) + { + } + + matrix4( + float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44): + m11(m11), m12(m12), m13(m13), m14(m14), + m21(m21), m22(m22), m23(m23), m24(m24), + m31(m31), m32(m32), m33(m33), m34(m34), + m41(m41), m42(m42), m43(m43), m44(m44) + { + } + + matrix4(const matrix& m): + m11(m.m11), m12(m.m12), m13(0.0f), m14(0.0f), + m21(m.m21), m22(m.m22), m23(0.0f), m24(0.0f), + m31(0.0f), m32(0.0f), m33(1.0f), m34(0.0f), + m41(m.m31), m42(m.m32), m43(0.0f), m44(1.0f) + { + } + + void zero(); + void reset(); + + bool invert(); + void transpose(); + + void translate(float x, float y, float z, matrix_order order = matrix_order::prepend); + void rotate_x(float angle, matrix_order order = matrix_order::prepend); + void rotate_y(float angle, matrix_order order = matrix_order::prepend); + void rotate_z(float angle, matrix_order order = matrix_order::prepend); + void rotate_axis(float angle, float x, float y, float z, matrix_order order = matrix_order::prepend); + void scale(float x, float y, float z, matrix_order order = matrix_order::prepend); + + void combine(const matrix4& matrix, matrix_order order = matrix_order::prepend); + + matrix4 inverted() const; + matrix4 transposed() const; +}; + + +//------------------------------------------------------------------------------ +namespace detail { +template void transform_points(const M& matrix, basic_point* points, size_t count); +template void transform_vectors(const M& matrix, basic_point* points, size_t count); +} // namespace detail + + +//------------------------------------------------------------------------------ +template +inline basic_point transformed(const basic_point& point, const M& matrix) +{ + auto result = point; + detail::transform_points(matrix, &result, 1); + return result; +} + +template +inline void transform(basic_point& point, const M& matrix) +{ + detail::transform_points(matrix, &point, 1); +} + +template +inline void transform(basic_point (&point)[N], const M& matrix) +{ + detail::transform_points(matrix, point, N); +} + +template +inline void transform(basic_point* point, size_t n, const M& matrix) +{ + detail::transform_points(matrix, point, n); +} + +template +inline basic_point transformed_v(const basic_point& vector, const M& matrix) +{ + auto result = vector; + detail::transform_vectors(matrix, &result, 1); + return result; +} + +template +inline void transform_v(basic_point& point, const M& matrix) +{ + detail::transform_vectors(matrix, &point, 1); +} + +template +inline void transform_v(basic_point(&point)[N], const M& matrix) +{ + detail::transform_vectors(matrix, point, N); +} + +template +inline void transform_v(basic_point* point, size_t n, const M& matrix) +{ + detail::transform_vectors(matrix, point, n); +} + + +//------------------------------------------------------------------------------ +template +inline P linear_bezier(const P& p0, const P& p1, T t) +{ + return p0 + t * (p1 - p0); +} + +template +inline P linear_bezier_dt(const P& p0, const P& p1, T t) +{ + return p1 - p0; +} + + +//------------------------------------------------------------------------------ +template +inline P quadratic_bezier(const P& p0, const P& p1, const P& p2, T t) +{ + const auto a = 1 - t; + + return a * a * p0 + 2 * t * a * p1 + t * t * p2; +} + +template +inline P quadratic_bezier_dt(const P& p0, const P& p1, const P& p2, T t) +{ + return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1); +} + + +//------------------------------------------------------------------------------ +template +inline P cubic_bezier(const P& p0, const P& p1, const P& p2, const P& p3, T t) +{ + const auto a = 1 - t; + const auto b = a * a * a; + const auto c = t * t * t; + + return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3; +} + +template +inline P cubic_bezier_dt(const P& p0, const P& p1, const P& p2, const P& p3, T t) +{ + const auto a = 1 - t; + const auto b = a * a; + const auto c = t * t; + const auto d = 2 * t * a; + + return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c; +} + + +//------------------------------------------------------------------------------ +struct cubic_bezier_t +{ + pointf p0; + pointf p1; + pointf p2; + pointf p3; + + inline pointf sample(float t) const + { + const auto cp0_zero = (p1 - p0).length_sq() < 0.0001f; + const auto cp1_zero = (p3 - p2).length_sq() < 0.0001f; + + if (cp0_zero && cp1_zero) + return linear_bezier(p0, p3, t); + else if (cp0_zero) + return quadratic_bezier(p0, p2, p3, t); + else if (cp1_zero) + return quadratic_bezier(p0, p1, p3, t); + else + return cubic_bezier(p0, p1, p2, p3, t); + } + + inline pointf tangent(float t) const + { + const auto cp0_zero = (p1 - p0).length_sq() < 0.0001f; + const auto cp1_zero = (p3 - p2).length_sq() < 0.0001f; + + if (cp0_zero && cp1_zero) + return linear_bezier_dt(p0, p3, t); + else if (cp0_zero) + return quadratic_bezier_dt(p0, p2, p3, t); + else if (cp1_zero) + return quadratic_bezier_dt(p0, p1, p3, t); + else + return cubic_bezier_dt(p0, p1, p2, p3, t); + } + + inline pointf normal(float t) const + { + const auto tangent = this->tangent(t); + return pointf(-tangent.y, tangent.x); + } +}; + +struct cubic_bezier_split_t +{ + cubic_bezier_t left; + cubic_bezier_t right; +}; + +struct cubic_bezier_project_result +{ + float position; + float distance; +}; + +inline cubic_bezier_project_result cubic_bezier_project_point(const pointf& f, const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, const int subdivisions = 100) +{ + // http://pomax.github.io/bezierinfo/#projections + + const float epsilon = 1e-6f; + const float fixed_step = 1.0f / static_cast(subdivisions - 1); + + float distance = std::numeric_limits::max(); + float position = 0.0f; + + // Step 1: Coarse check + for (int i = 0; i < subdivisions; ++i) + { + auto t = i * fixed_step; + auto p = cubic_bezier(p0, p1, p2, p3, t); + auto s = f - p; + auto d = dot(s, s); + + if (d < distance) + { + distance = d; + position = t; + } + } + + if (position == 0.0f || fabsf(position - 1.0f) <= epsilon) + return cubic_bezier_project_result{position, sqrtf(distance)}; + + // Step 2: Fine check + auto left = position - fixed_step; + auto right = position + fixed_step; + auto step = fixed_step * 0.1f; + + for (auto t = left; t < right + step; t += step) + { + auto p = cubic_bezier(p0, p1, p2, p3, t); + auto s = f - p; + auto d = dot(s, s); + + if (d < distance) + { + distance = d; + position = t; + } + } + + return cubic_bezier_project_result{ position, sqrtf(distance) }; +} + +inline int cubic_bezier_line_intersect(const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, const pointf& a0, const pointf& a1, pointf results[3]) +{ + auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int + { + int count = 0; + + auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; }; + + auto A = b / a; + auto B = c / a; + auto C = d / a; + + auto Q = (3 * B - powf(A, 2)) / 9; + auto R = (9 * A * B - 27 * C - 2 * powf(A, 3)) / 54; + auto D = powf(Q, 3) + powf(R, 2); // polynomial discriminant + + if (D >= 0) // complex or duplicate roots + { + auto S = sign(R + sqrtf(D)) * powf(fabsf(R + sqrtf(D)), (1.0f / 3.0f)); + auto T = sign(R - sqrtf(D)) * powf(fabsf(R - sqrtf(D)), (1.0f / 3.0f)); + + roots[0] = -A / 3 + (S + T); // real root + roots[1] = -A / 3 - (S + T) / 2; // real part of complex root + roots[2] = -A / 3 - (S + T) / 2; // real part of complex root + auto Im = fabsf(sqrtf(3) * (S - T) / 2); // complex part of root pair + + // discard complex roots + if (Im != 0) + count = 1; + else + count = 3; + } + else // distinct real roots + { + auto th = acosf(R / sqrtf(-powf(Q, 3))); + + roots[0] = 2 * sqrtf(-Q) * cosf(th / 3) - A / 3; + roots[1] = 2 * sqrtf(-Q) * cosf((th + 2 * AX_PI) / 3) - A / 3; + roots[2] = 2 * sqrtf(-Q) * cosf((th + 4 * AX_PI) / 3) - A / 3; + + count = 3; + } + + return count; + }; + + // https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as + // + // Start with Bezier using Bernstein polynomials for weighting functions: + // (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3 + // + // Expand and collect terms to form linear combinations of original Bezier + // controls. This ends up with a vector cubic in t: + // (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0 + // /\ /\ /\ /\ + // || || || || + // c3 c2 c1 c0 + + // Calculate the coefficients + auto c3 = -p0 + 3 * p1 - 3 * p2 + p3; + auto c2 = 3 * p0 - 6 * p1 + 3 * p2; + auto c1 = -3 * p0 + 3 * p1; + auto c0 = p0; + + // Convert line to normal form: ax + by + c = 0 + auto a = a1.y - a0.y; + auto b = a0.x - a1.x; + auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x); + + // Rotate each cubic coefficient using line for new coordinate system? + // Find roots of rotated cubic + float roots[3]; + auto rootCount = cubic_roots( + a * c3.x + b * c3.y, + a * c2.x + b * c2.y, + a * c1.x + b * c1.y, + a * c0.x + b * c0.y + c, + roots); + + // Any roots in closed interval [0,1] are intersections on Bezier, but + // might not be on the line segment. + // Find intersections and calculate point coordinates + + auto min = a0.cwise_min(a1); + auto max = a0.cwise_max(a1); + + auto result = results; + for (int i = 0; i < rootCount; ++i) + { + auto root = roots[i]; + + if (0 <= root && root <= 1) + { + // We're within the Bezier curve + // Find point on Bezier + auto p = cubic_bezier(p0, p1, p2, p3, root); + + // See if point is on line segment + // Had to make special cases for vertical and horizontal lines due + // to slight errors in calculation of p00 + if (a0.x == a1.x) + { + if (min.y <= p.y && p.y <= max.y) + *result++ = p; + } + else if (a0.y == a1.y) + { + if (min.x <= p.x && p.x <= max.x) + *result++ = p; + } + else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y) + { + *result++ = p; + } + } + } + + return static_cast(result - results); +} + +inline rectf cubic_bezier_bounding_rect(const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3) +{ + auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0; + auto b = 6 * p0 - 12 * p1 + 6 * p2; + auto c = 3 * p1 - 3 * p0; + auto delta_squared = b.cwise_product(b) - 4 * a.cwise_product(c); + + auto tl = p0.cwise_min(p3); + auto rb = p0.cwise_max(p3); + + for (int i = 0; i < 2; ++i) + { + if (delta_squared[i] >= 0) + { + auto delta = sqrtf(delta_squared[i]); + + auto t0 = (-b[i] + delta) / (2 * a[i]); + if (t0 > 0 && t0 < 1) + { + auto p = cubic_bezier(p0[i], p1[i], p2[i], p3[i], t0); + tl[i] = std::min(tl[i], p); + rb[i] = std::max(rb[i], p); + } + + auto t1 = (-b[i] - delta) / (2 * a[i]); + if (t1 > 0 && t1 < 1) + { + auto p = cubic_bezier(p0[i], p1[i], p2[i], p3[i], t1); + tl[i] = std::min(tl[i], p); + rb[i] = std::max(rb[i], p); + } + } + } + + return rectf(tl, rb); +} + +inline float cubic_bezier_length(const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3) +{ + // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + static const float t_values[] = + { + -0.0640568928626056260850430826247450385909f, + 0.0640568928626056260850430826247450385909f, + -0.1911188674736163091586398207570696318404f, + 0.1911188674736163091586398207570696318404f, + -0.3150426796961633743867932913198102407864f, + 0.3150426796961633743867932913198102407864f, + -0.4337935076260451384870842319133497124524f, + 0.4337935076260451384870842319133497124524f, + -0.5454214713888395356583756172183723700107f, + 0.5454214713888395356583756172183723700107f, + -0.6480936519369755692524957869107476266696f, + 0.6480936519369755692524957869107476266696f, + -0.7401241915785543642438281030999784255232f, + 0.7401241915785543642438281030999784255232f, + -0.8200019859739029219539498726697452080761f, + 0.8200019859739029219539498726697452080761f, + -0.8864155270044010342131543419821967550873f, + 0.8864155270044010342131543419821967550873f, + -0.9382745520027327585236490017087214496548f, + 0.9382745520027327585236490017087214496548f, + -0.9747285559713094981983919930081690617411f, + 0.9747285559713094981983919930081690617411f, + -0.9951872199970213601799974097007368118745f, + 0.9951872199970213601799974097007368118745f + }; + + // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) + static const float c_values[] = + { + 0.1279381953467521569740561652246953718517f, + 0.1279381953467521569740561652246953718517f, + 0.1258374563468282961213753825111836887264f, + 0.1258374563468282961213753825111836887264f, + 0.1216704729278033912044631534762624256070f, + 0.1216704729278033912044631534762624256070f, + 0.1155056680537256013533444839067835598622f, + 0.1155056680537256013533444839067835598622f, + 0.1074442701159656347825773424466062227946f, + 0.1074442701159656347825773424466062227946f, + 0.0976186521041138882698806644642471544279f, + 0.0976186521041138882698806644642471544279f, + 0.0861901615319532759171852029837426671850f, + 0.0861901615319532759171852029837426671850f, + 0.0733464814110803057340336152531165181193f, + 0.0733464814110803057340336152531165181193f, + 0.0592985849154367807463677585001085845412f, + 0.0592985849154367807463677585001085845412f, + 0.0442774388174198061686027482113382288593f, + 0.0442774388174198061686027482113382288593f, + 0.0285313886289336631813078159518782864491f, + 0.0285313886289336631813078159518782864491f, + 0.0123412297999871995468056670700372915759f, + 0.0123412297999871995468056670700372915759f + }; + + static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), ""); + + auto arc = [p0, p1, p2, p3](float t) + { + const auto p = cubic_bezier_dt(p0, p1, p2, p3, t); + const auto l = p.x * p.x + p.y * p.y; + return sqrtf(l); + }; + + const auto z = 0.5f; + const auto n = sizeof(t_values) / sizeof(*t_values); + + auto accumulator = 0.0f; + for (size_t i = 0; i < n; ++i) + { + const auto t = z * t_values[i] + z; + accumulator += c_values[i] * arc(t); + } + + return z * accumulator; +} + +inline cubic_bezier_split_t cubic_bezier_split(const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float t) +{ + const auto z1 = t; + const auto z2 = z1 * z1; + const auto z3 = z1 * z1 * z1; + const auto s1 = z1 - 1; + const auto s2 = s1 * s1; + const auto s3 = s1 * s1 * s1; + + return cubic_bezier_split_t + { + cubic_bezier_t + { + p0, + z1 * p1 - s1 * p0, + z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0, + z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0 + }, + cubic_bezier_t + { + z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3, + z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3, + z1 * p2 - s1 * p3, + p3, + } + }; +}; + +struct bezier_fixed_step_result_t +{ + float t; + float length; + pointf point; + bool break_search; + + bezier_fixed_step_result_t(): t(0), length(0), point(), break_search(false) {} +}; + +typedef void(*bezier_fixed_step_callback_t)(bezier_fixed_step_result_t& result, void* user_pointer); + +inline void cubic_bezier_fixed_step(bezier_fixed_step_callback_t callback, void* user_pointer, const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f) +{ + if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0) + return; + + bezier_fixed_step_result_t result; + result.point = p0; + + callback(result, user_pointer); + if (result.break_search) + return; + + const auto length = cubic_bezier_length(p0, p1, p2, p3); + const auto point_count = static_cast(length / step) + (overshoot ? 2 : 1); + const auto t_min = 0.0f; + const auto t_max = step * point_count / length; + const auto t_0 = (t_min + t_max) * 0.5f; + + std::map cache; + for (int point_index = 1; point_index < point_count; ++point_index) + { + const auto targetLength = point_index * step; + + float t_start = t_min; + float t_end = t_max; + float t = t_0; + + float t_best = t; + float error_best = length; + + while (true) + { + auto cacheIt = cache.find(t); + if (cacheIt == cache.end()) + { + const auto front = cubic_bezier_split(p0, p1, p2, p3, t).left; + const auto length = cubic_bezier_length(front.p0, front.p1, front.p2, front.p3); + + cacheIt = cache.emplace(t, length).first; + } + + const auto length = cacheIt->second; + const auto error = targetLength - length; + + if (error < error_best) + { + error_best = error; + t_best = t; + } + + if (fabsf(error) <= max_value_error || fabsf(t_start - t_end) <= max_t_error) + { + result.t = t; + result.length = length; + result.point = cubic_bezier(p0, p1, p2, p3, t); + + callback(result, user_pointer); + if (result.break_search) + return; + + break; + } + else if (error < 0.0f) + t_end = t; + else // if (error > 0.0f) + t_start = t; + + t = (t_start + t_end) * 0.5f; + } + } +} + +template +inline void cubic_bezier_fixed_step(F& callback, const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f) +{ + auto wrapper = [](bezier_fixed_step_result_t& result, void* user_pointer) + { + F& callback = *reinterpret_cast(user_pointer); + callback(result); + }; + + cubic_bezier_fixed_step(wrapper, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); +} + +inline void cubic_bezier_fixed_step(std::vector& points, const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f) +{ + points.resize(0); + + auto callback = [](bezier_fixed_step_result_t& result, void* user_pointer) + { + auto& points = *reinterpret_cast*>(user_pointer); + points.push_back(result.point); + }; + + cubic_bezier_fixed_step(callback, &points, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); +} + +inline std::vector cubic_bezier_fixed_step(const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f) +{ + std::vector result; + cubic_bezier_fixed_step(result, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); + return result; +} + +enum bezier_subdivide_flags_t +{ + bezier_subdivide_flags_none = 0, + bezier_subdivide_flags_skip_first = 1 +}; + +struct bezier_subdivide_result_t +{ + pointf point; + pointf tangent; +}; + +typedef void(*bezier_subdivide_callback_t)(const bezier_subdivide_result_t& p, void* user_pointer); + +namespace detail { +struct bezier_subdivide_context_impl_t: bezier_subdivide_result_t +{ + bezier_subdivide_callback_t callback; + void* user_pointer; + float tesselation_tollerance; + bezier_subdivide_flags_t flags; + + void commit(const pointf& p, const pointf& t) + { + point = p; + tangent = t; + callback(*this, user_pointer); + } +}; + +inline void cubic_bezier_subdivide_impl(bezier_subdivide_context_impl_t& context, const cubic_bezier_t& curve, int level = 0) +{ + float dx = curve.p3.x - curve.p0.x; + float dy = curve.p3.y - curve.p0.y; + float d2 = ((curve.p1.x - curve.p3.x) * dy - (curve.p1.y - curve.p3.y) * dx); + float d3 = ((curve.p2.x - curve.p3.x) * dy - (curve.p2.y - curve.p3.y) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < context.tesselation_tollerance * (dx * dx + dy * dy)) + { + context.commit(curve.p3, curve.tangent(1.0f)); + } + else if (level < 10) + { + const auto p12 = (curve.p0 + curve.p1) * 0.5f; + const auto p23 = (curve.p1 + curve.p2) * 0.5f; + const auto p34 = (curve.p2 + curve.p3) * 0.5f; + const auto p123 = (p12 + p23) * 0.5f; + const auto p234 = (p23 + p34) * 0.5f; + const auto p1234 = (p123 + p234) * 0.5f; + + cubic_bezier_subdivide_impl(context, cubic_bezier_t { curve.p0, p12, p123, p1234 }, level + 1); + cubic_bezier_subdivide_impl(context, cubic_bezier_t { p1234, p234, p34, curve.p3 }, level + 1); + } +} +} // namespace detail + +inline void cubic_bezier_subdivide(bezier_subdivide_callback_t callback, void* user_pointer, const cubic_bezier_t& curve, float tess_tol = -1.0f, bezier_subdivide_flags_t flags = bezier_subdivide_flags_none) +{ + if (tess_tol < 0) + tess_tol = 1.118f; // sqrtf(1.25f) + + detail::bezier_subdivide_context_impl_t context; + context.callback = callback; + context.user_pointer = user_pointer; + context.tesselation_tollerance = tess_tol * tess_tol; + context.flags = flags; + + if (!(context.flags & bezier_subdivide_flags_skip_first)) + context.commit(curve.p0, curve.tangent(0)); + + detail::cubic_bezier_subdivide_impl(context, curve, 0); +} + +template +inline void cubic_bezier_subdivide(F& callback, const cubic_bezier_t& curve, float tess_tol = -1.0f, bezier_subdivide_flags_t flags = bezier_subdivide_flags_none) +{ + auto wrapper = [](const bezier_subdivide_result_t& r, void* user_pointer) + { + F& callback = *reinterpret_cast(user_pointer); + callback(r); + }; + + cubic_bezier_subdivide(wrapper, &callback, curve, tess_tol, flags); +} + +template +inline void cubic_bezier_subdivide(F& callback, const pointf& p0, const pointf& p1, const pointf& p2, const pointf& p3, float tess_tol = -1.0f, bezier_subdivide_flags_t flags = bezier_subdivide_flags_none) +{ + cubic_bezier_subdivide(callback, cubic_bezier_t { p0, p1, p2, p3 }, tess_tol, flags); +} + + +//------------------------------------------------------------------------------ +namespace easing { + + +//------------------------------------------------------------------------------ +// http://gizma.com/easing/#quint2 +// +// t - current time +// b - start value +// c - change in value + +template +inline V ease_out_quad(V b, V c, T t) +{ + return b - c * (t * (t - 2)); +} + + +//------------------------------------------------------------------------------ +} // namespace easing + + +//------------------------------------------------------------------------------ +} // namespace ax + + +//------------------------------------------------------------------------------ +# endif // defined(__cplusplus) + + +//------------------------------------------------------------------------------ +# include "Math2D.inl" + + +//------------------------------------------------------------------------------ +# endif // __AX_MATH_2D_H__ diff --git a/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.inl b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.inl new file mode 100644 index 0000000..47bc328 --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Math2D.inl @@ -0,0 +1,642 @@ +//------------------------------------------------------------------------------ +// Math 2D +//------------------------------------------------------------------------------ +# if !defined(__AX_MATH_2D_INL__) +# define __AX_MATH_2D_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# if defined(__cplusplus) + + +//------------------------------------------------------------------------------ +# include +# include + + +//------------------------------------------------------------------------------ +template +inline ax::basic_point operator + (const ax::basic_size

& lhs, const ax::basic_point& rhs) +{ + return static_cast>(lhs) + rhs; +} + +template +inline ax::basic_point operator + (const ax::basic_point& lhs, const ax::basic_size

& rhs) +{ + return lhs + static_cast>(rhs); +} + + +//------------------------------------------------------------------------------ +template +inline typename ax::basic_rect::point_t ax::basic_rect::get_closest_point(const point_t& p, bool on_edge, float radius) const +{ + auto point = get_closest_point(p, on_edge); + + const auto offset = p - point; + const auto distance_sq = offset.x * offset.x + offset.y * offset.y; + if (distance_sq <= 0) + return point; + + const auto distance = sqrtf(static_cast(distance_sq)); + + return point + static_cast>(std::min(distance, static_cast(radius)) * static_cast>(offset) * (1.0f / distance)); +} + +template +inline typename ax::basic_rect::point_t ax::basic_rect::get_closest_point(const basic_rect& r) const +{ + point_t result; + if (r.left() >= right()) + result.x = right(); + else if (r.right() <= left()) + result.x = left(); + else + result.x = (std::max(left(), r.left()) + std::min(right(), r.right())) / 2; + + if (r.top() >= bottom()) + result.y = bottom(); + else if (r.bottom() <= top()) + result.y = top(); + else + result.y = (std::max(top(), r.top()) + std::min(bottom(), r.bottom())) / 2; + + return result; +} + +template +inline typename ax::basic_rect::point_t ax::basic_rect::get_closest_point_hollow(const point_t& p, T rounding, rect_region* region) const +{ + rounding = std::min(rounding, std::min(w, h) / 2); + + const auto inner = expanded(-rounding); + + if (p.x <= inner.left() && p.y <= inner.top()) + { + if (region) *region = rect_region::top_left; + return inner.top_left().followed(p + point_t(-1, -1), rounding); + } + else if (p.x >= inner.right() && p.y <= inner.top()) + { + if (region) *region = rect_region::top_right; + return inner.top_right().followed(p + point_t(1, -1), rounding); + } + else if (p.x <= inner.left() && p.y >= inner.bottom()) + { + if (region) *region = rect_region::bottom_left; + return inner.bottom_left().followed(p + point_t(-1, 1), rounding); + } + else if (p.x >= inner.right() && p.y >= inner.bottom()) + { + if (region) *region = rect_region::bottom_right; + return inner.bottom_right().followed(p + point_t(1, 1), rounding); + } + else + { + if (contains(p) || region) + { + const auto l_diff = abs(p.x - left()); + const auto r_diff = abs(p.x - right()); + const auto t_diff = abs(p.y - top()); + const auto b_diff = abs(p.y - bottom()); + + if (l_diff <= r_diff && l_diff <= t_diff && l_diff <= b_diff) + { + if (region) *region = rect_region::left; + return point_t(left(), p.y); + } + else if (r_diff <= l_diff && r_diff <= t_diff && r_diff <= b_diff) + { + if (region) *region = rect_region::right; + return point_t(right(), p.y); + } + else if (t_diff <= l_diff && t_diff <= r_diff && t_diff <= b_diff) + { + if (region) *region = rect_region::top; + return point_t(p.x, top()); + } + else + { + if (region) *region = rect_region::bottom; + return point_t(p.x, bottom()); + } + } + else + return get_closest_point(p, true); + } +} + +template +inline ax::basic_line ax::basic_rect::get_closest_line(const basic_rect& r) const +{ + auto a = get_closest_point(r); + auto b = r.get_closest_point(*this); + + auto distribute = [](T& a, T& b, T a0, T a1, T b0, T b1) + { + if (a0 >= b1 || a1 <= b0) + return; + + const auto aw = a1 - a0; + const auto bw = b1 - b0; + + if (aw > bw) + { + b = b0 + bw - bw * (a - a0) / aw; + a = b; + } + else if (aw < bw) + { + a = a0 + aw - aw * (b - b0) / bw; + b = a; + } + }; + + distribute(a.x, b.x, left(), right(), r.left(), r.right()); + distribute(a.y, b.y, top(), bottom(), r.top(), r.bottom()); + + return basic_line(a, b); +} + +template +inline ax::basic_line ax::basic_rect::get_closest_line(const basic_rect& r, T radius) const +{ + return get_closest_line(r, radius, radius); +} + +template +inline ax::basic_line ax::basic_rect::get_closest_line(const basic_rect& r, T radius_a, T radius_b) const +{ + auto line = get_closest_line(r); + if (radius_a < 0) + radius_a = 0; + if (radius_b < 0) + radius_b = 0; + + if (radius_a == 0 && radius_b == 0) + return line; + + const auto offset = line.b - line.a; + const auto length_sq = offset.x * offset.x + offset.y * offset.y; + const auto radius_a_sq = radius_a * radius_a; + const auto radius_b_sq = radius_b * radius_b; + + if (length_sq <= 0) + return line; + + const auto length = sqrtf(static_cast(length_sq)); + const auto direction = ax::basic_point(offset.x / length, offset.y / length); + + const auto total_radius_sq = radius_a_sq + radius_b_sq; + float radius_a_f = static_cast(radius_a); + float radius_b_f = static_cast(radius_b); + if (total_radius_sq > length_sq) + { + const auto scale = length / (radius_a_f + radius_b_f); + radius_a_f *= scale; + radius_b_f *= scale; + } + + line.a = line.a + static_cast(direction * radius_a_f); + line.b = line.b - static_cast(direction * radius_b_f); + + return line; +} + + +//------------------------------------------------------------------------------ +inline void ax::matrix::zero() +{ + *this = matrix(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); +} + +inline void ax::matrix::reset() +{ + *this = matrix(); +} + +inline bool ax::matrix::invert() +{ + const float det = (m11 * m22 - m21 * m12); + if (det == 0.0f) + return false; + + const float invDet = 1.0f / det; + + *this = matrix( + m22 * invDet, + -m12 * invDet, + -m21 * invDet, + m11 * invDet, + -(-m21 * m32 + m22 * m31) * invDet, + -( m11 * m32 - m12 * m31) * invDet); + + return true; +} + +inline void ax::matrix::translate(float x, float y, matrix_order order/* = matrix_order::prepend*/) +{ + combine(matrix(1.0f, 0.0f, 0.0f, 1.0f, x, y), order); +} + +inline void ax::matrix::rotate(float angle, matrix_order order/* = matrix_order::prepend*/) +{ + float angleRad = angle * AX_PI / 180.0f; + + const float c = cosf(angleRad); + const float s = sinf(angleRad); + + combine(matrix(c, s, -s, c, 0.0f, 0.0f), order); +} + +inline void ax::matrix::rotate_at(float angle, float cx, float cy, matrix_order order/* = matrix_order::prepend*/) +{ + const auto angleRad = angle * AX_PI / 180.0f; + + const auto c = cosf(angleRad); + const auto s = sinf(angleRad); + + combine(matrix(c, s, -s, c, + -cx * c - cy * -s + cx, + -cx * s - cy * c + cy), order); +} + +inline void ax::matrix::scale(float x, float y, matrix_order order/* = matrix_order::prepend*/) +{ + combine(matrix(x, 0.0f, 0.0f, y, 0.0f, 0.0f), order); +} + +inline void ax::matrix::shear(float x, float y, matrix_order order/* = matrix_order::prepend*/) +{ + combine(matrix(1.0f, y, x, 1.0f, 0.0f, 0.0f), order); +} + +inline void ax::matrix::combine(const matrix& matrix, matrix_order order/* = matrix_order::prepend*/) +{ + if (order == matrix_order::set) + { + if (this != &matrix) + *this = matrix; + return; + } + + const auto* am = this; + const auto* bm = &matrix; + + if (order == matrix_order::append) + { + using std::swap; + swap(am, bm); + } + + *this = ax::matrix( + am->m11 * bm->m11 + am->m21 * bm->m12, + bm->m11 * am->m12 + bm->m12 * am->m22, + bm->m21 * am->m11 + bm->m22 * am->m21, + am->m12 * bm->m21 + am->m22 * bm->m22, + bm->m31 * am->m11 + bm->m32 * am->m21 + am->m31, + bm->m31 * am->m12 + bm->m32 * am->m22 + am->m32); +} + +inline ax::matrix ax::matrix::inverted() const +{ + matrix inverted = *this; + inverted.invert(); + return inverted; +} + + +//------------------------------------------------------------------------------ +inline void ax::matrix4::zero() +{ + *this = matrix4( + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f); +} + +inline void ax::matrix4::reset() +{ + *this = matrix4(); +} + +inline bool ax::matrix4::invert() +{ +# if 0 + // wild magic 4 inverse - http://www.geometrictools.com/Documentation/LaplaceExpansionTheorem.pdf + // 84 multiplications + // 66 adds/subs + // 1 division + + // 24 + const float fA0 = m11 * m22 - m12 * m21; + const float fA1 = m11 * m23 - m13 * m21; + const float fA2 = m11 * m24 - m14 * m21; + const float fA3 = m12 * m23 - m13 * m22; + const float fA4 = m12 * m24 - m14 * m22; + const float fA5 = m13 * m24 - m14 * m23; + const float fB0 = m31 * m42 - m32 * m41; + const float fB1 = m31 * m43 - m33 * m41; + const float fB2 = m31 * m44 - m34 * m41; + const float fB3 = m32 * m43 - m33 * m42; + const float fB4 = m32 * m44 - m34 * m42; + const float fB5 = m33 * m44 - m34 * m43; + + // 6 + const float det = fA0 * fB5 - fA1 * fB4 + fA2 * fB3 + fA3 * fB2 - fA4 * fB1 + fA5 * fB0; + + if (det == 0.0f) + { + zero(); + return false; + } + + const float invDet = 1.0f / det; + + // 36 + 16 + *this = Matrix4( + ( m22 * fB5 - m23 * fB4 + m24 * fB3) * invDet, + (-m12 * fB5 + m13 * fB4 - m14 * fB3) * invDet, + ( m42 * fA5 - m43 * fA4 + m44 * fA3) * invDet, + (-m32 * fA5 + m33 * fA4 - m34 * fA3) * invDet, + (-m21 * fB5 + m23 * fB2 - m24 * fB1) * invDet, + ( m11 * fB5 - m13 * fB2 + m14 * fB1) * invDet, + (-m41 * fA5 + m43 * fA2 - m44 * fA1) * invDet, + ( m31 * fA5 - m33 * fA2 + m34 * fA1) * invDet, + ( m21 * fB4 - m22 * fB2 + m24 * fB0) * invDet, + (-m11 * fB4 + m12 * fB2 - m14 * fB0) * invDet, + ( m41 * fA4 - m42 * fA2 + m44 * fA0) * invDet, + (-m31 * fA4 + m32 * fA2 - m34 * fA0) * invDet, + (-m21 * fB3 + m22 * fB1 - m23 * fB0) * invDet, + ( m11 * fB3 - m12 * fB1 + m13 * fB0) * invDet, + (-m41 * fA3 + m42 * fA1 - m43 * fA0) * invDet, + ( m31 * fA3 - m32 * fA1 + m33 * fA0) * invDet); + + return true; +# else + // 214 multiplications + // 80 adds/subs + // 1 division + const float d00 = m22 * m33 * m44 + m32 * m43 * m24 + m42 * m23 * m34 - m24 * m33 * m42 - m34 * m43 * m22 - m44 * m23 * m32; + const float d01 = m12 * m33 * m44 + m32 * m43 * m14 + m42 * m13 * m34 - m14 * m33 * m42 - m34 * m43 * m12 - m44 * m13 * m32; + const float d02 = m12 * m23 * m44 + m22 * m43 * m14 + m42 * m13 * m24 - m14 * m23 * m42 - m24 * m43 * m12 - m44 * m13 * m22; + const float d03 = m12 * m23 * m34 + m22 * m33 * m14 + m32 * m13 * m24 - m14 * m23 * m32 - m24 * m33 * m12 - m34 * m13 * m22; + + const float d10 = m21 * m33 * m44 + m31 * m43 * m24 + m41 * m23 * m34 - m24 * m33 * m41 - m34 * m43 * m21 - m44 * m23 * m31; + const float d11 = m11 * m33 * m44 + m31 * m43 * m14 + m41 * m13 * m34 - m14 * m33 * m41 - m34 * m43 * m11 - m44 * m13 * m31; + const float d12 = m11 * m23 * m44 + m21 * m43 * m14 + m41 * m13 * m24 - m14 * m23 * m41 - m24 * m43 * m11 - m44 * m13 * m21; + const float d13 = m11 * m23 * m34 + m21 * m33 * m14 + m31 * m13 * m24 - m14 * m23 * m31 - m24 * m33 * m11 - m34 * m13 * m21; + + const float d20 = m21 * m32 * m44 + m31 * m42 * m24 + m41 * m22 * m34 - m24 * m32 * m41 - m34 * m42 * m21 - m44 * m22 * m31; + const float d21 = m11 * m32 * m44 + m31 * m42 * m14 + m41 * m12 * m34 - m14 * m32 * m41 - m34 * m42 * m11 - m44 * m12 * m31; + const float d22 = m11 * m22 * m44 + m21 * m42 * m14 + m41 * m12 * m24 - m14 * m22 * m41 - m24 * m42 * m11 - m44 * m12 * m21; + const float d23 = m11 * m22 * m34 + m21 * m32 * m14 + m31 * m12 * m24 - m14 * m22 * m31 - m24 * m32 * m11 - m34 * m12 * m21; + + const float d30 = m21 * m32 * m43 + m31 * m42 * m23 + m41 * m22 * m33 - m23 * m32 * m41 - m33 * m42 * m21 - m43 * m22 * m31; + const float d31 = m11 * m32 * m43 + m31 * m42 * m13 + m41 * m12 * m33 - m13 * m32 * m41 - m33 * m42 * m11 - m43 * m12 * m31; + const float d32 = m11 * m22 * m43 + m21 * m42 * m13 + m41 * m12 * m23 - m13 * m22 * m41 - m23 * m42 * m11 - m43 * m12 * m21; + const float d33 = m11 * m22 * m33 + m21 * m32 * m13 + m31 * m12 * m23 - m13 * m22 * m31 - m23 * m32 * m11 - m33 * m12 * m21; + + const float det = m11 * d00 - m21 * d01 + m31 * d02 - m41 * d03; + + if (det == 0.0f) + { + zero(); + return false; + } + + const float invDet = 1.0f / det; + + m11 = d00 * invDet; m21 = -d10 * invDet; m31 = d20 * invDet; m41 = -d30 * invDet; + m12 = -d01 * invDet; m22 = d11 * invDet; m32 = -d21 * invDet; m42 = d31 * invDet; + m13 = d02 * invDet; m23 = -d12 * invDet; m33 = d22 * invDet; m43 = -d32 * invDet; + m14 = -d03 * invDet; m24 = d13 * invDet; m34 = -d23 * invDet; m44 = d33 * invDet; + + return true; +# endif +} + +inline void ax::matrix4::transpose() +{ + using std::swap; + + swap(m12, m21); + swap(m13, m31); + swap(m14, m41); + swap(m23, m32); + swap(m24, m42); + swap(m34, m43); +} + +inline void ax::matrix4::translate(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/) +{ + combine(matrix4( + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + x, y, z, 1.0f), order); +} + +inline void ax::matrix4::rotate_x(float angle, matrix_order order/* = matrix_order::prepend*/) +{ + const auto angleRad = angle * AX_PI / 180.0f; + + const auto c = cosf(angleRad); + const auto s = sinf(angleRad); + + combine(matrix4( + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, c, -s, 0.0f, + 0.0f, s, c, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f), order); +} + +inline void ax::matrix4::rotate_y(float angle, matrix_order order/* = matrix_order::prepend*/) +{ + const auto angleRad = angle * AX_PI / 180.0f; + + const auto c = cosf(angleRad); + const auto s = sinf(angleRad); + + combine(matrix4( + c, 0.0f, -s, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + s, 0.0f, c, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f), order); +} + +inline void ax::matrix4::rotate_z(float angle, matrix_order order/* = matrix_order::prepend*/) +{ + const auto angleRad = angle * AX_PI / 180.0f; + + const auto c = cosf(angleRad); + const auto s = sinf(angleRad); + + combine(matrix4( + c, -s, 0.0f, 0.0f, + s, c, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f), order); +} + +inline void ax::matrix4::rotate_axis(float angle, float x, float y, float z, matrix_order order/* = matrix_order::prepend*/) +{ + const auto angleRad = angle * AX_PI / 180.0f; + + const auto c = cosf(angleRad); + const auto s = sinf(angleRad); + + const auto rc = 1.0f - c; + + combine(matrix4( + (rc * x * x) + c, (rc * x * y) + (z * s), (rc * x * z) - (y * s), 0.0f, + (rc * x * y) - (z * s), (rc * y * y) + c, (rc * z * y) + (x * s), 0.0f, + (rc * x * z) + (y * s), (rc * y * z) - (x * s), (rc * z * z) + c, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f), order); +} + +inline void ax::matrix4::scale(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/) +{ + combine(matrix4( + x, 0.0f, 0.0f, 0.0f, + 0.0f, y, 0.0f, 0.0f, + 0.0f, 0.0f, z, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f), order); +} + +inline void ax::matrix4::combine(const matrix4& matrix, matrix_order order/* = matrix_order::prepend*/) +{ + if (order == matrix_order::set) + { + if (this != &matrix) + *this = matrix; + return; + } + + const auto* am = this; + const auto* bm = &matrix; + + if (order == matrix_order::append) + { + using std::swap; + swap(am, bm); + } + + *this = matrix4( + am->m11 * bm->m11 + am->m21 * bm->m12 + am->m31 * bm->m13 + am->m41 * bm->m14, + am->m12 * bm->m11 + am->m22 * bm->m12 + am->m32 * bm->m13 + am->m42 * bm->m14, + am->m13 * bm->m11 + am->m23 * bm->m12 + am->m33 * bm->m13 + am->m43 * bm->m14, + am->m14 * bm->m11 + am->m24 * bm->m12 + am->m34 * bm->m13 + am->m44 * bm->m14, + + am->m11 * bm->m21 + am->m21 * bm->m22 + am->m31 * bm->m23 + am->m41 * bm->m24, + am->m12 * bm->m21 + am->m22 * bm->m22 + am->m32 * bm->m23 + am->m42 * bm->m24, + am->m13 * bm->m21 + am->m23 * bm->m22 + am->m33 * bm->m23 + am->m43 * bm->m24, + am->m14 * bm->m21 + am->m24 * bm->m22 + am->m34 * bm->m23 + am->m44 * bm->m24, + + am->m11 * bm->m31 + am->m21 * bm->m32 + am->m31 * bm->m33 + am->m41 * bm->m34, + am->m12 * bm->m31 + am->m22 * bm->m32 + am->m32 * bm->m33 + am->m42 * bm->m34, + am->m13 * bm->m31 + am->m23 * bm->m32 + am->m33 * bm->m33 + am->m43 * bm->m34, + am->m14 * bm->m31 + am->m24 * bm->m32 + am->m34 * bm->m33 + am->m44 * bm->m34, + + am->m11 * bm->m41 + am->m21 * bm->m42 + am->m31 * bm->m43 + am->m41 * bm->m44, + am->m12 * bm->m41 + am->m22 * bm->m42 + am->m32 * bm->m43 + am->m42 * bm->m44, + am->m13 * bm->m41 + am->m23 * bm->m42 + am->m33 * bm->m43 + am->m43 * bm->m44, + am->m14 * bm->m41 + am->m24 * bm->m42 + am->m34 * bm->m43 + am->m44 * bm->m44); +} + +inline ax::matrix4 ax::matrix4::inverted() const +{ + matrix4 inverted = *this; + inverted.invert(); + return inverted; +} + +inline ax::matrix4 ax::matrix4::transposed() const +{ + matrix4 transposed = *this; + transposed.transpose(); + return transposed; +} + + +//------------------------------------------------------------------------------ +namespace ax { +namespace detail { + +# if defined(__clang__) || defined(__GNUC__) +template +void transform_points(const M& m, basic_point* points, size_t count); +# else +template +inline void transform_points(const M& m, basic_point* points, size_t count) +{ + // UsdLayerManager local patch: static_assert(false, ...) here is + // ill-formed regardless of instantiation (its condition doesn't depend + // on a template parameter) — stricter MSVC (/permissive-, set for MSVC + // >= 1910 in this project's CMakeLists.txt) diagnoses it just from this + // primary template's declaration, even when every real call resolves to + // one of the matrix/matrix4 specializations below and this fallback is + // never instantiated. Make the condition depend on T so it only fires + // if something actually instantiates this unspecialized fallback. + static_assert(sizeof(T) == 0, "This combination of matrix type and point type is not supported"); +} +# endif + +template +inline void transform_vectors(const M& m, basic_point* points, size_t count) +{ + typedef basic_point point_t; + + for (size_t i = 0; i < count; ++i, ++points) + { + auto x = m.m11 * points->x + m.m21 * points->y; + auto y = m.m12 * points->x + m.m22 * points->y; + + points->x = static_cast(x); + points->y = static_cast(y); + } +} + +template +inline void transform_points(const matrix& m, basic_point* points, size_t count) +{ + typedef basic_point point_t; + + for (size_t i = 0; i < count; ++i, ++points) + { + auto x = m.m11 * points->x + m.m21 * points->y + m.m31; + auto y = m.m12 * points->x + m.m22 * points->y + m.m32; + + points->x = static_cast(x); + points->y = static_cast(y); + } +} + +template +inline void transform_points(const matrix4& m, basic_point* points, size_t count) +{ + typedef basic_point point_t; + + for (size_t i = 0; i < count; ++i, ++points) + { + auto x = m.m11 * points->x + m.m21 * points->y + m.m41; + auto y = m.m12 * points->x + m.m22 * points->y + m.m42; + + points->x = static_cast(x); + points->y = static_cast(y); + } +} + +} // namespace detail +} // namespace ax + + +//------------------------------------------------------------------------------ +# endif // defined(__cplusplus) + + +//------------------------------------------------------------------------------ +# endif // __AX_MATH_2D_INL__ diff --git a/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Widgets.h b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Widgets.h new file mode 100644 index 0000000..a81124b --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Include/ax/Widgets.h @@ -0,0 +1,13 @@ +#pragma once +#include "imgui.h" +#include "Drawing.h" + +namespace ax { +namespace Widgets { + +using Drawing::IconType; + +void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0)); + +} // namespace Widgets +} // namespace ax \ No newline at end of file diff --git a/src/ui/NodeEditor/BlueprintWidgets/Source/Interop.h b/src/ui/NodeEditor/BlueprintWidgets/Source/Interop.h new file mode 100644 index 0000000..9e7b9fe --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Source/Interop.h @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# pragma once +# include "imgui.h" +# include "ax/Math2D.h" + + +//------------------------------------------------------------------------------ +//namespace ax { +//namespace ImGuiInterop { + + +//------------------------------------------------------------------------------ +// Modern ImGui (1.89.4+) already defines these operators in imgui.h itself +// when IMGUI_DEFINE_MATH_OPERATORS is set before its first include — guard +// against redefinition against this project's ImGui version. +# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; } +static inline ImVec2 operator+(const ImVec2& lhs) { return ImVec2( lhs.x, lhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator*(float lhs, const ImVec2& rhs) { return ImVec2(lhs * rhs.x, lhs * rhs.y); } +# endif + +static inline int roundi(float value) { return static_cast(value); } +static inline ax::point to_point(const ImVec2& value) { return ax::point(roundi(value.x), roundi(value.y)); } +static inline ax::pointf to_pointf(const ImVec2& value) { return ax::pointf(value.x, value.y); } +static inline ax::pointf to_pointf(const ax::point& value) { return ax::pointf(static_cast(value.x), static_cast(value.y)); } +static inline ax::size to_size (const ImVec2& value) { return ax::size (roundi(value.x), roundi(value.y)); } +static inline ax::sizef to_sizef(const ImVec2& value) { return ax::sizef(value.x, value.y); } +static inline ImVec2 to_imvec(const ax::point& value) { return ImVec2(static_cast(value.x), static_cast(value.y)); } +static inline ImVec2 to_imvec(const ax::pointf& value) { return ImVec2(value.x, value.y); } +static inline ImVec2 to_imvec(const ax::size& value) { return ImVec2(static_cast(value.w), static_cast(value.h)); } +static inline ImVec2 to_imvec(const ax::sizef& value) { return ImVec2(value.w, value.h); } +static inline ax::rect ImGui_GetItemRect() { return ax::rect(to_point(ImGui::GetItemRectMin()), to_point(ImGui::GetItemRectMax())); } + + +//------------------------------------------------------------------------------ +struct FringeScaleScope +{ + FringeScaleScope(float scale) + : m_LastFringeScale(ImGui::GetWindowDrawList()->_FringeScale) + { + ImGui::GetWindowDrawList()->_FringeScale = scale; + } + + ~FringeScaleScope() + { + ImGui::GetWindowDrawList()->_FringeScale = m_LastFringeScale; + } + +private: + float m_LastFringeScale; +}; + + +//------------------------------------------------------------------------------ +//} // namespace ImGuiInterop +//} // namespace ax diff --git a/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Drawing.cpp b/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Drawing.cpp new file mode 100644 index 0000000..b7dcc65 --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Drawing.cpp @@ -0,0 +1,207 @@ +//# include "stdafx.h" +# include "ax/Drawing.h" +# include "ax/Math2D.h" +# include "Interop.h" +# include + +void ax::Drawing::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) +{ + auto rect = ax::rect(to_point(a), to_point(b)); + const auto outline_scale = rect.w / 24.0f; + const auto extra_segments = roundi(2 * outline_scale); // for full circle + + if (type == IconType::Flow) + { + const auto origin_scale = rect.w / 24.0f; + + const auto offset_x = 1.0f * origin_scale; + const auto offset_y = 0.0f * origin_scale; + const auto margin = (filled ? 2.0f : 2.0f) * origin_scale; + const auto rounding = 0.1f * origin_scale; + const auto tip_round = 0.7f; // percentage of triangle edge (for tip) + //const auto edge_round = 0.7f; // percentage of triangle edge (for corner) + const auto canvas = rectf( + rect.x + margin + offset_x, + rect.y + margin + offset_y, + rect.w - margin * 2.0f, + rect.h - margin * 2.0f); + + const auto left = canvas.x + canvas.w * 0.5f * 0.3f; + const auto right = canvas.x + canvas.w - canvas.w * 0.5f * 0.3f; + const auto top = canvas.y + canvas.h * 0.5f * 0.2f; + const auto bottom = canvas.y + canvas.h - canvas.h * 0.5f * 0.2f; + const auto center_y = (top + bottom) * 0.5f; + //const auto angle = AX_PI * 0.5f * 0.5f * 0.5f; + + const auto tip_top = ImVec2(canvas.x + canvas.w * 0.5f, top); + const auto tip_right = ImVec2(right, center_y); + const auto tip_bottom = ImVec2(canvas.x + canvas.w * 0.5f, bottom); + + drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, top), + ImVec2(left, top), + ImVec2(left, top) + ImVec2(rounding, 0)); + drawList->PathLineTo(tip_top); + drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round); + drawList->PathBezierCubicCurveTo( + tip_right, + tip_right, + tip_bottom + (tip_right - tip_bottom) * tip_round); + drawList->PathLineTo(tip_bottom); + drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, bottom), + ImVec2(left, bottom), + ImVec2(left, bottom) - ImVec2(0, rounding)); + + if (!filled) + { + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + else + drawList->PathFillConvex(color); + } + else + { + auto triangleStart = rect.center_x() + 0.32f * rect.w; + + rect.x -= roundi(rect.w * 0.25f * 0.25f); + + if (type == IconType::Circle) + { + const auto c = to_imvec(rect.center()); + + if (!filled) + { + const auto r = 0.5f * rect.w / 2.0f - 0.5f; + + if (innerColor & 0xFF000000) + drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments); + drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale); + } + else + drawList->AddCircleFilled(c, 0.5f * rect.w / 2.0f, color, 12 + extra_segments); + } + + if (type == IconType::Square) + { + if (filled) + { + const auto r = 0.5f * rect.w / 2.0f; + const auto p0 = to_imvec(rect.center()) - ImVec2(r, r); + const auto p1 = to_imvec(rect.center()) + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments); + } + else + { + const auto r = 0.5f * rect.w / 2.0f - 0.5f; + const auto p0 = to_imvec(rect.center()) - ImVec2(r, r); + const auto p1 = to_imvec(rect.center()) + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments); + + drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale); + } + } + + if (type == IconType::Grid) + { + const auto r = 0.5f * rect.w / 2.0f; + const auto w = ceilf(r / 3.0f); + + const auto baseTl = ImVec2(floorf(rect.center_x() - w * 2.5f), floorf(rect.center_y() - w * 2.5f)); + const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w)); + + auto tl = baseTl; + auto br = baseBr; + for (int i = 0; i < 3; ++i) + { + tl.x = baseTl.x; + br.x = baseBr.x; + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + if (i != 1 || filled) + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + drawList->AddRectFilled(tl, br, color); + + tl.y += w * 2; + br.y += w * 2; + } + + triangleStart = br.x + w + 1.0f / 24.0f * rect.w; + } + + if (type == IconType::RoundSquare) + { + if (filled) + { + const auto r = 0.5f * rect.w / 2.0f; + const auto cr = r * 0.5f; + const auto p0 = to_imvec(rect.center()) - ImVec2(r, r); + const auto p1 = to_imvec(rect.center()) + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, cr, 15); + } + else + { + const auto r = 0.5f * rect.w / 2.0f - 0.5f; + const auto cr = r * 0.5f; + const auto p0 = to_imvec(rect.center()) - ImVec2(r, r); + const auto p1 = to_imvec(rect.center()) + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, cr, 15); + + drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale); + } + } + else if (type == IconType::Diamond) + { + if (filled) + { + const auto r = 0.607f * rect.w / 2.0f; + const auto c = rect.center(); + + drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r)); + drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0)); + drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r)); + drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0)); + drawList->PathFillConvex(color); + } + else + { + const auto r = 0.607f * rect.w / 2.0f - 0.5f; + const auto c = rect.center(); + + drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r)); + drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0)); + drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r)); + drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0)); + + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + } + else + { + const auto triangleTip = triangleStart + rect.w * (0.45f - 0.32f); + + drawList->AddTriangleFilled( + ImVec2(ceilf(triangleTip), rect.top() + rect.h * 0.5f), + ImVec2(triangleStart, rect.center_y() + 0.15f * rect.h), + ImVec2(triangleStart, rect.center_y() - 0.15f * rect.h), + color); + } + } +} diff --git a/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Widgets.cpp b/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Widgets.cpp new file mode 100644 index 0000000..c16232b --- /dev/null +++ b/src/ui/NodeEditor/BlueprintWidgets/Source/ax/Widgets.cpp @@ -0,0 +1,15 @@ +#include "ax/Widgets.h" +#include "Interop.h" + +void ax::Widgets::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, const ImVec4& innerColor/* = ImVec4(0, 0, 0, 0)*/) +{ + if (ImGui::IsRectVisible(size)) + { + auto cursorPos = ImGui::GetCursorScreenPos(); + auto drawList = ImGui::GetWindowDrawList(); + ax::Drawing::DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor)); + } + + ImGui::Dummy(size); +} + diff --git a/src/ui/NodeEditor/Include/imgui_node_editor.h b/src/ui/NodeEditor/Include/imgui_node_editor.h new file mode 100644 index 0000000..4b8e738 --- /dev/null +++ b/src/ui/NodeEditor/Include/imgui_node_editor.h @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_NODE_EDITOR_H__ +# define __IMGUI_NODE_EDITOR_H__ +# pragma once + + +//------------------------------------------------------------------------------ +# include +# include // std::uintXX_t +# include // std::move + + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { + + +//------------------------------------------------------------------------------ +struct NodeId; +struct LinkId; +struct PinId; + + +//------------------------------------------------------------------------------ +enum class SaveReasonFlags: uint32_t +{ + None = 0x00000000, + Navigation = 0x00000001, + Position = 0x00000002, + Size = 0x00000004, + Selection = 0x00000008, + User = 0x00000010 +}; + +inline SaveReasonFlags operator |(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } +inline SaveReasonFlags operator &(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast(static_cast(lhs) & static_cast(rhs)); } + +using ConfigSaveSettings = bool (*)(const char* data, size_t size, SaveReasonFlags reason, void* userPointer); +using ConfigLoadSettings = size_t (*)(char* data, void* userPointer); + +using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, size_t size, SaveReasonFlags reason, void* userPointer); +using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, void* userPointer); + +using ConfigSession = void (*)(void* userPointer); + +struct Config +{ + const char* SettingsFile; + ConfigSession BeginSaveSession; + ConfigSession EndSaveSession; + ConfigSaveSettings SaveSettings; + ConfigLoadSettings LoadSettings; + ConfigSaveNodeSettings SaveNodeSettings; + ConfigLoadNodeSettings LoadNodeSettings; + void* UserPointer; + + Config() + : SettingsFile("NodeEditor.json") + , BeginSaveSession(nullptr) + , EndSaveSession(nullptr) + , SaveSettings(nullptr) + , LoadSettings(nullptr) + , SaveNodeSettings(nullptr) + , LoadNodeSettings(nullptr) + , UserPointer(nullptr) + { + } +}; + + +//------------------------------------------------------------------------------ +enum class PinKind +{ + Input, + Output +}; + + +//------------------------------------------------------------------------------ +enum StyleColor +{ + StyleColor_Bg, + StyleColor_Grid, + StyleColor_NodeBg, + StyleColor_NodeBorder, + StyleColor_HovNodeBorder, + StyleColor_SelNodeBorder, + StyleColor_NodeSelRect, + StyleColor_NodeSelRectBorder, + StyleColor_HovLinkBorder, + StyleColor_SelLinkBorder, + StyleColor_LinkSelRect, + StyleColor_LinkSelRectBorder, + StyleColor_PinRect, + StyleColor_PinRectBorder, + StyleColor_Flow, + StyleColor_FlowMarker, + StyleColor_GroupBg, + StyleColor_GroupBorder, + + StyleColor_Count +}; + +enum StyleVar +{ + StyleVar_NodePadding, + StyleVar_NodeRounding, + StyleVar_NodeBorderWidth, + StyleVar_HoveredNodeBorderWidth, + StyleVar_SelectedNodeBorderWidth, + StyleVar_PinRounding, + StyleVar_PinBorderWidth, + StyleVar_LinkStrength, + StyleVar_SourceDirection, + StyleVar_TargetDirection, + StyleVar_ScrollDuration, + StyleVar_FlowMarkerDistance, + StyleVar_FlowSpeed, + StyleVar_FlowDuration, + StyleVar_PivotAlignment, + StyleVar_PivotSize, + StyleVar_PivotScale, + StyleVar_PinCorners, + StyleVar_PinRadius, + StyleVar_PinArrowSize, + StyleVar_PinArrowWidth, + StyleVar_GroupRounding, + StyleVar_GroupBorderWidth, +}; + +struct Style +{ + ImVec4 NodePadding; + float NodeRounding; + float NodeBorderWidth; + float HoveredNodeBorderWidth; + float SelectedNodeBorderWidth; + float PinRounding; + float PinBorderWidth; + float LinkStrength; + ImVec2 SourceDirection; + ImVec2 TargetDirection; + float ScrollDuration; + float FlowMarkerDistance; + float FlowSpeed; + float FlowDuration; + ImVec2 PivotAlignment; + ImVec2 PivotSize; + ImVec2 PivotScale; + float PinCorners; + float PinRadius; + float PinArrowSize; + float PinArrowWidth; + float GroupRounding; + float GroupBorderWidth; + ImVec4 Colors[StyleColor_Count]; + + Style() + { + NodePadding = ImVec4(8, 8, 8, 8); + NodeRounding = 12.0f; + NodeBorderWidth = 1.5f; + HoveredNodeBorderWidth = 3.5f; + SelectedNodeBorderWidth = 3.5f; + PinRounding = 4.0f; + PinBorderWidth = 0.0f; + LinkStrength = 100.0f; + SourceDirection = ImVec2(1.0f, 0.0f); + TargetDirection = ImVec2(-1.0f, 0.0f); + ScrollDuration = 0.35f; + FlowMarkerDistance = 30.0f; + FlowSpeed = 150.0f; + FlowDuration = 2.0f; + PivotAlignment = ImVec2(0.5f, 0.5f); + PivotSize = ImVec2(-1, -1); + PivotScale = ImVec2(1, 1); + PinCorners = ImDrawFlags_RoundCornersAll; // ImDrawCornerFlags_All renamed upstream in ImGui + PinRadius = 0.0f; + PinArrowSize = 0.0f; + PinArrowWidth = 0.0f; + GroupRounding = 6.0f; + GroupBorderWidth = 1.0f; + + Colors[StyleColor_Bg] = ImColor( 60, 60, 70, 200); + Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40); + Colors[StyleColor_NodeBg] = ImColor( 32, 32, 32, 200); + Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96); + Colors[StyleColor_HovNodeBorder] = ImColor( 50, 176, 255, 255); + Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255); + Colors[StyleColor_NodeSelRect] = ImColor( 5, 130, 255, 64); + Colors[StyleColor_NodeSelRectBorder] = ImColor( 5, 130, 255, 128); + Colors[StyleColor_HovLinkBorder] = ImColor( 50, 176, 255, 255); + Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255); + Colors[StyleColor_LinkSelRect] = ImColor( 5, 130, 255, 64); + Colors[StyleColor_LinkSelRectBorder] = ImColor( 5, 130, 255, 128); + Colors[StyleColor_PinRect] = ImColor( 60, 180, 255, 100); + Colors[StyleColor_PinRectBorder] = ImColor( 60, 180, 255, 128); + Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255); + Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255); + Colors[StyleColor_GroupBg] = ImColor( 0, 0, 0, 160); + Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32); + } +}; + + +//------------------------------------------------------------------------------ +struct EditorContext; + + +//------------------------------------------------------------------------------ +void SetCurrentEditor(EditorContext* ctx); +EditorContext* GetCurrentEditor(); +EditorContext* CreateEditor(const Config* config = nullptr); +void DestroyEditor(EditorContext* ctx); + +Style& GetStyle(); +const char* GetStyleColorName(StyleColor colorIndex); + +void PushStyleColor(StyleColor colorIndex, const ImVec4& color); +void PopStyleColor(int count = 1); + +void PushStyleVar(StyleVar varIndex, float value); +void PushStyleVar(StyleVar varIndex, const ImVec2& value); +void PushStyleVar(StyleVar varIndex, const ImVec4& value); +void PopStyleVar(int count = 1); + +void Begin(const char* id, const ImVec2& size = ImVec2(0, 0)); +void End(); + +void BeginNode(NodeId id); +void BeginPin(PinId id, PinKind kind); +void PinRect(const ImVec2& a, const ImVec2& b); +void PinPivotRect(const ImVec2& a, const ImVec2& b); +void PinPivotSize(const ImVec2& size); +void PinPivotScale(const ImVec2& scale); +void PinPivotAlignment(const ImVec2& alignment); +void EndPin(); +void Group(const ImVec2& size); +void EndNode(); + +bool BeginGroupHint(NodeId nodeId); +ImVec2 GetGroupMin(); +ImVec2 GetGroupMax(); +ImDrawList* GetHintForegroundDrawList(); +ImDrawList* GetHintBackgroundDrawList(); +void EndGroupHint(); + +// TODO: Add a way to manage node background channels +ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId); + +bool Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f); + +void Flow(LinkId linkId); + +bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f); +bool QueryNewLink(PinId* startId, PinId* endId); +bool QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness = 1.0f); +bool QueryNewNode(PinId* pinId); +bool QueryNewNode(PinId* pinId, const ImVec4& color, float thickness = 1.0f); +bool AcceptNewItem(); +bool AcceptNewItem(const ImVec4& color, float thickness = 1.0f); +void RejectNewItem(); +void RejectNewItem(const ImVec4& color, float thickness = 1.0f); +void EndCreate(); + +bool BeginDelete(); +bool QueryDeletedLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr); +bool QueryDeletedNode(NodeId* nodeId); +bool AcceptDeletedItem(); +void RejectDeletedItem(); +void EndDelete(); + +void SetNodePosition(NodeId nodeId, const ImVec2& editorPosition); +ImVec2 GetNodePosition(NodeId nodeId); +ImVec2 GetNodeSize(NodeId nodeId); +void CenterNodeOnScreen(NodeId nodeId); + +void RestoreNodeState(NodeId nodeId); + +void Suspend(); +void Resume(); +bool IsSuspended(); + +bool IsActive(); + +bool HasSelectionChanged(); +int GetSelectedObjectCount(); +int GetSelectedNodes(NodeId* nodes, int size); +int GetSelectedLinks(LinkId* links, int size); +void ClearSelection(); +void SelectNode(NodeId nodeId, bool append = false); +void SelectLink(LinkId linkId, bool append = false); +void DeselectNode(NodeId nodeId); +void DeselectLink(LinkId linkId); + +bool DeleteNode(NodeId nodeId); +bool DeleteLink(LinkId linkId); + +void NavigateToContent(float duration = -1); +void NavigateToSelection(bool zoomIn = false, float duration = -1); + +bool ShowNodeContextMenu(NodeId* nodeId); +bool ShowPinContextMenu(PinId* pinId); +bool ShowLinkContextMenu(LinkId* linkId); +bool ShowBackgroundContextMenu(); + +void EnableShortcuts(bool enable); +bool AreShortcutsEnabled(); + +bool BeginShortcut(); +bool AcceptCut(); +bool AcceptCopy(); +bool AcceptPaste(); +bool AcceptDuplicate(); +bool AcceptCreateNode(); +int GetActionContextSize(); +int GetActionContextNodes(NodeId* nodes, int size); +int GetActionContextLinks(LinkId* links, int size); +void EndShortcut(); + +float GetCurrentZoom(); + +NodeId GetDoubleClickedNode(); +PinId GetDoubleClickedPin(); +LinkId GetDoubleClickedLink(); +bool IsBackgroundClicked(); +bool IsBackgroundDoubleClicked(); + +bool PinHadAnyLinks(PinId pinId); + +ImVec2 GetScreenSize(); +ImVec2 ScreenToCanvas(const ImVec2& pos); +ImVec2 CanvasToScreen(const ImVec2& pos); + + + + + + + + + + +//------------------------------------------------------------------------------ +namespace Details { + +template +struct SafeType +{ + SafeType(T t) + : m_Value(std::move(t)) + { + } + + SafeType(const SafeType&) = default; + + template + SafeType( + const SafeType + < + typename std::enable_if::value, T2>::type, + typename std::enable_if::value, Tag2>::type + >&) = delete; + + SafeType& operator=(const SafeType&) = default; + + explicit operator T() const { return Get(); } + + T Get() const { return m_Value; } + +private: + T m_Value; +}; + + +template +struct SafePointerType + : SafeType +{ + static const Tag Invalid; + + using SafeType::SafeType; + + SafePointerType() + : SafePointerType(Invalid) + { + } + + template explicit SafePointerType(T* ptr): SafePointerType(reinterpret_cast(ptr)) {} + template T* AsPointer() const { return reinterpret_cast(this->Get()); } + + explicit operator bool() const { return *this != Invalid; } +}; + +template +const Tag SafePointerType::Invalid = { 0 }; + +template +inline bool operator==(const SafePointerType& lhs, const SafePointerType& rhs) +{ + return lhs.Get() == rhs.Get(); +} + +template +inline bool operator!=(const SafePointerType& lhs, const SafePointerType& rhs) +{ + return lhs.Get() != rhs.Get(); +} + +} // namespace Details + +struct NodeId final: Details::SafePointerType +{ + using SafePointerType::SafePointerType; +}; + +struct LinkId final: Details::SafePointerType +{ + using SafePointerType::SafePointerType; +}; + +struct PinId final: Details::SafePointerType +{ + using SafePointerType::SafePointerType; +}; + + +//------------------------------------------------------------------------------ +} // namespace Editor +} // namespace ax + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_NODE_EDITOR_H__ \ No newline at end of file diff --git a/src/ui/NodeEditor/LICENSE b/src/ui/NodeEditor/LICENSE new file mode 100644 index 0000000..ae0db1a --- /dev/null +++ b/src/ui/NodeEditor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Michał Cichoń + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/ui/NodeEditor/Source/crude_json.cpp b/src/ui/NodeEditor/Source/crude_json.cpp new file mode 100644 index 0000000..7608e1a --- /dev/null +++ b/src/ui/NodeEditor/Source/crude_json.cpp @@ -0,0 +1,814 @@ +// Crude implementation of JSON value object and parser. +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +# include "crude_json.h" +# include +# include +# include +# include +# include +# include + + +namespace crude_json { + +value::value(value&& other) + : m_Type(other.m_Type) +{ + switch (m_Type) + { + case type_t::object: construct(m_Storage, std::move( *object_ptr(other.m_Storage))); break; + case type_t::array: construct(m_Storage, std::move( *array_ptr(other.m_Storage))); break; + case type_t::string: construct(m_Storage, std::move( *string_ptr(other.m_Storage))); break; + case type_t::boolean: construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); break; + case type_t::number: construct(m_Storage, std::move( *number_ptr(other.m_Storage))); break; + default: break; + } + destruct(other.m_Storage, other.m_Type); + other.m_Type = type_t::null; +} + +value::value(const value& other) + : m_Type(other.m_Type) +{ + switch (m_Type) + { + case type_t::object: construct(m_Storage, *object_ptr(other.m_Storage)); break; + case type_t::array: construct(m_Storage, *array_ptr(other.m_Storage)); break; + case type_t::string: construct(m_Storage, *string_ptr(other.m_Storage)); break; + case type_t::boolean: construct(m_Storage, *boolean_ptr(other.m_Storage)); break; + case type_t::number: construct(m_Storage, *number_ptr(other.m_Storage)); break; + default: break; + } +} + +value& value::operator[](size_t index) +{ + if (is_null()) + m_Type = construct(m_Storage, type_t::array); + + if (is_array()) + { + auto& v = *array_ptr(m_Storage); + if (index >= v.size()) + v.insert(v.end(), index - v.size() + 1, value()); + + return v[index]; + } + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +const value& value::operator[](size_t index) const +{ + if (is_array()) + return (*array_ptr(m_Storage))[index]; + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +value& value::operator[](const string& key) +{ + if (is_null()) + m_Type = construct(m_Storage, type_t::object); + + if (is_object()) + return (*object_ptr(m_Storage))[key]; + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +const value& value::operator[](const string& key) const +{ + if (is_object()) + { + auto& o = *object_ptr(m_Storage); + auto it = o.find(key); + CRUDE_ASSERT(it != o.end()); + return it->second; + } + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +bool value::contains(const string& key) const +{ + if (is_object()) + { + auto& o = *object_ptr(m_Storage); + auto it = o.find(key); + return it != o.end(); + } + + return false; +} + +void value::push_back(const value& value) +{ + if (is_null()) + m_Type = construct(m_Storage, type_t::array); + + if (is_array()) + { + auto& v = *array_ptr(m_Storage); + v.push_back(value); + } + else + { + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); + } +} + +void value::push_back(value&& value) +{ + if (is_null()) + m_Type = construct(m_Storage, type_t::array); + + if (is_array()) + { + auto& v = *array_ptr(m_Storage); + v.push_back(std::move(value)); + } + else + { + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); + } +} + +void value::swap(value& other) +{ + using std::swap; + + if (m_Type == other.m_Type) + { + switch (m_Type) + { + case type_t::object: swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); break; + case type_t::array: swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); break; + case type_t::string: swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); break; + case type_t::boolean: swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); break; + case type_t::number: swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); break; + default: break; + } + } + else + { + value tmp(std::move(other)); + other.~value(); + new (&other) value(std::move(*this)); + this->~value(); + new (this) value(std::move(tmp)); + } +} + +string value::dump(const int indent, const char indent_char) const +{ + dump_context_t context(indent, indent_char); + + context.out.precision(std::numeric_limits::max_digits10 + 1); + context.out << std::defaultfloat; + + dump(context, 0); + return context.out.str(); +} + +void value::dump_context_t::write_indent(int level) +{ + if (indent <= 0 || level == 0) + return; + + out.fill(indent_char); + out.width(indent * level); + out << indent_char; + out.width(0); +} + +void value::dump_context_t::write_separator() +{ + if (indent < 0) + return; + + out.put(' '); +} + +void value::dump_context_t::write_newline() +{ + if (indent < 0) + return; + + out.put('\n'); +} + +void value::dump(dump_context_t& context, int level) const +{ + context.write_indent(level); + + switch (m_Type) + { + case type_t::null: + context.out << "null"; + break; + + case type_t::object: + context.out << '{'; + { + context.write_newline(); + bool first = true; + for (auto& entry : *object_ptr(m_Storage)) + { + if (!first) { context.out << ','; context.write_newline(); } else first = false; + context.write_indent(level + 1); + context.out << '\"' << entry.first << "\":"; + if (!entry.second.is_structured()) + { + context.write_separator(); + entry.second.dump(context, 0); + } + else + { + context.write_newline(); + entry.second.dump(context, level + 1); + } + } + if (!first) + context.write_newline(); + } + context.write_indent(level); + context.out << '}'; + break; + + case type_t::array: + context.out << '['; + { + context.write_newline(); + bool first = true; + for (auto& entry : *array_ptr(m_Storage)) + { + if (!first) { context.out << ','; context.write_newline(); } else first = false; + if (!entry.is_structured()) + { + context.write_indent(level + 1); + entry.dump(context, 0); + } + else + { + entry.dump(context, level + 1); + } + } + if (!first) + context.write_newline(); + } + context.write_indent(level); + context.out << ']'; + break; + + case type_t::string: + context.out << '\"'; + + if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != string::npos || string_ptr(m_Storage)->find('\0') != string::npos) + { + for (auto c : *string_ptr(m_Storage)) + { + if (c == '\"') context.out << "\\\""; + else if (c == '\\') context.out << "\\\\"; + else if (c == '/') context.out << "\\/"; + else if (c == '\b') context.out << "\\b"; + else if (c == '\f') context.out << "\\f"; + else if (c == '\n') context.out << "\\n"; + else if (c == '\r') context.out << "\\r"; + else if (c == '\t') context.out << "\\t"; + else if (c == 0) context.out << "\\u0000"; + else context.out << c; + } + } + else + context.out << *string_ptr(m_Storage); + context.out << '\"'; + break; + + + case type_t::boolean: + if (*boolean_ptr(m_Storage)) + context.out << "true"; + else + context.out << "false"; + break; + + case type_t::number: + context.out << *number_ptr(m_Storage); + break; + + default: + break; + } +} + +struct value::parser +{ + parser(const char* begin, const char* end) + : m_Cursor(begin) + , m_End(end) + { + } + + value parse() + { + value v; + + // Switch to C locale to make strtod and strtol work as expected + auto previous_locale = std::setlocale(LC_NUMERIC, "C"); + + // Accept single value only when end of the stream is reached. + if (!accept_element(v) || !eof()) + v = value(type_t::discarded); + + if (previous_locale && strcmp(previous_locale, "C") != 0) + std::setlocale(LC_NUMERIC, previous_locale); + + return v; + } + +private: + struct cursor_state + { + cursor_state(parser* p) + : m_Owner(p) + , m_LastCursor(p->m_Cursor) + { + } + + void reset() + { + m_Owner->m_Cursor = m_LastCursor; + } + + bool operator()(bool accept) + { + if (!accept) + reset(); + else + m_LastCursor = m_Owner->m_Cursor; + return accept; + } + + private: + parser* m_Owner; + const char* m_LastCursor; + }; + + cursor_state state() + { + return cursor_state(this); + } + + bool accept_value(value& result) + { + return accept_object(result) + || accept_array(result) + || accept_string(result) + || accept_number(result) + || accept_boolean(result) + || accept_null(result); + } + + bool accept_object(value& result) + { + auto s = state(); + + object o; + if (s(accept('{') && accept_ws() && accept('}'))) + { + result = o; + return true; + } + else if (s(accept('{') && accept_members(o) && accept('}'))) + { + result = std::move(o); + return true; + } + + return false; + } + + bool accept_members(object& o) + { + if (!accept_member(o)) + return false; + + while (true) + { + auto s = state(); + if (!s(accept(',') && accept_member(o))) + break; + } + + return true; + } + + bool accept_member(object& o) + { + auto s = state(); + + value key; + value v; + if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && accept_element(v))) + { + o.emplace(std::move(key.get()), std::move(v)); + return true; + } + + return false; + } + + bool accept_array(value& result) + { + auto s = state(); + + if (s(accept('[') && accept_ws() && accept(']'))) + { + result = array(); + return true; + } + + array a; + if (s(accept('[') && accept_elements(a) && accept(']'))) + { + result = std::move(a); + return true; + } + + return false; + } + + bool accept_elements(array& a) + { + value v; + if (!accept_element(v)) + return false; + + a.emplace_back(std::move(v)); + while (true) + { + auto s = state(); + v = nullptr; + if (!s(accept(',') && accept_element(v))) + break; + a.emplace_back(std::move(v)); + } + + return true; + } + + bool accept_element(value& result) + { + auto s = state(); + return s(accept_ws() && accept_value(result) && accept_ws()); + } + + bool accept_string(value& result) + { + auto s = state(); + + string v; + if (s(accept('\"') && accept_characters(v) && accept('\"'))) + { + result = std::move(v); + return true; + } + else + return false; + } + + bool accept_characters(string& result) + { + int c; + while (accept_character(c)) + { + CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8 + result.push_back(static_cast(c)); + } + + return true; + } + + bool accept_character(int& c) + { + auto s = state(); + + if (accept('\\')) + { + return accept_escape(c); + } + else if (expect('\"')) + return false; + + // #todo: Handle UTF-8 sequences. + return s((c = peek()) >= 0) && advance(); + } + + bool accept_escape(int& c) + { + if (accept('\"')) { c = '\"'; return true; } + if (accept('\\')) { c = '\\'; return true; } + if (accept('/')) { c = '/'; return true; } + if (accept('b')) { c = '\b'; return true; } + if (accept('f')) { c = '\f'; return true; } + if (accept('n')) { c = '\n'; return true; } + if (accept('r')) { c = '\r'; return true; } + if (accept('t')) { c = '\t'; return true; } + + auto s = state(); + + string hex; + hex.reserve(4); + if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && accept_hex(hex) && accept_hex(hex))) + { + char* end = nullptr; + auto v = std::strtol(hex.c_str(), &end, 16); + if (end != hex.c_str() + hex.size()) + return false; + + c = v; + return true; + } + + return false; + } + + bool accept_hex(string& result) + { + if (accept_digit(result)) + return true; + + auto c = peek(); + if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + { + advance(); + result.push_back(c); + return true; + } + + return false; + } + + bool accept_number(value& result) + { + auto s = state(); + + string n; + if (s(accept_int(n) && accept_frac(n) && accept_exp(n))) + { + char* end = nullptr; + auto v = std::strtod(n.c_str(), &end); + if (end != n.c_str() + n.size()) + return false; + + if (!std::isnormal(v)) + return false; + + result = v; + return true; + } + + return false; + } + + bool accept_int(string& result) + { + auto s = state(); + + string part; + if (s(accept_onenine(part) && accept_digits(part))) + { + result += std::move(part); + return true; + } + + part.resize(0); + if (accept_digit(part)) + { + result += std::move(part); + return true; + } + + part.resize(0); + if (s(accept('-') && accept_onenine(part) && accept_digits(part))) + { + result += '-'; + result += std::move(part); + return true; + } + + part.resize(0); + if (s(accept('-') && accept_digit(part))) + { + result += '-'; + result += std::move(part); + return true; + } + + return false; + } + + bool accept_digits(string& result) + { + string part; + if (!accept_digit(part)) + return false; + + while (accept_digit(part)) + ; + + result += std::move(part); + + return true; + } + + bool accept_digit(string& result) + { + if (accept('0')) + { + result.push_back('0'); + return true; + } + else if (accept_onenine(result)) + return true; + + return false; + } + + bool accept_onenine(string& result) + { + auto c = peek(); + if (c >= '1' && c <= '9') + { + result.push_back(static_cast(c)); + return advance(); + } + + return false; + } + + bool accept_frac(string& result) + { + auto s = state(); + + string part; + if (s(accept('.') && accept_digits(part))) + { + result += '.'; + result += std::move(part); + } + + return true; + } + + bool accept_exp(string& result) + { + auto s = state(); + + string part; + if (s(accept('e') && accept_sign(part) && accept_digits(part))) + { + result += 'e'; + result += std::move(part); + return true; + } + part.resize(0); + if (s(accept('E') && accept_sign(part) && accept_digits(part))) + { + result += 'E'; + result += std::move(part); + } + + return true; + } + + bool accept_sign(string& result) + { + if (accept('+')) + result.push_back('+'); + else if (accept('-')) + result.push_back('-'); + + return true; + } + + bool accept_ws() + { + while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20')) + advance(); + return true; + } + + bool accept_boolean(value& result) + { + if (accept("true")) + { + result = true; + return true; + } + else if (accept("false")) + { + result = false; + return true; + } + + return false; + } + + bool accept_null(value& result) + { + if (accept("null")) + { + result = nullptr; + return true; + } + + return false; + } + + bool accept(char c) + { + if (expect(c)) + return advance(); + else + return false; + } + + bool accept(const char* str) + { + auto last = m_Cursor; + + while (*str) + { + if (eof() || *str != *m_Cursor) + { + m_Cursor = last; + return false; + } + + advance(); + ++str; + } + + return true; + } + + int peek() const + { + if (!eof()) + return *m_Cursor; + else + return -1; + } + + bool expect(char c) + { + return peek() == c; + } + + bool advance(int count = 1) + { + if (m_Cursor + count > m_End) + { + m_Cursor = m_End; + return false; + } + + m_Cursor += count; + + return true; + } + + bool eof() const + { + return m_Cursor == m_End; + } + + const char* m_Cursor; + const char* m_End; +}; + +value value::parse(const string& data) +{ + auto p = parser(data.c_str(), data.c_str() + data.size()); + + auto v = p.parse(); + + return v; +} + +} // namespace crude_json diff --git a/src/ui/NodeEditor/Source/crude_json.h b/src/ui/NodeEditor/Source/crude_json.h new file mode 100644 index 0000000..2450e63 --- /dev/null +++ b/src/ui/NodeEditor/Source/crude_json.h @@ -0,0 +1,223 @@ +// Crude implementation of JSON value object and parser. +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +# ifndef __CRUDE_JSON_H__ +# define __CRUDE_JSON_H__ +# pragma once + +# include +# include +# include +# include +# include +# include +# include + +# ifndef CRUDE_ASSERT +# include +# define CRUDE_ASSERT(expr) assert(expr) +# endif + +namespace crude_json { + +struct value; + +using string = std::string; +using object = std::map; +using array = std::vector; +using number = double; +using boolean = bool; +using null = std::nullptr_t; + +enum class type_t +{ + null, + object, + array, + string, + boolean, + number, + discarded +}; + +struct value +{ + value(type_t type = type_t::null): m_Type(construct(m_Storage, type)) {} + value(value&& other); + value(const value& other); + + value( null) : m_Type(construct(m_Storage, null())) {} + value( object&& v): m_Type(construct(m_Storage, std::move(v))) {} + value(const object& v): m_Type(construct(m_Storage, v)) {} + value( array&& v): m_Type(construct(m_Storage, std::move(v))) {} + value(const array& v): m_Type(construct(m_Storage, v)) {} + value( string&& v): m_Type(construct(m_Storage, std::move(v))) {} + value(const string& v): m_Type(construct(m_Storage, v)) {} + value(const char* v): m_Type(construct(m_Storage, v)) {} + value( boolean v): m_Type(construct(m_Storage, v)) {} + value( number v): m_Type(construct(m_Storage, v)) {} + ~value() { destruct(m_Storage, m_Type); } + + value& operator=(value&& other) { if (this != &other) { value(std::move(other)).swap(*this); } return *this; } + value& operator=(const value& other) { if (this != &other) { value( other).swap(*this); } return *this; } + + value& operator=( null) { auto other = value( ); swap(other); return *this; } + value& operator=( object&& v) { auto other = value(std::move(v)); swap(other); return *this; } + value& operator=(const object& v) { auto other = value( v); swap(other); return *this; } + value& operator=( array&& v) { auto other = value(std::move(v)); swap(other); return *this; } + value& operator=(const array& v) { auto other = value( v); swap(other); return *this; } + value& operator=( string&& v) { auto other = value(std::move(v)); swap(other); return *this; } + value& operator=(const string& v) { auto other = value( v); swap(other); return *this; } + value& operator=(const char* v) { auto other = value( v); swap(other); return *this; } + value& operator=( boolean v) { auto other = value( v); swap(other); return *this; } + value& operator=( number v) { auto other = value( v); swap(other); return *this; } + + type_t type() const { return m_Type; } + + operator type_t() const { return m_Type; } + + value& operator[](size_t index); + const value& operator[](size_t index) const; + value& operator[](const string& key); + const value& operator[](const string& key) const; + + bool contains(const string& key) const; + + void push_back(const value& value); + void push_back(value&& value); + + bool is_primitive() const { return is_string() || is_number() || is_boolean() || is_null(); } + bool is_structured() const { return is_object() || is_array(); } + bool is_null() const { return m_Type == type_t::null; } + bool is_object() const { return m_Type == type_t::object; } + bool is_array() const { return m_Type == type_t::array; } + bool is_string() const { return m_Type == type_t::string; } + bool is_boolean() const { return m_Type == type_t::boolean; } + bool is_number() const { return m_Type == type_t::number; } + bool is_discarded() const { return m_Type == type_t::discarded; } + + template const T& get() const; + template T& get(); + + string dump(const int indent = -1, const char indent_char = ' ') const; + + void swap(value& other); + + inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); } + + // Returns discarded value for invalid inputs. + static value parse(const string& data); + +private: + struct parser; + + // VS2015: std::max() is not constexpr yet. +# define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a)) +# define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c) +# define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d) +# define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e) + enum + { + max_size = CRUDE_MAX5( sizeof(string), sizeof(object), sizeof(array), sizeof(number), sizeof(boolean)), + max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), alignof(number), alignof(boolean)) + }; +# undef CRUDE_MAX5 +# undef CRUDE_MAX4 +# undef CRUDE_MAX3 +# undef CRUDE_MAX2 + using storage_t = std::aligned_storage::type; + + static object* object_ptr( storage_t& storage) { return reinterpret_cast< object*>(&storage); } + static const object* object_ptr(const storage_t& storage) { return reinterpret_cast(&storage); } + static array* array_ptr( storage_t& storage) { return reinterpret_cast< array*>(&storage); } + static const array* array_ptr(const storage_t& storage) { return reinterpret_cast(&storage); } + static string* string_ptr( storage_t& storage) { return reinterpret_cast< string*>(&storage); } + static const string* string_ptr(const storage_t& storage) { return reinterpret_cast(&storage); } + static boolean* boolean_ptr( storage_t& storage) { return reinterpret_cast< boolean*>(&storage); } + static const boolean* boolean_ptr(const storage_t& storage) { return reinterpret_cast(&storage); } + static number* number_ptr( storage_t& storage) { return reinterpret_cast< number*>(&storage); } + static const number* number_ptr(const storage_t& storage) { return reinterpret_cast(&storage); } + + static type_t construct(storage_t& storage, type_t type) + { + switch (type) + { + case type_t::object: new (&storage) object(); break; + case type_t::array: new (&storage) array(); break; + case type_t::string: new (&storage) string(); break; + case type_t::boolean: new (&storage) boolean(); break; + case type_t::number: new (&storage) number(); break; + default: break; + } + + return type; + } + + static type_t construct(storage_t& storage, null) { return type_t::null; } + static type_t construct(storage_t& storage, object&& value) { new (&storage) object(std::forward(value)); return type_t::object; } + static type_t construct(storage_t& storage, const object& value) { new (&storage) object(value); return type_t::object; } + static type_t construct(storage_t& storage, array&& value) { new (&storage) array(std::forward(value)); return type_t::array; } + static type_t construct(storage_t& storage, const array& value) { new (&storage) array(value); return type_t::array; } + static type_t construct(storage_t& storage, string&& value) { new (&storage) string(std::forward(value)); return type_t::string; } + static type_t construct(storage_t& storage, const string& value) { new (&storage) string(value); return type_t::string; } + static type_t construct(storage_t& storage, const char* value) { new (&storage) string(value); return type_t::string; } + static type_t construct(storage_t& storage, boolean value) { new (&storage) boolean(value); return type_t::boolean; } + static type_t construct(storage_t& storage, number value) { new (&storage) number(value); return type_t::number; } + + static void destruct(storage_t& storage, type_t type) + { + switch (type) + { + case type_t::object: object_ptr(storage)->~object(); break; + case type_t::array: array_ptr(storage)->~array(); break; + case type_t::string: string_ptr(storage)->~string(); break; + default: break; + } + } + + struct dump_context_t + { + std::ostringstream out; + const int indent = -1; + const char indent_char = ' '; + + // VS2015: Aggregate initialization isn't a thing yet. + dump_context_t(const int indent, const char indent_char) + : indent(indent) + , indent_char(indent_char) + { + } + + void write_indent(int level); + void write_separator(); + void write_newline(); + }; + + void dump(dump_context_t& context, int level) const; + + storage_t m_Storage; + type_t m_Type; +}; + +template <> inline const object& value::get() const { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); } +template <> inline const array& value::get() const { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); } +template <> inline const string& value::get() const { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); } +template <> inline const boolean& value::get() const { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); } +template <> inline const number& value::get() const { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); } + +template <> inline object& value::get() { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); } +template <> inline array& value::get() { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); } +template <> inline string& value::get() { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); } +template <> inline boolean& value::get() { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); } +template <> inline number& value::get() { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); } + + +} // namespace crude_json + +# endif // __CRUDE_JSON_H__ \ No newline at end of file diff --git a/src/ui/NodeEditor/Source/imgui_bezier_math.h b/src/ui/NodeEditor/Source/imgui_bezier_math.h new file mode 100644 index 0000000..09b76e7 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_bezier_math.h @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_BEZIER_MATH_H__ +# define __IMGUI_BEZIER_MATH_H__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_extra_math.h" + + +//------------------------------------------------------------------------------ +template +struct ImCubicBezierPointsT +{ + T P0; + T P1; + T P2; + T P3; +}; +using ImCubicBezierPoints = ImCubicBezierPointsT; + + +//------------------------------------------------------------------------------ +// Low-level Bezier curve sampling. +template inline T ImLinearBezier(const T& p0, const T& p1, float t); +template inline T ImLinearBezierDt(const T& p0, const T& p1, float t); +template inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t); +template inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t); +template inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t); +template inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t); + + +// High-level Bezier sampling, automatically collapse to lower level Bezier curves if control points overlap. +template inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t); +template inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t); +template inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t); +template inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t); + + +// Calculate approximate length of Cubic Bezier curve. +template inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3); +template inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve); + + +// Splits Cubic Bezier curve into two curves. +template +struct ImCubicBezierSplitResultT +{ + ImCubicBezierPointsT Left; + ImCubicBezierPointsT Right; +}; +using ImCubicBezierSplitResult = ImCubicBezierSplitResultT; + +template inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t); +template inline ImCubicBezierSplitResultT ImCubicBezierSplit(const ImCubicBezierPointsT& curve, float t); + + +// Returns bounding rectangle of Cubic Bezier curve. +inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3); +inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve); + + +// Project point on Cubic Bezier curve. +struct ImProjectResult +{ + ImVec2 Point; // Point on curve + float Time; // [0 - 1] + float Distance; // Distance to curve +}; + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions = 100); +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions = 100); + + +// Calculate intersection between line and a Cubic Bezier curve. +struct ImCubicBezierIntersectResult +{ + int Count; + ImVec2 Points[3]; +}; + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1); +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line); + + +// Adaptive Cubic Bezier subdivision. +enum ImCubicBezierSubdivideFlags +{ + ImCubicBezierSubdivide_None = 0, + ImCubicBezierSubdivide_SkipFirst = 1 +}; + +struct ImCubicBezierSubdivideSample +{ + ImVec2 Point; + ImVec2 Tangent; +}; + +using ImCubicBezierSubdivideCallback = void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer); + +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); + + +// F has signature void(const ImCubicBezierSubdivideSample& p) +template inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); +template inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); + +// Fixed step Cubic Bezier subdivision. +struct ImCubicBezierFixedStepSample +{ + float T; + float Length; + ImVec2 Point; + bool BreakSearch; +}; + +using ImCubicBezierFixedStepCallback = void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer); + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f); +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f); + + +// F has signature void(const ImCubicBezierFixedStepSample& p) +template inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f); +template inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f); + + +//------------------------------------------------------------------------------ +# include "imgui_bezier_math.inl" + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_BEZIER_MATH_H__ diff --git a/src/ui/NodeEditor/Source/imgui_bezier_math.inl b/src/ui/NodeEditor/Source/imgui_bezier_math.inl new file mode 100644 index 0000000..19bc429 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_bezier_math.inl @@ -0,0 +1,668 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_BEZIER_MATH_INL__ +# define __IMGUI_BEZIER_MATH_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_bezier_math.h" +# include // used in ImCubicBezierFixedStep + + +//------------------------------------------------------------------------------ +template +inline T ImLinearBezier(const T& p0, const T& p1, float t) +{ + return p0 + t * (p1 - p0); +} + +template +inline T ImLinearBezierDt(const T& p0, const T& p1, float t) +{ + return p1 - p0; +} + +template +inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t) +{ + const auto a = 1 - t; + + return a * a * p0 + 2 * t * a * p1 + t * t * p2; +} + +template +inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t) +{ + return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1); +} + +template +inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto a = 1 - t; + const auto b = a * a * a; + const auto c = t * t * t; + + return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3; +} + +template +inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto a = 1 - t; + const auto b = a * a; + const auto c = t * t; + const auto d = 2 * t * a; + + return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c; +} + +template +inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; + const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; + + if (cp0_zero && cp1_zero) + return ImLinearBezier(p0, p3, t); + else if (cp0_zero) + return ImQuadraticBezier(p0, p2, p3, t); + else if (cp1_zero) + return ImQuadraticBezier(p0, p1, p3, t); + else + return ImCubicBezier(p0, p1, p2, p3, t); +} + +template +inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +template +inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; + const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; + + if (cp0_zero && cp1_zero) + return ImLinearBezierDt(p0, p3, t); + else if (cp0_zero) + return ImQuadraticBezierDt(p0, p2, p3, t); + else if (cp1_zero) + return ImQuadraticBezierDt(p0, p1, p3, t); + else + return ImCubicBezierDt(p0, p1, p2, p3, t); +} + +template +inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +template +inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3) +{ + // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + static const float t_values[] = + { + -0.0640568928626056260850430826247450385909f, + 0.0640568928626056260850430826247450385909f, + -0.1911188674736163091586398207570696318404f, + 0.1911188674736163091586398207570696318404f, + -0.3150426796961633743867932913198102407864f, + 0.3150426796961633743867932913198102407864f, + -0.4337935076260451384870842319133497124524f, + 0.4337935076260451384870842319133497124524f, + -0.5454214713888395356583756172183723700107f, + 0.5454214713888395356583756172183723700107f, + -0.6480936519369755692524957869107476266696f, + 0.6480936519369755692524957869107476266696f, + -0.7401241915785543642438281030999784255232f, + 0.7401241915785543642438281030999784255232f, + -0.8200019859739029219539498726697452080761f, + 0.8200019859739029219539498726697452080761f, + -0.8864155270044010342131543419821967550873f, + 0.8864155270044010342131543419821967550873f, + -0.9382745520027327585236490017087214496548f, + 0.9382745520027327585236490017087214496548f, + -0.9747285559713094981983919930081690617411f, + 0.9747285559713094981983919930081690617411f, + -0.9951872199970213601799974097007368118745f, + 0.9951872199970213601799974097007368118745f + }; + + // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) + static const float c_values[] = + { + 0.1279381953467521569740561652246953718517f, + 0.1279381953467521569740561652246953718517f, + 0.1258374563468282961213753825111836887264f, + 0.1258374563468282961213753825111836887264f, + 0.1216704729278033912044631534762624256070f, + 0.1216704729278033912044631534762624256070f, + 0.1155056680537256013533444839067835598622f, + 0.1155056680537256013533444839067835598622f, + 0.1074442701159656347825773424466062227946f, + 0.1074442701159656347825773424466062227946f, + 0.0976186521041138882698806644642471544279f, + 0.0976186521041138882698806644642471544279f, + 0.0861901615319532759171852029837426671850f, + 0.0861901615319532759171852029837426671850f, + 0.0733464814110803057340336152531165181193f, + 0.0733464814110803057340336152531165181193f, + 0.0592985849154367807463677585001085845412f, + 0.0592985849154367807463677585001085845412f, + 0.0442774388174198061686027482113382288593f, + 0.0442774388174198061686027482113382288593f, + 0.0285313886289336631813078159518782864491f, + 0.0285313886289336631813078159518782864491f, + 0.0123412297999871995468056670700372915759f, + 0.0123412297999871995468056670700372915759f + }; + + static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), ""); + + auto arc = [p0, p1, p2, p3](float t) + { + const auto p = ImCubicBezierDt(p0, p1, p2, p3, t); + const auto l = ImLength(p); + return l; + }; + + const auto z = 0.5f; + const auto n = sizeof(t_values) / sizeof(*t_values); + + auto accumulator = 0.0f; + for (size_t i = 0; i < n; ++i) + { + const auto t = z * t_values[i] + z; + accumulator += c_values[i] * arc(t); + } + + return z * accumulator; +} + +template +inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve) +{ + return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); +} + +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto z1 = t; + const auto z2 = z1 * z1; + const auto z3 = z1 * z1 * z1; + const auto s1 = z1 - 1; + const auto s2 = s1 * s1; + const auto s3 = s1 * s1 * s1; + + return ImCubicBezierSplitResultT + { + ImCubicBezierPointsT + { + p0, + z1 * p1 - s1 * p0, + z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0, + z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0 + }, + ImCubicBezierPointsT + { + z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3, + z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3, + z1 * p2 - s1 * p3, + p3, + } + }; +} + +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3) +{ + auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0; + auto b = 6 * p0 - 12 * p1 + 6 * p2; + auto c = 3 * p1 - 3 * p0; + auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c); + + auto tl = ImMin(p0, p3); + auto rb = ImMax(p0, p3); + +# define IM_VEC2_INDEX(v, i) *(&v.x + i) + + for (int i = 0; i < 2; ++i) + { + if (IM_VEC2_INDEX(delta_squared, i) >= 0) + { + auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i)); + + auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i)); + if (t0 > 0 && t0 < 1) + { + auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0); + IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); + IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); + } + + auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i)); + if (t1 > 0 && t1 < 1) + { + auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1); + IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); + IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); + } + } + } + +# undef IM_VEC2_INDEX + + return ImRect(tl, rb); +} + +inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve) +{ + return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); +} + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions) +{ + // http://pomax.github.io/bezierinfo/#projections + + const float epsilon = 1e-5f; + const float fixed_step = 1.0f / static_cast(subdivisions - 1); + + ImProjectResult result; + result.Point = point; + result.Time = 0.0f; + result.Distance = FLT_MAX; + + // Step 1: Coarse check + for (int i = 0; i < subdivisions; ++i) + { + auto t = i * fixed_step; + auto p = ImCubicBezier(p0, p1, p2, p3, t); + auto s = point - p; + auto d = ImDot(s, s); + + if (d < result.Distance) + { + result.Point = p; + result.Time = t; + result.Distance = d; + } + } + + if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon) + { + result.Distance = ImSqrt(result.Distance); + return result; + } + + // Step 2: Fine check + auto left = result.Time - fixed_step; + auto right = result.Time + fixed_step; + auto step = fixed_step * 0.1f; + + for (auto t = left; t < right + step; t += step) + { + auto p = ImCubicBezier(p0, p1, p2, p3, t); + auto s = point - p; + auto d = ImDot(s, s); + + if (d < result.Distance) + { + result.Point = p; + result.Time = t; + result.Distance = d; + } + } + + result.Distance = ImSqrt(result.Distance); + + return result; +} + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions) +{ + return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions); +} + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1) +{ + auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int + { + int count = 0; + + auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; }; + + auto A = b / a; + auto B = c / a; + auto C = d / a; + + auto Q = (3 * B - ImPow(A, 2)) / 9; + auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54; + auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant + + if (D >= 0) // complex or duplicate roots + { + auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f)); + auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f)); + + roots[0] = -A / 3 + (S + T); // real root + roots[1] = -A / 3 - (S + T) / 2; // real part of complex root + roots[2] = -A / 3 - (S + T) / 2; // real part of complex root + auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair + + // discard complex roots + if (Im != 0) + count = 1; + else + count = 3; + } + else // distinct real roots + { + auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3))); + + roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3; + roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3; + roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3; + + count = 3; + } + + return count; + }; + + // https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as + // + // Start with Bezier using Bernstein polynomials for weighting functions: + // (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3 + // + // Expand and collect terms to form linear combinations of original Bezier + // controls. This ends up with a vector cubic in t: + // (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0 + // /\ /\ /\ /\ + // || || || || + // c3 c2 c1 c0 + + // Calculate the coefficients + auto c3 = -p0 + 3 * p1 - 3 * p2 + p3; + auto c2 = 3 * p0 - 6 * p1 + 3 * p2; + auto c1 = -3 * p0 + 3 * p1; + auto c0 = p0; + + // Convert line to normal form: ax + by + c = 0 + auto a = a1.y - a0.y; + auto b = a0.x - a1.x; + auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x); + + // Rotate each cubic coefficient using line for new coordinate system? + // Find roots of rotated cubic + float roots[3]; + auto rootCount = cubic_roots( + a * c3.x + b * c3.y, + a * c2.x + b * c2.y, + a * c1.x + b * c1.y, + a * c0.x + b * c0.y + c, + roots); + + // Any roots in closed interval [0,1] are intersections on Bezier, but + // might not be on the line segment. + // Find intersections and calculate point coordinates + + auto min = ImMin(a0, a1); + auto max = ImMax(a0, a1); + + ImCubicBezierIntersectResult result; + auto points = result.Points; + + for (int i = 0; i < rootCount; ++i) + { + auto root = roots[i]; + + if (0 <= root && root <= 1) + { + // We're within the Bezier curve + // Find point on Bezier + auto p = ImCubicBezier(p0, p1, p2, p3, root); + + // See if point is on line segment + // Had to make special cases for vertical and horizontal lines due + // to slight errors in calculation of p00 + if (a0.x == a1.x) + { + if (min.y <= p.y && p.y <= max.y) + *points++ = p; + } + else if (a0.y == a1.y) + { + if (min.x <= p.x && p.x <= max.x) + *points++ = p; + } + else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y) + { + *points++ = p; + } + } + } + + result.Count = static_cast(points - result.Points); + + return result; +} + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line) +{ + return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B); +} + +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); +} + +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + struct Tesselator + { + ImCubicBezierSubdivideCallback Callback; + void* UserPointer; + float TesselationTollerance; + ImCubicBezierSubdivideFlags Flags; + + void Commit(const ImVec2& p, const ImVec2& t) + { + ImCubicBezierSubdivideSample sample; + sample.Point = p; + sample.Tangent = t; + Callback(sample, UserPointer); + } + + void Subdivide(const ImCubicBezierPoints& curve, int level = 0) + { + float dx = curve.P3.x - curve.P0.x; + float dy = curve.P3.y - curve.P0.y; + float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx); + float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy)) + { + Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f)); + } + else if (level < 10) + { + const auto p12 = (curve.P0 + curve.P1) * 0.5f; + const auto p23 = (curve.P1 + curve.P2) * 0.5f; + const auto p34 = (curve.P2 + curve.P3) * 0.5f; + const auto p123 = (p12 + p23) * 0.5f; + const auto p234 = (p23 + p34) * 0.5f; + const auto p1234 = (p123 + p234) * 0.5f; + + Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1); + Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1); + } + } + }; + + if (tess_tol < 0) + tess_tol = 1.118f; // sqrtf(1.25f) + + Tesselator tesselator; + tesselator.Callback = callback; + tesselator.UserPointer = user_pointer; + tesselator.TesselationTollerance = tess_tol * tess_tol; + tesselator.Flags = flags; + + if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst)) + tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f)); + + tesselator.Subdivide(curve, 0); +} + +template inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(p); + }; + + ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); +} + +template inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(p); + }; + + ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags); +} + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) +{ + if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0) + return; + + ImCubicBezierFixedStepSample sample; + sample.T = 0.0f; + sample.Length = 0.0f; + sample.Point = p0; + sample.BreakSearch = false; + + callback(sample, user_pointer); + if (sample.BreakSearch) + return; + + const auto length = ImCubicBezierLength(p0, p1, p2, p3); + const auto point_count = static_cast(length / step) + (overshoot ? 2 : 1); + const auto t_min = 0.0f; + const auto t_max = step * point_count / length; + const auto t_0 = (t_min + t_max) * 0.5f; + + // #todo: replace map with ImVector + binary search + std::map cache; + for (int point_index = 1; point_index < point_count; ++point_index) + { + const auto targetLength = point_index * step; + + float t_start = t_min; + float t_end = t_max; + float t = t_0; + + float t_best = t; + float error_best = length; + + while (true) + { + auto cacheIt = cache.find(t); + if (cacheIt == cache.end()) + { + const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left; + const auto length = ImCubicBezierLength(front); + + cacheIt = cache.emplace(t, length).first; + } + + const auto length = cacheIt->second; + const auto error = targetLength - length; + + if (error < error_best) + { + error_best = error; + t_best = t; + } + + if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error) + { + sample.T = t; + sample.Length = length; + sample.Point = ImCubicBezier(p0, p1, p2, p3, t); + + callback(sample, user_pointer); + if (sample.BreakSearch) + return; + + break; + } + else if (error < 0.0f) + t_end = t; + else // if (error > 0.0f) + t_start = t; + + t = (t_start + t_end) * 0.5f; + } + } +} + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) +{ + ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); +} + +// F has signature void(const ImCubicBezierFixedStepSample& p) +template +inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) +{ + auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(sample); + }; + + ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); +} + +template +inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) +{ + auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(sample); + }; + + ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); +} + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_BEZIER_MATH_INL__ diff --git a/src/ui/NodeEditor/Source/imgui_canvas.cpp b/src/ui/NodeEditor/Source/imgui_canvas.cpp new file mode 100644 index 0000000..6845d73 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_canvas.cpp @@ -0,0 +1,452 @@ +# define IMGUI_DEFINE_MATH_OPERATORS +# include "imgui_canvas.h" +# include + +// https://stackoverflow.com/a/36079786 +# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ + \ + template \ + class __trait_name__ \ + { \ + using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ + struct no_type {char x[2];}; \ + using yes_type = char; \ + \ + struct base { void __member_name__() {}}; \ + struct mixin : public base, public check_type {}; \ + \ + template struct aux {}; \ + \ + template static no_type test(aux<&U::__member_name__>*); \ + template static yes_type test(...); \ + \ + public: \ + \ + static constexpr bool value = (sizeof(yes_type) == sizeof(test(0))); \ + } + +namespace ImCanvasDetails { + +DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); + +struct FringeScaleRef +{ + // Overload is present when ImDrawList does have _FringeScale member variable. + template + static float& Get(typename std::enable_if::value, T>::type* drawList) + { + return drawList->_FringeScale; + } + + // Overload is present when ImDrawList does not have _FringeScale member variable. + template + static float& Get(typename std::enable_if::value, T>::type*) + { + static float placeholder = 1.0f; + return placeholder; + } +}; + +} // namespace ImCanvasDetails + +// Returns a reference to _FringeScale extension to ImDrawList +// +// If ImDrawList does not have _FringeScale a placeholder is returned. +static inline float& ImFringeScaleRef(ImDrawList* drawList) +{ + using namespace ImCanvasDetails; + return FringeScaleRef::Get(drawList); +} + +static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); } + +bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size) +{ + return Begin(ImGui::GetID(id), size); +} + +bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size) +{ + IM_ASSERT(m_InBeginEnd == false); + + m_WidgetPosition = ImGui::GetCursorScreenPos(); + m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail()); + m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize); + m_DrawList = ImGui::GetWindowDrawList(); + + UpdateViewTransformPosition(); + + if (ImGui::IsClippedEx(m_WidgetRect, id)) // IsClippedEx dropped its 3rd arg upstream in ImGui + return false; + + // Save current channel, so we can assert when user + // call canvas API with different one. + m_ExpectedChannel = m_DrawList->_Splitter._Current; + + // #debug: Canvas content. + //m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, IM_COL32(0, 0, 0, 64)); + m_DrawList->AddRect(m_WidgetRect.Min, m_WidgetRect.Max, IM_COL32(255, 0, 255, 64)); + + ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); + +# if IMGUI_EX_CANVAS_DEFERED() + m_Ranges.resize(0); +# endif + + SaveInputState(); + + EnterLocalSpace(); + + // Emit dummy widget matching bounds of the canvas. + ImGui::SetCursorScreenPos(m_ViewRect.Min); + ImGui::Dummy(m_ViewRect.GetSize()); + + ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); + + m_InBeginEnd = true; + + return true; +} + +void ImGuiEx::Canvas::End() +{ + // If you're here your call to Begin() returned false, + // or Begin() wasn't called at all. + IM_ASSERT(m_InBeginEnd == true); + + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + //auto& io = ImGui::GetIO(); + + // Check: Unmatched calls to Suspend() / Resume(). Please check your code. + IM_ASSERT(m_SuspendCounter == 0); + + LeaveLocalSpace(); + + // Emit dummy widget matching bounds of the canvas. + ImGui::SetCursorScreenPos(m_WidgetPosition); + ImGui::Dummy(m_WidgetSize); + + // #debug: Rect around canvas. Content should be inside these bounds. + //m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255)); + + m_InBeginEnd = false; +} + +void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale) +{ + SetView(CanvasView(origin, scale)); +} + +void ImGuiEx::Canvas::SetView(const CanvasView& view) +{ + if (m_InBeginEnd) + LeaveLocalSpace(); + + if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y) + { + m_View.Origin = view.Origin; + + UpdateViewTransformPosition(); + } + + if (m_View.Scale != view.Scale) + { + m_View.Scale = view.Scale; + m_View.InvScale = view.InvScale; + } + + if (m_InBeginEnd) + EnterLocalSpace(); +} + +void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint) +{ + auto view = CalcCenterView(canvasPoint); + SetView(view); +} + +ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImVec2& canvasPoint) const +{ + auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f); + auto localOffset = canvasPoint - localCenter; + auto offset = FromLocalV(localOffset); + + return CanvasView{ m_View.Origin - offset, m_View.Scale }; +} + +void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect) +{ + auto view = CalcCenterView(canvasRect); + + SetView(view); +} + +ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImRect& canvasRect) const +{ + auto canvasRectSize = canvasRect.GetSize(); + + if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f) + return View(); + + auto widgetAspectRatio = m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f; + auto canvasRectAspectRatio = canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f; + + if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f) + return View(); + + auto newOrigin = m_View.Origin; + auto newScale = m_View.Scale; + if (canvasRectAspectRatio > widgetAspectRatio) + { + // width span across view + newScale = m_WidgetSize.x / canvasRectSize.x; + newOrigin = canvasRect.Min * -newScale; + newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f; + } + else + { + // height span across view + newScale = m_WidgetSize.y / canvasRectSize.y; + newOrigin = canvasRect.Min * -newScale; + newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f; + } + + return CanvasView{ newOrigin, newScale }; +} + +void ImGuiEx::Canvas::Suspend() +{ + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + if (m_SuspendCounter == 0) + LeaveLocalSpace(); + + ++m_SuspendCounter; +} + +void ImGuiEx::Canvas::Resume() +{ + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + // Check: Number of calls to Resume() do not match calls to Suspend(). Please check your code. + IM_ASSERT(m_SuspendCounter > 0); + if (--m_SuspendCounter == 0) + EnterLocalSpace(); +} + +ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const +{ + return point * m_View.Scale + m_ViewTransformPosition; +} + +ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, const CanvasView& view) const +{ + return point * view.Scale + view.Origin + m_WidgetPosition; +} + +ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const +{ + return vector * m_View.Scale; +} + +ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, const CanvasView& view) const +{ + return vector * view.Scale; +} + +ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const +{ + return (point - m_ViewTransformPosition) * m_View.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, const CanvasView& view) const +{ + return (point - view.Origin - m_WidgetPosition) * view.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const +{ + return vector * m_View.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, const CanvasView& view) const +{ + return vector * view.InvScale; +} + +ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const +{ + ImRect result; + result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale; + result.Max = (m_WidgetSize - view.Origin) * view.InvScale; + return result; +} + +void ImGuiEx::Canvas::UpdateViewTransformPosition() +{ + m_ViewTransformPosition = m_View.Origin + m_WidgetPosition; +} + +void ImGuiEx::Canvas::SaveInputState() +{ + auto& io = ImGui::GetIO(); + m_MousePosBackup = io.MousePos; + m_MousePosPrevBackup = io.MousePosPrev; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + m_MouseClickedPosBackup[i] = io.MouseClickedPos[i]; + + // Record cursor max to prevent scrollbars from appearing. + m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos; +} + +void ImGuiEx::Canvas::RestoreInputState() +{ + auto& io = ImGui::GetIO(); + io.MousePos = m_MousePosBackup; + io.MousePosPrev = m_MousePosPrevBackup; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + io.MouseClickedPos[i] = m_MouseClickedPosBackup[i]; + ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup; +} + +void ImGuiEx::Canvas::EnterLocalSpace() +{ + // Prepare ImDrawList for drawing in local coordinate system: + // - determine visible part of the canvas + // - start unique draw command + // - add clip rect matching canvas size + // - record current command index + // - record current vertex write index + + // Determine visible part of the canvas. Make it before + // adding new command, to avoid round rip where command + // is removed in PopClipRect() and added again next PushClipRect(). + ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true); + auto clipped_clip_rect = m_DrawList->_ClipRectStack.back(); + ImGui::PopClipRect(); + + // Make sure we do not share draw command with anyone. We don't want to mess + // with someones clip rectangle. + + // #FIXME: + // This condition is not enough to avoid when user choose + // to use channel splitter. + // + // To deal with Suspend()/Resume() calls empty draw command + // is always added then splitter is active. Otherwise + // channel merger will collapse our draw command one with + // different clip rectangle. + // + // More investigation is needed. To get to the bottom of this. + if ((!m_DrawList->CmdBuffer.empty() && m_DrawList->CmdBuffer.back().ElemCount > 0) || m_DrawList->_Splitter._Count > 1) + m_DrawList->AddDrawCmd(); + +# if IMGUI_EX_CANVAS_DEFERED() + m_Ranges.resize(m_Ranges.Size + 1); + m_CurrentRange = &m_Ranges.back(); + m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size - 1, 0); + m_CurrentRange->BeginVertexIndex = m_DrawList->_VtxCurrentIdx; +# endif + m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size - 1, 0); + m_DrawListStartVertexIndex = m_DrawList->_VtxCurrentIdx; + + // Clip rectangle in parent canvas space and move it to local space. + clipped_clip_rect.x = (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale; + clipped_clip_rect.y = (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale; + clipped_clip_rect.z = (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale; + clipped_clip_rect.w = (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale; + ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false); + + // Transform mouse position to local space. + auto& io = ImGui::GetIO(); + io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale; + io.MousePosPrev = (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale; + + m_ViewRect = CalcViewRect(m_View);; + + auto& fringeScale = ImFringeScaleRef(m_DrawList); + m_LastFringeScale = fringeScale; + fringeScale *= m_View.InvScale; +} + +void ImGuiEx::Canvas::LeaveLocalSpace() +{ + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + +# if IMGUI_EX_CANVAS_DEFERED() + IM_ASSERT(m_CurrentRange != nullptr); + + m_CurrentRange->EndVertexIndex = m_DrawList->_VtxCurrentIdx; + m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size(); + if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex) + { + // Drop empty range + m_Ranges.resize(m_Ranges.Size - 1); + } + m_CurrentRange = nullptr; +# endif + + // Move vertices to screen space. + auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex; + auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx; + + // If canvas view is not scaled take a faster path. + if (m_View.Scale != 1.0f) + { + while (vertex < vertexEnd) + { + vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x; + vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y; + ++vertex; + } + + // Move clip rectangles to screen space. + for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i) + { + auto& command = m_DrawList->CmdBuffer[i]; + command.ClipRect.x = command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x; + command.ClipRect.y = command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y; + command.ClipRect.z = command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x; + command.ClipRect.w = command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y; + } + } + else + { + while (vertex < vertexEnd) + { + vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x; + vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y; + ++vertex; + } + + // Move clip rectangles to screen space. + for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i) + { + auto& command = m_DrawList->CmdBuffer[i]; + command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x; + command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y; + command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x; + command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y; + } + } + + auto& fringeScale = ImFringeScaleRef(m_DrawList); + fringeScale = m_LastFringeScale; + + // And pop \o/ + ImGui::PopClipRect(); + + RestoreInputState(); +} diff --git a/src/ui/NodeEditor/Source/imgui_canvas.h b/src/ui/NodeEditor/Source/imgui_canvas.h new file mode 100644 index 0000000..f6b1a85 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_canvas.h @@ -0,0 +1,250 @@ +// Canvas widget - view over infinite virtual space. +// +// Canvas allows you to draw your widgets anywhere over infinite space and provide +// view over it with support for panning and scaling. +// +// When you enter a canvas ImGui is moved to virtual space which mean: +// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top left corner +// of the canvas on the screen (this can be changed usign CanvasView()). +// - Mouse input is brought to canvas space, so widgets works as usual. +// - Everything you draw with ImDrawList will be in virtual space. +// +// By default origin point is on top left corner of canvas widget. It can be +// changed with call to CanvasView() where you can specify what part of space +// should be viewed by setting viewport origin point and scale. Current state +// can be queried with CanvasViewOrigin() and CanvasViewScale(). +// +// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can query +// it using CanvasContentMin/Max/Size functions. They are useful if you to not specify +// canvas size in which case all free space is used. +// +// Bounds of visible region of infinite space can be queried using CanvasViewMin/Max/Size +// functions. Everything that is drawn outside of this region will be clipped +// as usual in ImGui. +// +// While drawing inside canvas you can translate position from world (usual ImGui space) +// to virtual space and back usign CanvasFromWorld()/CanvasToWorld(). +// +// Canvas can be nested in each other (they are regular widgets after all). There +// is a way to transform position between current and parent canvas with +// CanvasFromParent()/CanvasToParent(). +// +// Sometimes in more elaborate scenarios you want to move out canvas virtual space, +// do something and came back. You can do that with SuspendCanvas() and ResumeCanvas(). +// +// Note: +// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() scope. +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +# ifndef __IMGUI_EX_CANVAS_H__ +# define __IMGUI_EX_CANVAS_H__ +# pragma once + +# include +# include // ImRect, ImFloor + +namespace ImGuiEx { + +struct CanvasView +{ + ImVec2 Origin; + float Scale = 1.0f; + float InvScale = 1.0f; + + CanvasView() = default; + CanvasView(const ImVec2& origin, float scale) + : Origin(origin) + , Scale(scale) + , InvScale(scale ? 1.0f / scale : 0.0f) + { + } + + void Set(const ImVec2& origin, float scale) + { + *this = CanvasView(origin, scale); + } +}; + +// Canvas widget represent view over infinite plane. +// +// It acts like a child window without scroll bars with +// ability to zoom to specific part of canvas plane. +// +// Widgets are clipped according to current view exactly +// same way ImGui do. To avoid `missing widgets` artifacts first +// setup visible region with SetView() then draw content. +// +// Everything drawn with ImDrawList betwen calls to Begin()/End() +// will be drawn on canvas plane. This behavior can be suspended +// by calling Suspend() and resumed by calling Resume(). +// +// Warning: +// Please do not interleave canvas with use of channel splitter. +// Keep channel splitter contained inside canvas or always +// call canvas functions from same channel. +struct Canvas +{ + // Begins drawing content of canvas plane. + // + // When false is returned that mean canvas is not visible to the + // user can drawing should be skipped and End() not called. + // When true is returned drawing must be ended with call to End(). + // + // If any size component is equal to zero or less canvas will + // automatically expand to all available area on that axis. + // So (0, 300) will take horizontal space and have height + // of 300 points. (0, 0) will take all remaining space of + // the window. + // + // You can query size of the canvas while it is being drawn + // by calling Rect(). + bool Begin(const char* id, const ImVec2& size); + bool Begin(ImGuiID id, const ImVec2& size); + + // Ends interaction with canvas plane. + // + // Must be called only when Begin() retuned true. + void End(); + + // Sets visible region of canvas plane. + // + // Origin is an offset of infinite plane origin from top left + // corner of the canvas. + // + // Scale greater than 1 make canvas content be bigger, less than 1 smaller. + void SetView(const ImVec2& origin, float scale); + void SetView(const CanvasView& view); + + // Centers view over specific point on canvas plane. + // + // View will be centered on specific point by changing origin + // but not scale. + void CenterView(const ImVec2& canvasPoint); + + // Calculates view over specific point on canvas plane. + CanvasView CalcCenterView(const ImVec2& canvasPoint) const; + + // Centers view over specific rectangle on canvas plane. + // + // Whole rectangle will fit in canvas view. This will affect both + // origin and scale. + void CenterView(const ImRect& canvasRect); + + // Calculates view over specific rectangle on canvas plane. + CanvasView CalcCenterView(const ImRect& canvasRect) const; + + // Suspends canvas by returning to normal ImGui transformation space. + // While suspended UI will not be drawn on canvas plane. + // + // Calls to Suspend()/Resume() are symetrical. Each call to Suspend() + // must be matched with call to Resume(). + void Suspend(); + void Resume(); + + // Transforms point from canvas plane to ImGui. + ImVec2 FromLocal(const ImVec2& point) const; + ImVec2 FromLocal(const ImVec2& point, const CanvasView& view) const; + + // Transforms vector from canvas plant to ImGui. + ImVec2 FromLocalV(const ImVec2& vector) const; + ImVec2 FromLocalV(const ImVec2& vector, const CanvasView& view) const; + + // Transforms point from ImGui to canvas plane. + ImVec2 ToLocal(const ImVec2& point) const; + ImVec2 ToLocal(const ImVec2& point, const CanvasView& view) const; + + // Transforms vector from ImGui to canvas plane. + ImVec2 ToLocalV(const ImVec2& vector) const; + ImVec2 ToLocalV(const ImVec2& vector, const CanvasView& view) const; + + // Returns widget bounds. + // + // Note: + // Rect is valid after call to Begin(). + const ImRect& Rect() const { return m_WidgetRect; } + + // Returns visible region on canvas plane (in canvas plane coordinates). + const ImRect& ViewRect() const { return m_ViewRect; } + + // Calculates visible region for view. + ImRect CalcViewRect(const CanvasView& view) const; + + // Returns current view. + const CanvasView& View() const { return m_View; } + + // Returns origin of the view. + // + // Origin is an offset of infinite plane origin from top left + // corner of the canvas. + const ImVec2& ViewOrigin() const { return m_View.Origin; } + + // Returns scale of the view. + float ViewScale() const { return m_View.Scale; } + + // Returns true if canvas is suspended. + // + // See: Suspend()/Resume() + bool IsSuspended() const { return m_SuspendCounter > 0; } + +private: +# define IMGUI_EX_CANVAS_DEFERED() 0 + +# if IMGUI_EX_CANVAS_DEFERED() + struct Range + { + int BeginVertexIndex = 0; + int EndVertexIndex = 0; + int BeginComandIndex = 0; + int EndCommandIndex = 0; + }; +# endif + + void UpdateViewTransformPosition(); + + void SaveInputState(); + void RestoreInputState(); + + void EnterLocalSpace(); + void LeaveLocalSpace(); + + bool m_InBeginEnd = false; + + ImVec2 m_WidgetPosition; + ImVec2 m_WidgetSize; + ImRect m_WidgetRect; + + ImDrawList* m_DrawList = nullptr; + int m_ExpectedChannel = 0; + +# if IMGUI_EX_CANVAS_DEFERED() + ImVector m_Ranges; + Range* m_CurrentRange = nullptr; +# endif + + int m_DrawListCommadBufferSize = 0; + int m_DrawListStartVertexIndex = 0; + + CanvasView m_View; + ImRect m_ViewRect; + + ImVec2 m_ViewTransformPosition; + + int m_SuspendCounter = 0; + + float m_LastFringeScale = 1.0f; + + ImVec2 m_MousePosBackup; + ImVec2 m_MousePosPrevBackup; + ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)]; + ImVec2 m_WindowCursorMaxBackup; +}; + +} // namespace ImGuiEx + +# endif // __IMGUI_EX_CANVAS_H__ \ No newline at end of file diff --git a/src/ui/NodeEditor/Source/imgui_extra_math.h b/src/ui/NodeEditor/Source/imgui_extra_math.h new file mode 100644 index 0000000..5664c5c --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_extra_math.h @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_EXTRA_MATH_H__ +# define __IMGUI_EXTRA_MATH_H__ +# pragma once + + +//------------------------------------------------------------------------------ +# include +# ifndef IMGUI_DEFINE_MATH_OPERATORS +# define IMGUI_DEFINE_MATH_OPERATORS +# endif +# include + + +//------------------------------------------------------------------------------ +struct ImLine +{ + ImVec2 A, B; +}; + + +//------------------------------------------------------------------------------ +// Modern ImGui (1.89.4+) already defines these four operators in imgui.h +// itself when IMGUI_DEFINE_MATH_OPERATORS is set before its first include — +// guard against redefinition against this project's ImGui version. +# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +inline bool operator==(const ImVec2& lhs, const ImVec2& rhs); +inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs); +inline ImVec2 operator*(const float lhs, const ImVec2& rhs); +inline ImVec2 operator-(const ImVec2& lhs); +# endif + + +//------------------------------------------------------------------------------ +inline float ImLength(float v); +inline float ImLength(const ImVec2& v); +inline float ImLengthSqr(float v); +inline ImVec2 ImNormalized(const ImVec2& v); + + +//------------------------------------------------------------------------------ +inline bool ImRect_IsEmpty(const ImRect& rect); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b); +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b); +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b); + + + +//------------------------------------------------------------------------------ +namespace ImEasing { + +template +inline V EaseOutQuad(V b, V c, T t) +{ + return b - c * (t * (t - 2)); +} + +} // namespace ImEasing + + +//------------------------------------------------------------------------------ +# include "imgui_extra_math.inl" + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_EXTRA_MATH_H__ diff --git a/src/ui/NodeEditor/Source/imgui_extra_math.inl b/src/ui/NodeEditor/Source/imgui_extra_math.inl new file mode 100644 index 0000000..ff46736 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_extra_math.inl @@ -0,0 +1,191 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_EXTRA_MATH_INL__ +# define __IMGUI_EXTRA_MATH_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_extra_math.h" + + +//------------------------------------------------------------------------------ +// See guard note in imgui_extra_math.h — already provided by imgui.h on +// this project's ImGui version. +# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) +{ + return lhs.x == rhs.x && lhs.y == rhs.y; +} + +inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) +{ + return lhs.x != rhs.x || lhs.y != rhs.y; +} + +inline ImVec2 operator*(const float lhs, const ImVec2& rhs) +{ + return ImVec2(lhs * rhs.x, lhs * rhs.y); +} + +inline ImVec2 operator-(const ImVec2& lhs) +{ + return ImVec2(-lhs.x, -lhs.y); +} +# endif + + +//------------------------------------------------------------------------------ +inline float ImLength(float v) +{ + return v; +} + +inline float ImLength(const ImVec2& v) +{ + return ImSqrt(ImLengthSqr(v)); +} + +inline float ImLengthSqr(float v) +{ + return v * v; +} + +inline ImVec2 ImNormalized(const ImVec2& v) +{ + return v * ImInvLength(v, 0.0f); +} + + + + +//------------------------------------------------------------------------------ +inline bool ImRect_IsEmpty(const ImRect& rect) +{ + return rect.Min.x >= rect.Max.x + || rect.Min.y >= rect.Max.y; +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge) +{ + if (!snap_to_edge && rect.Contains(p)) + return p; + + return ImVec2( + (p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x), + (p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y) + ); +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius) +{ + auto point = ImRect_ClosestPoint(rect, p, snap_to_edge); + + const auto offset = p - point; + const auto distance_sq = offset.x * offset.x + offset.y * offset.y; + if (distance_sq <= 0) + return point; + + const auto distance = ImSqrt(distance_sq); + + return point + offset * (ImMin(distance, radius) * (1.0f / distance)); +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other) +{ + ImVec2 result; + if (other.Min.x >= rect.Max.x) + result.x = rect.Max.x; + else if (other.Max.x <= rect.Min.x) + result.x = rect.Min.x; + else + result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2; + + if (other.Min.y >= rect.Max.y) + result.y = rect.Max.y; + else if (other.Max.y <= rect.Min.y) + result.y = rect.Min.y; + else + result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2; + + return result; +} + +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b) +{ + ImLine result; + result.A = ImRect_ClosestPoint(rect_a, rect_b); + result.B = ImRect_ClosestPoint(rect_b, rect_a); + + auto distribute = [](float& a, float& b, float a0, float a1, float b0, float b1) + { + if (a0 >= b1 || a1 <= b0) + return; + + const auto aw = a1 - a0; + const auto bw = b1 - b0; + + if (aw > bw) + { + b = b0 + bw - bw * (a - a0) / aw; + a = b; + } + else if (aw < bw) + { + a = a0 + aw - aw * (b - b0) / bw; + b = a; + } + }; + + distribute(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x); + distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y); + + return result; +} + +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b) +{ + auto line = ImRect_ClosestLine(rect_a, rect_b); + if (radius_a < 0) + radius_a = 0; + if (radius_b < 0) + radius_b = 0; + + if (radius_a == 0 && radius_b == 0) + return line; + + const auto offset = line.B - line.A; + const auto length_sq = offset.x * offset.x + offset.y * offset.y; + const auto radius_a_sq = radius_a * radius_a; + const auto radius_b_sq = radius_b * radius_b; + + if (length_sq <= 0) + return line; + + const auto length = ImSqrt(length_sq); + const auto direction = ImVec2(offset.x / length, offset.y / length); + + const auto total_radius_sq = radius_a_sq + radius_b_sq; + if (total_radius_sq > length_sq) + { + const auto scale = length / (radius_a + radius_b); + radius_a *= scale; + radius_b *= scale; + } + + line.A = line.A + (direction * radius_a); + line.B = line.B - (direction * radius_b); + + return line; +} + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_EXTRA_MATH_INL__ diff --git a/src/ui/NodeEditor/Source/imgui_node_editor.cpp b/src/ui/NodeEditor/Source/imgui_node_editor.cpp new file mode 100644 index 0000000..6dac961 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_node_editor.cpp @@ -0,0 +1,5445 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# include "imgui_node_editor_internal.h" +# include // snprintf +# include +# include +# include +# include +# include +# include +# include +# include + +// https://stackoverflow.com/a/8597498 +# define DECLARE_HAS_NESTED(Name, Member) \ + \ + template \ + struct has_nested_ ## Name \ + { \ + typedef char yes; \ + typedef yes(&no)[2]; \ + \ + template static yes test(decltype(U::Member)*); \ + template static no test(...); \ + \ + static bool const value = sizeof(test(0)) == sizeof(yes); \ + }; + + +namespace ax { +namespace NodeEditor { +namespace Detail { + +// Modern ImGui (1.87+) has no legacy KeyMap/GetKeyIndex indirection — +// ImGuiKey values are usable directly with IsKeyPressed(). The upstream +// SFINAE shim here existed to bridge older ImGui API shapes; not needed +// against the ImGui version this project vendors. +static inline ImGuiKey GetKeyIndexForF() +{ + return ImGuiKey_F; +} + +static inline ImGuiKey GetKeyIndexForD() +{ + return ImGuiKey_D; +} + +} // namespace Detail +} // namespace NodeEditor +} // namespace ax + + +//------------------------------------------------------------------------------ +namespace ed = ax::NodeEditor::Detail; + + +//------------------------------------------------------------------------------ +static const int c_BackgroundChannelCount = 1; +static const int c_LinkChannelCount = 4; +static const int c_UserLayersCount = 5; + +static const int c_UserLayerChannelStart = 0; +static const int c_BackgroundChannelStart = c_UserLayerChannelStart + c_UserLayersCount; +static const int c_LinkStartChannel = c_BackgroundChannelStart + c_BackgroundChannelCount; +static const int c_NodeStartChannel = c_LinkStartChannel + c_LinkChannelCount; + +static const int c_BackgroundChannel_SelectionRect = c_BackgroundChannelStart + 0; + +static const int c_UserChannel_Content = c_UserLayerChannelStart + 1; +static const int c_UserChannel_Grid = c_UserLayerChannelStart + 2; +static const int c_UserChannel_HintsBackground = c_UserLayerChannelStart + 3; +static const int c_UserChannel_Hints = c_UserLayerChannelStart + 4; + +static const int c_LinkChannel_Selection = c_LinkStartChannel + 0; +static const int c_LinkChannel_Links = c_LinkStartChannel + 1; +static const int c_LinkChannel_Flow = c_LinkStartChannel + 2; +static const int c_LinkChannel_NewLink = c_LinkStartChannel + 3; + +static const int c_ChannelsPerNode = 5; +static const int c_NodeBaseChannel = 0; +static const int c_NodeBackgroundChannel = 1; +static const int c_NodeUserBackgroundChannel = 2; +static const int c_NodePinChannel = 3; +static const int c_NodeContentChannel = 4; + +static const float c_GroupSelectThickness = 6.0f; // canvas pixels +static const float c_LinkSelectThickness = 5.0f; // canvas pixels +static const float c_NavigationZoomMargin = 0.1f; // percentage of visible bounds +static const float c_MouseZoomDuration = 0.15f; // seconds +static const float c_SelectionFadeOutDuration = 0.15f; // seconds +// UsdLayerManager local patch: pan with the middle mouse button (Maya-style +// Alt+MMB, matching the viewport) instead of upstream's right button, which +// this app reserves for context menus. Re-apply if this vendored copy is +// ever refreshed from upstream. +static const auto c_ScrollButtonIndex = 2; + +// UsdLayerManager local patch: optional input-diagnostics sink. When set by +// the application, BuildControl dumps its hit-test state on every mouse +// click so misrouted presses (node vs. background) can be debugged from the +// application log. Temporary debugging aid — safe to remove. +void (*g_AxNodeEditorDebugLog)(const char*) = nullptr; + + +//------------------------------------------------------------------------------ +# if defined(_DEBUG) && defined(_WIN32) +extern "C" __declspec(dllimport) void __stdcall OutputDebugStringA(const char* string); + +static void LogV(const char* fmt, va_list args) +{ + const int buffer_size = 1024; + static char buffer[1024]; + + int w = vsnprintf(buffer, buffer_size - 1, fmt, args); + buffer[buffer_size - 1] = 0; + + ImGui::LogText("\nNode Editor: %s", buffer); + + OutputDebugStringA("NodeEditor: "); + OutputDebugStringA(buffer); + OutputDebugStringA("\n"); +} +# endif + +void ed::Log(const char* fmt, ...) +{ +# if defined(_DEBUG) && defined(_WIN32) + va_list args; + va_start(args, fmt); + LogV(fmt, args); + va_end(args); +# endif +} + + +//------------------------------------------------------------------------------ +static bool IsGroup(const ed::Node* node) +{ + if (node && node->m_Type == ed::NodeType::Group) + return true; + else + return false; +} + + +//------------------------------------------------------------------------------ +static void ImDrawListSplitter_Grow(ImDrawList* draw_list, ImDrawListSplitter* splitter, int channels_count) +{ + IM_ASSERT(splitter != nullptr); + IM_ASSERT(splitter->_Count <= channels_count); + + if (splitter->_Count == 1) + { + splitter->Split(draw_list, channels_count); + return; + } + + int old_channels_count = splitter->_Channels.Size; + if (old_channels_count < channels_count) + splitter->_Channels.resize(channels_count); + int old_used_channels_count = splitter->_Count; + splitter->_Count = channels_count; + + for (int i = old_used_channels_count; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&splitter->_Channels[i]) ImDrawChannel(); + } + else + { + splitter->_Channels[i]._CmdBuffer.resize(0); + splitter->_Channels[i]._IdxBuffer.resize(0); + } + if (splitter->_Channels[i]._CmdBuffer.Size == 0) + { + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = draw_list->_ClipRectStack.back(); + draw_cmd.TexRef = draw_list->_TextureStack.back(); // ImDrawCmd::TextureId/_TextureIdStack renamed upstream in ImGui 1.92 + splitter->_Channels[i]._CmdBuffer.push_back(draw_cmd); + } + } +} + +static void ImDrawList_ChannelsGrow(ImDrawList* draw_list, int channels_count) +{ + ImDrawListSplitter_Grow(draw_list, &draw_list->_Splitter, channels_count); +} + +static void ImDrawListSplitter_SwapChannels(ImDrawListSplitter* splitter, int left, int right) +{ + IM_ASSERT(left < splitter->_Count && right < splitter->_Count); + if (left == right) + return; + + auto currentChannel = splitter->_Current; + + auto* leftCmdBuffer = &splitter->_Channels[left]._CmdBuffer; + auto* leftIdxBuffer = &splitter->_Channels[left]._IdxBuffer; + auto* rightCmdBuffer = &splitter->_Channels[right]._CmdBuffer; + auto* rightIdxBuffer = &splitter->_Channels[right]._IdxBuffer; + + leftCmdBuffer->swap(*rightCmdBuffer); + leftIdxBuffer->swap(*rightIdxBuffer); + + if (currentChannel == left) + splitter->_Current = right; + else if (currentChannel == right) + splitter->_Current = left; +} + +static void ImDrawList_SwapChannels(ImDrawList* drawList, int left, int right) +{ + ImDrawListSplitter_SwapChannels(&drawList->_Splitter, left, right); +} + +//static void ImDrawList_TransformChannel_Inner(ImVector& vtxBuffer, const ImVector& idxBuffer, const ImVector& cmdBuffer, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset) +//{ +// auto idxRead = idxBuffer.Data; +// +// int indexOffset = 0; +// for (auto& cmd : cmdBuffer) +// { +// auto idxCount = cmd.ElemCount; +// +// if (idxCount == 0) continue; +// +// auto minIndex = idxRead[indexOffset]; +// auto maxIndex = idxRead[indexOffset]; +// +// for (auto i = 1u; i < idxCount; ++i) +// { +// auto idx = idxRead[indexOffset + i]; +// minIndex = std::min(minIndex, idx); +// maxIndex = ImMax(maxIndex, idx); +// } +// +// for (auto vtx = vtxBuffer.Data + minIndex, vtxEnd = vtxBuffer.Data + maxIndex + 1; vtx < vtxEnd; ++vtx) +// { +// vtx->pos.x = (vtx->pos.x + preOffset.x) * scale.x + postOffset.x; +// vtx->pos.y = (vtx->pos.y + preOffset.y) * scale.y + postOffset.y; +// } +// +// indexOffset += idxCount; +// } +//} + +//static void ImDrawList_TransformChannels(ImDrawList* drawList, int begin, int end, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset) +//{ +// int lastCurrentChannel = drawList->_ChannelsCurrent; +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(0); +// +// auto& vtxBuffer = drawList->VtxBuffer; +// +// if (begin == 0 && begin != end) +// { +// ImDrawList_TransformChannel_Inner(vtxBuffer, drawList->IdxBuffer, drawList->CmdBuffer, preOffset, scale, postOffset); +// ++begin; +// } +// +// for (int channelIndex = begin; channelIndex < end; ++channelIndex) +// { +// auto& channel = drawList->_Channels[channelIndex]; +// ImDrawList_TransformChannel_Inner(vtxBuffer, channel.IdxBuffer, channel.CmdBuffer, preOffset, scale, postOffset); +// } +// +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(lastCurrentChannel); +//} + +//static void ImDrawList_ClampClipRects_Inner(ImVector& cmdBuffer, const ImVec4& clipRect, const ImVec2& offset) +//{ +// for (auto& cmd : cmdBuffer) +// { +// cmd.ClipRect.x = ImMax(cmd.ClipRect.x + offset.x, clipRect.x); +// cmd.ClipRect.y = ImMax(cmd.ClipRect.y + offset.y, clipRect.y); +// cmd.ClipRect.z = std::min(cmd.ClipRect.z + offset.x, clipRect.z); +// cmd.ClipRect.w = std::min(cmd.ClipRect.w + offset.y, clipRect.w); +// } +//} + +//static void ImDrawList_TranslateAndClampClipRects(ImDrawList* drawList, int begin, int end, const ImVec2& offset) +//{ +// int lastCurrentChannel = drawList->_ChannelsCurrent; +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(0); +// +// auto clipRect = drawList->_ClipRectStack.back(); +// +// if (begin == 0 && begin != end) +// { +// ImDrawList_ClampClipRects_Inner(drawList->CmdBuffer, clipRect, offset); +// ++begin; +// } +// +// for (int channelIndex = begin; channelIndex < end; ++channelIndex) +// { +// auto& channel = drawList->_Channels[channelIndex]; +// ImDrawList_ClampClipRects_Inner(channel.CmdBuffer, clipRect, offset); +// } +// +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(lastCurrentChannel); +//} + +static void ImDrawList_PathBezierOffset(ImDrawList* drawList, float offset, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3) +{ + using namespace ed; + + auto acceptPoint = [drawList, offset](const ImCubicBezierSubdivideSample& r) + { + drawList->PathLineTo(r.Point + ImNormalized(ImVec2(-r.Tangent.y, r.Tangent.x)) * offset); + }; + + ImCubicBezierSubdivide(acceptPoint, p0, p1, p2, p3); +} + +/* +static void ImDrawList_PolyFillScanFlood(ImDrawList *draw, std::vector* poly, ImColor color, int gap = 1, float strokeWidth = 1.0f) +{ + std::vector scanHits; + ImVec2 min, max; // polygon min/max points + auto io = ImGui::GetIO(); + float y; + bool isMinMaxDone = false; + unsigned int polysize = poly->size(); + + // find the orthagonal bounding box + // probably can put this as a predefined + if (!isMinMaxDone) + { + min.x = min.y = FLT_MAX; + max.x = max.y = FLT_MIN; + for (auto p : *poly) + { + if (p.x < min.x) min.x = p.x; + if (p.y < min.y) min.y = p.y; + if (p.x > max.x) max.x = p.x; + if (p.y > max.y) max.y = p.y; + } + isMinMaxDone = true; + } + + // Bounds check + if ((max.x < 0) || (min.x > io.DisplaySize.x) || (max.y < 0) || (min.y > io.DisplaySize.y)) return; + + // Vertically clip + if (min.y < 0) min.y = 0; + if (max.y > io.DisplaySize.y) max.y = io.DisplaySize.y; + + // so we know we start on the outside of the object we step out by 1. + min.x -= 1; + max.x += 1; + + // Initialise our starting conditions + y = min.y; + + // Go through each scan line iteratively, jumping by 'gap' pixels each time + while (y < max.y) + { + scanHits.clear(); + + { + int jump = 1; + ImVec2 fp = poly->at(0); + + for (size_t i = 0; i < polysize - 1; i++) + { + ImVec2 pa = poly->at(i); + ImVec2 pb = poly->at(i + 1); + + // jump double/dud points + if (pa.x == pb.x && pa.y == pb.y) continue; + + // if we encounter our hull/poly start point, then we've now created the + // closed + // hull, jump the next segment and reset the first-point + if ((!jump) && (fp.x == pb.x) && (fp.y == pb.y)) + { + if (i < polysize - 2) + { + fp = poly->at(i + 2); + jump = 1; + i++; + } + } + else + { + jump = 0; + } + + // test to see if this segment makes the scan-cut. + if ((pa.y > pb.y && y < pa.y && y > pb.y) || (pa.y < pb.y && y > pa.y && y < pb.y)) + { + ImVec2 intersect; + + intersect.y = y; + if (pa.x == pb.x) + { + intersect.x = pa.x; + } + else + { + intersect.x = (pb.x - pa.x) / (pb.y - pa.y) * (y - pa.y) + pa.x; + } + scanHits.push_back(intersect); + } + } + + // Sort the scan hits by X, so we have a proper left->right ordering + sort(scanHits.begin(), scanHits.end(), [](ImVec2 const &a, ImVec2 const &b) { return a.x < b.x; }); + + // generate the line segments. + { + int i = 0; + int l = scanHits.size() - 1; // we need pairs of points, this prevents segfault. + for (i = 0; i < l; i += 2) + { + draw->AddLine(scanHits[i], scanHits[i + 1], color, strokeWidth); + } + } + } + y += gap; + } // for each scan line + scanHits.clear(); +} +*/ + +static void ImDrawList_AddBezierWithArrows(ImDrawList* drawList, const ImCubicBezierPoints& curve, float thickness, + float startArrowSize, float startArrowWidth, float endArrowSize, float endArrowWidth, + bool fill, ImU32 color, float strokeThickness) +{ + using namespace ax; + + if ((color >> 24) == 0) + return; + + const auto half_thickness = thickness * 0.5f; + + if (fill) + { + drawList->AddBezierCubic(curve.P0, curve.P1, curve.P2, curve.P3, color, thickness); // AddBezierCurve renamed AddBezierCubic upstream in ImGui 1.80 + + if (startArrowSize > 0.0f) + { + const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); + const auto start_n = ImVec2(-start_dir.y, start_dir.x); + const auto half_width = startArrowWidth * 0.5f; + const auto tip = curve.P0 - start_dir * startArrowSize; + + drawList->PathLineTo(curve.P0 - start_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(curve.P0 + start_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(tip); + drawList->PathFillConvex(color); + } + + if (endArrowSize > 0.0f) + { + const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); + const auto end_n = ImVec2( -end_dir.y, end_dir.x); + const auto half_width = endArrowWidth * 0.5f; + const auto tip = curve.P3 + end_dir * endArrowSize; + + drawList->PathLineTo(curve.P3 + end_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(curve.P3 - end_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(tip); + drawList->PathFillConvex(color); + } + } + else + { + if (startArrowSize > 0.0f) + { + const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); + const auto start_n = ImVec2(-start_dir.y, start_dir.x); + const auto half_width = startArrowWidth * 0.5f; + const auto tip = curve.P0 - start_dir * startArrowSize; + + if (half_width > half_thickness) + drawList->PathLineTo(curve.P0 - start_n * half_width); + drawList->PathLineTo(tip); + if (half_width > half_thickness) + drawList->PathLineTo(curve.P0 + start_n * half_width); + } + + ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P0, curve.P1, curve.P2, curve.P3); + + if (endArrowSize > 0.0f) + { + const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); + const auto end_n = ImVec2( -end_dir.y, end_dir.x); + const auto half_width = endArrowWidth * 0.5f; + const auto tip = curve.P3 + end_dir * endArrowSize; + + if (half_width > half_thickness) + drawList->PathLineTo(curve.P3 + end_n * half_width); + drawList->PathLineTo(tip); + if (half_width > half_thickness) + drawList->PathLineTo(curve.P3 - end_n * half_width); + } + + ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P3, curve.P2, curve.P1, curve.P0); + + drawList->PathStroke(color, true, strokeThickness); + } +} + + + + +//------------------------------------------------------------------------------ +// +// Pin +// +//------------------------------------------------------------------------------ +void ed::Pin::Draw(ImDrawList* drawList, DrawFlags flags) +{ + if (flags & Hovered) + { + drawList->ChannelsSetCurrent(m_Node->m_Channel + c_NodePinChannel); + + drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max, + m_Color, m_Rounding, m_Corners); + + if (m_BorderWidth > 0.0f) + { + FringeScaleScope fringe(1.0f); + drawList->AddRect(m_Bounds.Min, m_Bounds.Max, + m_BorderColor, m_Rounding, m_Corners, m_BorderWidth); + } + + if (!Editor->IsSelected(m_Node)) + m_Node->Draw(drawList, flags); + } +} + +ImVec2 ed::Pin::GetClosestPoint(const ImVec2& p) const +{ + return ImRect_ClosestPoint(m_Pivot, p, true, m_Radius + m_ArrowSize); +} + +ImLine ed::Pin::GetClosestLine(const Pin* pin) const +{ + return ImRect_ClosestLine(m_Pivot, pin->m_Pivot, m_Radius + m_ArrowSize, pin->m_Radius + pin->m_ArrowSize); +} + + + + +//------------------------------------------------------------------------------ +// +// Node +// +//------------------------------------------------------------------------------ +bool ed::Node::AcceptDrag() +{ + m_DragStart = m_Bounds.Min; + return true; +} + +void ed::Node::UpdateDrag(const ImVec2& offset) +{ + auto size = m_Bounds.GetSize(); + m_Bounds.Min = ImFloor(m_DragStart + offset); + m_Bounds.Max = m_Bounds.Min + size; +} + +bool ed::Node::EndDrag() +{ + return m_Bounds.Min != m_DragStart; +} + +void ed::Node::Draw(ImDrawList* drawList, DrawFlags flags) +{ + if (flags == Detail::Object::None) + { + drawList->ChannelsSetCurrent(m_Channel + c_NodeBackgroundChannel); + + drawList->AddRectFilled( + m_Bounds.Min, + m_Bounds.Max, + m_Color, m_Rounding); + + if (IsGroup(this)) + { + drawList->AddRectFilled( + m_GroupBounds.Min, + m_GroupBounds.Max, + m_GroupColor, m_GroupRounding); + + if (m_GroupBorderWidth > 0.0f) + { + FringeScaleScope fringe(1.0f); + + drawList->AddRect( + m_GroupBounds.Min, + m_GroupBounds.Max, + m_GroupBorderColor, m_GroupRounding, 15, m_GroupBorderWidth); + } + } + +# if 0 + // #debug: highlight group regions + auto drawRect = [drawList](const ImRect& rect, ImU32 color) + { + if (ImRect_IsEmpty(rect)) return; + drawList->AddRectFilled(rect.Min, rect.Max, color); + }; + + drawRect(GetRegionBounds(NodeRegion::Top), IM_COL32(255, 0, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Bottom), IM_COL32(255, 0, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Left), IM_COL32(0, 255, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Right), IM_COL32(0, 255, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::TopLeft), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::TopRight), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::BottomLeft), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::BottomRight), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::Center), IM_COL32(0, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::Header), IM_COL32(0, 255, 255, 64)); +# endif + + DrawBorder(drawList, m_BorderColor, m_BorderWidth); + } + else if (flags & Selected) + { + const auto borderColor = Editor->GetColor(StyleColor_SelNodeBorder); + const auto& editorStyle = Editor->GetStyle(); + + drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); + + DrawBorder(drawList, borderColor, editorStyle.SelectedNodeBorderWidth); + } + else if (!IsGroup(this) && (flags & Hovered)) + { + const auto borderColor = Editor->GetColor(StyleColor_HovNodeBorder); + const auto& editorStyle = Editor->GetStyle(); + + drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); + + DrawBorder(drawList, borderColor, editorStyle.HoveredNodeBorderWidth); + } +} + +void ed::Node::DrawBorder(ImDrawList* drawList, ImU32 color, float thickness) +{ + if (thickness > 0.0f) + { + drawList->AddRect(m_Bounds.Min, m_Bounds.Max, + color, m_Rounding, 15, thickness); + } +} + +void ed::Node::GetGroupedNodes(std::vector& result, bool append) +{ + if (!append) + result.resize(0); + + if (!IsGroup(this)) + return; + + const auto firstNodeIndex = result.size(); + Editor->FindNodesInRect(m_GroupBounds, result, true, false); + + for (auto index = firstNodeIndex; index < result.size(); ++index) + result[index]->GetGroupedNodes(result, true); +} + +ImRect ed::Node::GetRegionBounds(NodeRegion region) const +{ + if (m_Type == NodeType::Node) + { + if (region == NodeRegion::Header) + return m_Bounds; + } + else if (m_Type == NodeType::Group) + { + const float activeAreaMinimumSize = ImMax(ImMax( + Editor->GetView().InvScale * c_GroupSelectThickness, + m_GroupBorderWidth), c_GroupSelectThickness); + const float minimumSize = activeAreaMinimumSize * 5; + + auto bounds = m_Bounds; + if (bounds.GetWidth() < minimumSize) + bounds.Expand(ImVec2(minimumSize - bounds.GetWidth(), 0.0f)); + if (bounds.GetHeight() < minimumSize) + bounds.Expand(ImVec2(0.0f, minimumSize - bounds.GetHeight())); + + if (region == NodeRegion::Top) + { + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize; + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + return bounds; + } + else if (region == NodeRegion::Bottom) + { + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize; + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + return bounds; + } + else if (region == NodeRegion::Left) + { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } + else if (region == NodeRegion::Right) + { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } + else if (region == NodeRegion::TopLeft) + { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; + return bounds; + } + else if (region == NodeRegion::TopRight) + { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; + return bounds; + } + else if (region == NodeRegion::BottomRight) + { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; + return bounds; + } + else if (region == NodeRegion::BottomLeft) + { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; + return bounds; + } + else if (region == NodeRegion::Header) + { + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); + return bounds; + } + else if (region == NodeRegion::Center) + { + bounds.Max.x -= activeAreaMinimumSize; + bounds.Min.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } + } + + return ImRect(); +} + +ed::NodeRegion ed::Node::GetRegion(const ImVec2& point) const +{ + if (m_Type == NodeType::Node) + { + if (m_Bounds.Contains(point)) + return NodeRegion::Header; + else + return NodeRegion::None; + } + else if (m_Type == NodeType::Group) + { + static const NodeRegion c_Regions[] = + { + // Corners first, they may overlap other regions. + NodeRegion::TopLeft, + NodeRegion::TopRight, + NodeRegion::BottomLeft, + NodeRegion::BottomRight, + NodeRegion::Header, + NodeRegion::Top, + NodeRegion::Bottom, + NodeRegion::Left, + NodeRegion::Right, + NodeRegion::Center + }; + + for (auto region : c_Regions) + { + auto bounds = GetRegionBounds(region); + if (bounds.Contains(point)) + return region; + } + } + + return NodeRegion::None; +} + + + + +//------------------------------------------------------------------------------ +// +// Link +// +//------------------------------------------------------------------------------ +void ed::Link::Draw(ImDrawList* drawList, DrawFlags flags) +{ + if (flags == None) + { + drawList->ChannelsSetCurrent(c_LinkChannel_Links); + + Draw(drawList, m_Color, 0.0f); + } + else if (flags & Selected) + { + const auto borderColor = Editor->GetColor(StyleColor_SelLinkBorder); + + drawList->ChannelsSetCurrent(c_LinkChannel_Selection); + + Draw(drawList, borderColor, 4.5f); + } + else if (flags & Hovered) + { + const auto borderColor = Editor->GetColor(StyleColor_HovLinkBorder); + + drawList->ChannelsSetCurrent(c_LinkChannel_Selection); + + Draw(drawList, borderColor, 2.0f); + } +} + +void ed::Link::Draw(ImDrawList* drawList, ImU32 color, float extraThickness) const +{ + if (!m_IsLive) + return; + + const auto curve = GetCurve(); + + ImDrawList_AddBezierWithArrows(drawList, curve, m_Thickness + extraThickness, + m_StartPin && m_StartPin->m_ArrowSize > 0.0f ? m_StartPin->m_ArrowSize + extraThickness : 0.0f, + m_StartPin && m_StartPin->m_ArrowWidth > 0.0f ? m_StartPin->m_ArrowWidth + extraThickness : 0.0f, + m_EndPin && m_EndPin->m_ArrowSize > 0.0f ? m_EndPin->m_ArrowSize + extraThickness : 0.0f, + m_EndPin && m_EndPin->m_ArrowWidth > 0.0f ? m_EndPin->m_ArrowWidth + extraThickness : 0.0f, + true, color, 1.0f); +} + +void ed::Link::UpdateEndpoints() +{ + const auto line = m_StartPin->GetClosestLine(m_EndPin); + m_Start = line.A; + m_End = line.B; +} + +ImCubicBezierPoints ed::Link::GetCurve() const +{ + auto easeLinkStrength = [](const ImVec2& a, const ImVec2& b, float strength) + { + const auto distanceX = b.x - a.x; + const auto distanceY = b.y - a.y; + const auto distance = ImSqrt(distanceX * distanceX + distanceY * distanceY); + const auto halfDistance = distance * 0.5f; + + if (halfDistance < strength) + strength = strength * ImSin(IM_PI * 0.5f * halfDistance / strength); + + return strength; + }; + + const auto startStrength = easeLinkStrength(m_Start, m_End, m_StartPin->m_Strength); + const auto endStrength = easeLinkStrength(m_Start, m_End, m_EndPin->m_Strength); + const auto cp0 = m_Start + m_StartPin->m_Dir * startStrength; + const auto cp1 = m_End + m_EndPin->m_Dir * endStrength; + + ImCubicBezierPoints result; + result.P0 = m_Start; + result.P1 = cp0; + result.P2 = cp1; + result.P3 = m_End; + + return result; +} + +bool ed::Link::TestHit(const ImVec2& point, float extraThickness) const +{ + if (!m_IsLive) + return false; + + auto bounds = GetBounds(); + if (extraThickness > 0.0f) + bounds.Expand(extraThickness); + + if (!bounds.Contains(point)) + return false; + + const auto bezier = GetCurve(); + const auto result = ImProjectOnCubicBezier(point, bezier.P0, bezier.P1, bezier.P2, bezier.P3, 50); + + return result.Distance <= m_Thickness + extraThickness; +} + +bool ed::Link::TestHit(const ImRect& rect, bool allowIntersect) const +{ + if (!m_IsLive) + return false; + + const auto bounds = GetBounds(); + + if (rect.Contains(bounds)) + return true; + + if (!allowIntersect || !rect.Overlaps(bounds)) + return false; + + const auto bezier = GetCurve(); + + const auto p0 = rect.GetTL(); + const auto p1 = rect.GetTR(); + const auto p2 = rect.GetBR(); + const auto p3 = rect.GetBL(); + + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p0, p1).Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p1, p2).Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p2, p3).Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p3, p0).Count > 0) + return true; + + return false; +} + +ImRect ed::Link::GetBounds() const +{ + if (m_IsLive) + { + const auto curve = GetCurve(); + auto bounds = ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); + + if (bounds.GetWidth() == 0.0f) + { + bounds.Min.x -= 0.5f; + bounds.Max.x += 0.5f; + } + + if (bounds.GetHeight() == 0.0f) + { + bounds.Min.y -= 0.5f; + bounds.Max.y += 0.5f; + } + + if (m_StartPin->m_ArrowSize) + { + const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); + const auto p0 = curve.P0; + const auto p1 = curve.P0 - start_dir * m_StartPin->m_ArrowSize; + const auto min = ImMin(p0, p1); + const auto max = ImMax(p0, p1); + auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); + bounds.Add(arrowBounds); + } + + if (m_EndPin->m_ArrowSize) + { + const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); + const auto p0 = curve.P3; + const auto p1 = curve.P3 + end_dir * m_EndPin->m_ArrowSize; + const auto min = ImMin(p0, p1); + const auto max = ImMax(p0, p1); + auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); + bounds.Add(arrowBounds); + } + + return bounds; + } + else + return ImRect(); +} + + + + +//------------------------------------------------------------------------------ +// +// Editor Context +// +//------------------------------------------------------------------------------ +ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config) + : m_IsFirstFrame(true) + , m_IsWindowActive(false) + , m_ShortcutsEnabled(true) + , m_Style() + , m_Nodes() + , m_Pins() + , m_Links() + , m_SelectionId(1) + , m_LastActiveLink(nullptr) + , m_PressedNode(nullptr) + , m_PressedPin(nullptr) + , m_PressedInCanvas(false) + , m_Canvas() + , m_IsCanvasVisible(false) + , m_NodeBuilder(this) + , m_HintBuilder(this) + , m_CurrentAction(nullptr) + , m_NavigateAction(this, m_Canvas) + , m_SizeAction(this) + , m_DragAction(this) + , m_SelectAction(this) + , m_ContextMenuAction(this) + , m_ShortcutAction(this) + , m_CreateItemAction(this) + , m_DeleteItemsAction(this) + , m_AnimationControllers{ &m_FlowAnimationController } + , m_FlowAnimationController(this) + , m_DoubleClickedNode(0) + , m_DoubleClickedPin(0) + , m_DoubleClickedLink(0) + , m_BackgroundClicked(false) + , m_BackgroundDoubleClicked(false) + , m_IsInitialized(false) + , m_Settings() + , m_Config(config) +{ +} + +ed::EditorContext::~EditorContext() +{ + if (m_IsInitialized) + SaveSettings(); + + for (auto link : m_Links) delete link.m_Object; + for (auto pin : m_Pins) delete pin.m_Object; + for (auto node : m_Nodes) delete node.m_Object; +} + +void ed::EditorContext::Begin(const char* id, const ImVec2& size) +{ + if (!m_IsInitialized) + { + LoadSettings(); + m_IsInitialized = true; + } + + //ImGui::LogToClipboard(); + //Log("---- begin ----"); + + for (auto node : m_Nodes) node->Reset(); + for (auto pin : m_Pins) pin->Reset(); + for (auto link : m_Links) link->Reset(); + + ImGui::PushID(id); + + auto availableContentSize = ImGui::GetContentRegionAvail(); + ImVec2 canvasSize = ImFloor(size); + if (canvasSize.x <= 0.0f) + canvasSize.x = ImMax(4.0f, availableContentSize.x); + if (canvasSize.y <= 0.0f) + canvasSize.y = ImMax(4.0f, availableContentSize.y); + + m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize); + + //ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); + //ImGui::BeginChild(id, size, false, + // ImGuiWindowFlags_NoMove | + // ImGuiWindowFlags_NoScrollbar | + // ImGuiWindowFlags_NoScrollWithMouse); + + ImGui::SetNextFrameWantCaptureKeyboard(true); // CaptureKeyboardFromApp renamed upstream in ImGui + + m_IsWindowActive = ImGui::IsWindowFocused(); + + // + m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, m_Canvas.ViewRect().GetSize()); + + // UsdLayerManager local patch: only auto-scroll-past-edge for a drag that + // actually started inside the canvas (see m_PressedInCanvas). Dragging the + // host window by its title bar moves the cursor "outside" the canvas every + // frame while a node is selected/active, which otherwise triggers this + // pan; requiring an in-canvas press origin makes a window move never pan + // the graph. + if (m_PressedInCanvas && m_CurrentAction && m_CurrentAction->IsDragging() && m_NavigateAction.MoveOverEdge()) + { + auto& io = ImGui::GetIO(); + auto offset = m_NavigateAction.GetMoveOffset(); + for (int i = 0; i < 5; ++i) + io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset; + } + else + m_NavigateAction.StopMoveOverEdge(); + + m_Canvas.SetView(m_NavigateAction.GetView()); + + auto drawList = ImGui::GetWindowDrawList(); + + // #debug #clip + //ImGui::Text("CLIP = { x=%g y=%g w=%g h=%g r=%g b=%g }", + // clipMin.x, clipMin.y, clipMax.x - clipMin.x, clipMax.y - clipMin.y, clipMax.x, clipMax.y); + + // Reserve channels for background and links + ImDrawList_ChannelsGrow(drawList, c_NodeStartChannel); + + if (HasSelectionChanged()) + ++m_SelectionId; + + m_LastSelectedObjects = m_SelectedObjects; +} + +void ed::EditorContext::End() +{ + //auto& io = ImGui::GetIO(); + auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge() + auto drawList = ImGui::GetWindowDrawList(); + //auto& editorStyle = GetStyle(); + + m_DoubleClickedNode = control.DoubleClickedNode ? control.DoubleClickedNode->m_ID : 0; + m_DoubleClickedPin = control.DoubleClickedPin ? control.DoubleClickedPin->m_ID : 0; + m_DoubleClickedLink = control.DoubleClickedLink ? control.DoubleClickedLink->m_ID : 0; + m_BackgroundClicked = control.BackgroundClicked; + m_BackgroundDoubleClicked = control.BackgroundDoubleClicked; + + //if (DoubleClickedNode) LOG_TRACE(0, "DOUBLE CLICK NODE: %d", DoubleClickedNode); + //if (DoubleClickedPin) LOG_TRACE(0, "DOUBLE CLICK PIN: %d", DoubleClickedPin); + //if (DoubleClickedLink) LOG_TRACE(0, "DOUBLE CLICK LINK: %d", DoubleClickedLink); + //if (BackgroundDoubleClicked) LOG_TRACE(0, "DOUBLE CLICK BACKGROUND", DoubleClickedLink); + + const bool isSelecting = m_CurrentAction && m_CurrentAction->AsSelect() != nullptr; + const bool isDragging = m_CurrentAction && m_CurrentAction->AsDrag() != nullptr; + //const bool isSizing = CurrentAction && CurrentAction->AsSize() != nullptr; + + // Draw nodes + for (auto node : m_Nodes) + if (node->m_IsLive && node->IsVisible()) + node->Draw(drawList); + + // Draw links + for (auto link : m_Links) + if (link->m_IsLive && link->IsVisible()) + link->Draw(drawList); + + // Highlight selected objects + { + auto selectedObjects = &m_SelectedObjects; + if (auto selectAction = m_CurrentAction ? m_CurrentAction->AsSelect() : nullptr) + selectedObjects = &selectAction->m_CandidateObjects; + + for (auto selectedObject : *selectedObjects) + if (selectedObject->IsVisible()) + selectedObject->Draw(drawList, Object::Selected); + } + + if (!isSelecting) + { + auto hoveredObject = control.HotObject; + if (auto dragAction = m_CurrentAction ? m_CurrentAction->AsDrag() : nullptr) + hoveredObject = dragAction->m_DraggedObject; + if (auto sizeAction = m_CurrentAction ? m_CurrentAction->AsSize() : nullptr) + hoveredObject = sizeAction->m_SizedNode; + + if (hoveredObject && !IsSelected(hoveredObject) && hoveredObject->IsVisible()) + hoveredObject->Draw(drawList, Object::Hovered); + } + + // Draw animations + for (auto controller : m_AnimationControllers) + controller->Draw(drawList); + + if (m_CurrentAction && !m_CurrentAction->Process(control)) + m_CurrentAction = nullptr; + + if (m_NavigateAction.m_IsActive) + m_NavigateAction.Process(control); + else + m_NavigateAction.Accept(control); + + if (nullptr == m_CurrentAction) + { + EditorAction* possibleAction = nullptr; + + auto accept = [&possibleAction, &control](EditorAction& action) + { + auto result = action.Accept(control); + + if (result == EditorAction::True) + return true; + else if (/*!possibleAction &&*/ result == EditorAction::Possible) + possibleAction = &action; + else if (result == EditorAction::Possible) + action.Reject(); + + return false; + }; + + if (accept(m_ContextMenuAction)) + m_CurrentAction = &m_ContextMenuAction; + else if (accept(m_ShortcutAction)) + m_CurrentAction = &m_ShortcutAction; + else if (accept(m_SizeAction)) + m_CurrentAction = &m_SizeAction; + else if (accept(m_DragAction)) + m_CurrentAction = &m_DragAction; + else if (accept(m_SelectAction)) + m_CurrentAction = &m_SelectAction; + else if (accept(m_CreateItemAction)) + m_CurrentAction = &m_CreateItemAction; + else if (accept(m_DeleteItemsAction)) + m_CurrentAction = &m_DeleteItemsAction; + + if (possibleAction) + ImGui::SetMouseCursor(possibleAction->GetCursor()); + + // UsdLayerManager local patch: report which action just claimed (or + // failed to claim) the mouse this frame, to correlate with the [ed] + // click/held dump in BuildControl above (control.ActiveObject etc.). + if (g_AxNodeEditorDebugLog && ImGui::IsMouseDown(0)) + { + const char* chosen = + m_CurrentAction == &m_ContextMenuAction ? "ContextMenu" : + m_CurrentAction == &m_ShortcutAction ? "Shortcut" : + m_CurrentAction == &m_SizeAction ? "Size" : + m_CurrentAction == &m_DragAction ? "Drag" : + m_CurrentAction == &m_SelectAction ? "Select" : + m_CurrentAction == &m_CreateItemAction ? "CreateItem" : + m_CurrentAction == &m_DeleteItemsAction ? "DeleteItems" : + "none"; + char buf[256]; + snprintf(buf, sizeof(buf), + "[ed] action dispatch: chosen=%s control.Active=%p control.Hot=%p bgActive=%d", + chosen, control.ActiveObject, control.HotObject, control.BackgroundActive ? 1 : 0); + g_AxNodeEditorDebugLog(buf); + } + + if (m_CurrentAction && possibleAction) + possibleAction->Reject(); + } + + if (m_CurrentAction) + ImGui::SetMouseCursor(m_CurrentAction->GetCursor()); + + // Draw selection rectangle + m_SelectAction.Draw(drawList); + + bool sortGroups = false; + if (control.ActiveNode) + { + if (!IsGroup(control.ActiveNode)) + { + // Bring active node to front + auto activeNodeIt = std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode); + std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end()); + } + else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag()) + { + // Bring content of dragged group to front + std::vector nodes; + control.ActiveNode->GetGroupedNodes(nodes); + + std::stable_partition(m_Nodes.begin(), m_Nodes.end(), [&nodes](Node* node) + { + return std::find(nodes.begin(), nodes.end(), node) == nodes.end(); + }); + + sortGroups = true; + } + } + + // Sort nodes if bounds of node changed + if (sortGroups || ((m_Settings.m_DirtyReason & (SaveReasonFlags::Position | SaveReasonFlags::Size)) != SaveReasonFlags::None)) + { + // Bring all groups before regular nodes + auto groupsItEnd = std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup); + + // Sort groups by area + std::sort(m_Nodes.begin(), groupsItEnd, [this](Node* lhs, Node* rhs) + { + const auto& lhsSize = lhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : lhs->m_GroupBounds.GetSize(); + const auto& rhsSize = rhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : rhs->m_GroupBounds.GetSize(); + + const auto lhsArea = lhsSize.x * lhsSize.y; + const auto rhsArea = rhsSize.x * rhsSize.y; + + return lhsArea > rhsArea; + }); + } + +# if 1 + // Every node has few channels assigned. Grow channel list + // to hold twice as much of channels and place them in + // node drawing order. + { + // Copy group nodes + auto liveNodeCount = static_cast(std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; })); + + // Reserve two additional channels for sorted list of channels + auto nodeChannelCount = drawList->_Splitter._Count; + ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode * liveNodeCount + c_LinkChannelCount); + + int targetChannel = nodeChannelCount; + + auto copyNode = [&targetChannel, drawList](Node* node) + { + if (!node->m_IsLive) + return; + + for (int i = 0; i < c_ChannelsPerNode; ++i) + ImDrawList_SwapChannels(drawList, node->m_Channel + i, targetChannel + i); + + node->m_Channel = targetChannel; + targetChannel += c_ChannelsPerNode; + }; + + auto groupsItEnd = std::find_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return !IsGroup(node); }); + + // Copy group nodes + std::for_each(m_Nodes.begin(), groupsItEnd, copyNode); + + // Copy links + for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel) + ImDrawList_SwapChannels(drawList, c_LinkStartChannel + i, targetChannel); + + // Copy normal nodes + std::for_each(groupsItEnd, m_Nodes.end(), copyNode); + } +# endif + + // ImGui::PopClipRect(); + + // Draw grid +# if 1 // #FIXME + { + //auto& style = ImGui::GetStyle(); + + drawList->ChannelsSetCurrent(c_UserChannel_Grid); + + ImVec2 offset = m_Canvas.ViewOrigin() * (1.0f / m_Canvas.ViewScale()); + ImU32 GRID_COLOR = GetColor(StyleColor_Grid, ImClamp(m_Canvas.ViewScale() * m_Canvas.ViewScale(), 0.0f, 1.0f)); + float GRID_SX = 32.0f;// * m_Canvas.ViewScale(); + float GRID_SY = 32.0f;// * m_Canvas.ViewScale(); + ImVec2 VIEW_POS = m_Canvas.ViewRect().Min; + ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize(); + + drawList->AddRectFilled(VIEW_POS, VIEW_POS + VIEW_SIZE, GetColor(StyleColor_Bg)); + + for (float x = fmodf(offset.x, GRID_SX); x < VIEW_SIZE.x; x += GRID_SX) + drawList->AddLine(ImVec2(x, 0.0f) + VIEW_POS, ImVec2(x, VIEW_SIZE.y) + VIEW_POS, GRID_COLOR); + for (float y = fmodf(offset.y, GRID_SY); y < VIEW_SIZE.y; y += GRID_SY) + drawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR); + } +# endif + +# if 0 + { + auto userChannel = drawList->_Splitter._Count; + auto channelsToCopy = c_UserLayersCount; + ImDrawList_ChannelsGrow(drawList, userChannel + channelsToCopy); + for (int i = 0; i < channelsToCopy; ++i) + ImDrawList_SwapChannels(drawList, userChannel + i, c_UserLayerChannelStart + i); + } +# endif + +# if 0 + { + auto preOffset = ImVec2(0, 0); + auto postOffset = m_OldCanvas.WindowScreenPos + m_OldCanvas.ClientOrigin; + auto scale = m_OldCanvas.Zoom; + + ImDrawList_TransformChannels(drawList, 0, 1, preOffset, scale, postOffset); + ImDrawList_TransformChannels(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, preOffset, scale, postOffset); + + auto clipTranslation = m_OldCanvas.WindowScreenPos - m_OldCanvas.FromScreen(m_OldCanvas.WindowScreenPos); + ImGui::PushClipRect(m_OldCanvas.WindowScreenPos + ImVec2(1, 1), m_OldCanvas.WindowScreenPos + m_OldCanvas.WindowScreenSize - ImVec2(1, 1), false); + ImDrawList_TranslateAndClampClipRects(drawList, 0, 1, clipTranslation); + ImDrawList_TranslateAndClampClipRects(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, clipTranslation); + ImGui::PopClipRect(); + + // #debug: Static grid in local space + //for (float x = 0; x < Canvas.WindowScreenSize.x; x += 100) + // drawList->AddLine(ImVec2(x, 0.0f) + Canvas.WindowScreenPos, ImVec2(x, Canvas.WindowScreenSize.y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); + //for (float y = 0; y < Canvas.WindowScreenSize.y; y += 100) + // drawList->AddLine(ImVec2(0.0f, y) + Canvas.WindowScreenPos, ImVec2(Canvas.WindowScreenSize.x, y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); + } +# endif + +# if 1 + // Move user and hint channels to top + { + // Clip plane is transformed to global space. + // These channels already have clip planes in global space, so + // we move them to clip plane. Batch transformation in canvas + // will bring them back to global space. + auto preTransformClipRect = [this, drawList](int channelIndex) + { + ImDrawChannel& channel = drawList->_Splitter._Channels[channelIndex]; + for (ImDrawCmd& cmd : channel._CmdBuffer) + { + auto a = ToCanvas(ImVec2(cmd.ClipRect.x, cmd.ClipRect.y)); + auto b = ToCanvas(ImVec2(cmd.ClipRect.z, cmd.ClipRect.w)); + cmd.ClipRect = ImVec4(a.x, a.y, b.x, b.y); + } + }; + + drawList->ChannelsSetCurrent(0); + + auto channelCount = drawList->_Splitter._Count; + ImDrawList_ChannelsGrow(drawList, channelCount + 3); + ImDrawList_SwapChannels(drawList, c_UserChannel_HintsBackground, channelCount + 0); + ImDrawList_SwapChannels(drawList, c_UserChannel_Hints, channelCount + 1); + ImDrawList_SwapChannels(drawList, c_UserChannel_Content, channelCount + 2); + + preTransformClipRect(channelCount + 0); + preTransformClipRect(channelCount + 1); + preTransformClipRect(channelCount + 2); + } +# endif + + UpdateAnimations(); + + drawList->ChannelsMerge(); + + // #debug + // drawList->AddRectFilled(ImVec2(-10.0f, -10.0f), ImVec2(10.0f, 10.0f), IM_COL32(255, 0, 255, 255)); + + // ImGui::EndChild(); + // ImGui::PopStyleColor(); + if (m_IsCanvasVisible) + m_Canvas.End(); + + // Draw border + { + auto& style = ImGui::GetStyle(); + auto borderShadoColor = style.Colors[ImGuiCol_BorderShadow]; + auto borderColor = style.Colors[ImGuiCol_Border]; + drawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), m_Canvas.Rect().Max - ImVec2(1, 1), ImColor(borderShadoColor)); + drawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, ImColor(borderColor)); + } + + // ShowMetrics(control); + + ImGui::PopID(); + + if (!m_CurrentAction && m_IsFirstFrame && !m_Settings.m_Selection.empty()) + { + ClearSelection(); + for (auto id : m_Settings.m_Selection) + if (auto object = FindObject(id)) + SelectObject(object); + } + + if (HasSelectionChanged()) + MakeDirty(SaveReasonFlags::Selection); + + if (m_Settings.m_IsDirty && !m_CurrentAction) + SaveSettings(); + + m_IsFirstFrame = false; +} + +bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness) +{ + //auto& editorStyle = GetStyle(); + + auto startPin = FindPin(startPinId); + auto endPin = FindPin(endPinId); + + if (!startPin || !startPin->m_IsLive || !endPin || !endPin->m_IsLive) + return false; + + startPin->m_HasConnection = true; + endPin->m_HasConnection = true; + + auto link = GetLink(id); + link->m_StartPin = startPin; + link->m_EndPin = endPin; + link->m_Color = color; + link->m_Thickness = thickness; + link->m_IsLive = true; + + link->UpdateEndpoints(); + + return true; +} + +void ed::EditorContext::SetNodePosition(NodeId nodeId, const ImVec2& position) +{ + auto node = FindNode(nodeId); + if (!node) + { + node = CreateNode(nodeId); + node->m_IsLive = false; + } + + if (node->m_Bounds.Min != position) + { + node->m_Bounds.Translate(position - node->m_Bounds.Min); + node->m_Bounds.Min = ImFloor(node->m_Bounds.Min); node->m_Bounds.Max = ImFloor(node->m_Bounds.Max); // ImRect::Floor removed upstream in ImGui + MakeDirty(NodeEditor::SaveReasonFlags::Position, node); + } +} + +ImVec2 ed::EditorContext::GetNodePosition(NodeId nodeId) +{ + auto node = FindNode(nodeId); + if (!node) + return ImVec2(FLT_MAX, FLT_MAX); + + return node->m_Bounds.Min; +} + +ImVec2 ed::EditorContext::GetNodeSize(NodeId nodeId) +{ + auto node = FindNode(nodeId); + if (!node) + return ImVec2(0, 0); + + return node->m_Bounds.GetSize(); +} + +void ed::EditorContext::MarkNodeToRestoreState(Node* node) +{ + node->m_RestoreState = true; +} + +void ed::EditorContext::RestoreNodeState(Node* node) +{ + auto settings = m_Settings.FindNode(node->m_ID); + if (!settings) + return; + + // Load state from config (if possible) + if (!NodeSettings::Parse(m_Config.LoadNode(node->m_ID), *settings)) + return; + + node->m_Bounds.Min = settings->m_Location; + node->m_Bounds.Max = node->m_Bounds.Min + settings->m_Size; + node->m_Bounds.Min = ImFloor(node->m_Bounds.Min); node->m_Bounds.Max = ImFloor(node->m_Bounds.Max); // ImRect::Floor removed upstream in ImGui + node->m_GroupBounds.Min = settings->m_Location; + node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize; + node->m_GroupBounds.Min = ImFloor(node->m_GroupBounds.Min); node->m_GroupBounds.Max = ImFloor(node->m_GroupBounds.Max); // ImRect::Floor removed upstream in ImGui +} + +void ed::EditorContext::ClearSelection() +{ + m_SelectedObjects.clear(); +} + +void ed::EditorContext::SelectObject(Object* object) +{ + m_SelectedObjects.push_back(object); +} + +void ed::EditorContext::DeselectObject(Object* object) +{ + auto objectIt = std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object); + if (objectIt != m_SelectedObjects.end()) + m_SelectedObjects.erase(objectIt); +} + +void ed::EditorContext::SetSelectedObject(Object* object) +{ + ClearSelection(); + SelectObject(object); +} + +void ed::EditorContext::ToggleObjectSelection(Object* object) +{ + if (IsSelected(object)) + DeselectObject(object); + else + SelectObject(object); +} + +bool ed::EditorContext::IsSelected(Object* object) +{ + return std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object) != m_SelectedObjects.end(); +} + +const ed::vector& ed::EditorContext::GetSelectedObjects() +{ + return m_SelectedObjects; +} + +bool ed::EditorContext::IsAnyNodeSelected() +{ + for (auto object : m_SelectedObjects) + if (object->AsNode()) + return true; + + return false; +} + +bool ed::EditorContext::IsAnyLinkSelected() +{ + for (auto object : m_SelectedObjects) + if (object->AsLink()) + return true; + + return false; +} + +bool ed::EditorContext::HasSelectionChanged() +{ + return m_LastSelectedObjects != m_SelectedObjects; +} + +ed::Node* ed::EditorContext::FindNodeAt(const ImVec2& p) +{ + for (auto node : m_Nodes) + if (node->TestHit(p)) + return node; + + return nullptr; +} + +// UsdLayerManager local patch: see the declaration. Iterates live pins, +// returning the first whose bounds contain p. Pins don't overlap each other, +// so first-hit is unambiguous. +ed::Pin* ed::EditorContext::FindPinAt(const ImVec2& p) +{ + for (auto pin : m_Pins) + if (pin->m_IsLive && pin->TestHit(p)) + return pin; + + return nullptr; +} + +void ed::EditorContext::FindNodesInRect(const ImRect& r, vector& result, bool append, bool includeIntersecting) +{ + if (!append) + result.resize(0); + + if (ImRect_IsEmpty(r)) + return; + + for (auto node : m_Nodes) + if (node->TestHit(r, includeIntersecting)) + result.push_back(node); +} + +void ed::EditorContext::FindLinksInRect(const ImRect& r, vector& result, bool append) +{ + if (!append) + result.resize(0); + + if (ImRect_IsEmpty(r)) + return; + + for (auto link : m_Links) + if (link->TestHit(r)) + result.push_back(link); +} + +void ed::EditorContext::FindLinksForNode(NodeId nodeId, vector& result, bool add) +{ + if (!add) + result.clear(); + + for (auto link : m_Links) + { + if (!link->m_IsLive) + continue; + + if (link->m_StartPin->m_Node->m_ID == nodeId || link->m_EndPin->m_Node->m_ID == nodeId) + result.push_back(link); + } +} + +bool ed::EditorContext::PinHadAnyLinks(PinId pinId) +{ + auto pin = FindPin(pinId); + if (!pin || !pin->m_IsLive) + return false; + + return pin->m_HasConnection || pin->m_HadConnection; +} + +void ed::EditorContext::NotifyLinkDeleted(Link* link) +{ + if (m_LastActiveLink == link) + m_LastActiveLink = nullptr; +} + +void ed::EditorContext::Suspend() +{ + auto lastChannel = ImGui::GetWindowDrawList()->_Splitter._Current; + ImGui::GetWindowDrawList()->ChannelsSetCurrent(0); + m_Canvas.Suspend(); + ImGui::GetWindowDrawList()->ChannelsSetCurrent(lastChannel); +} + +void ed::EditorContext::Resume() +{ + auto lastChannel = ImGui::GetWindowDrawList()->_Splitter._Current; + ImGui::GetWindowDrawList()->ChannelsSetCurrent(0); + m_Canvas.Resume(); + ImGui::GetWindowDrawList()->ChannelsSetCurrent(lastChannel); +} + +bool ed::EditorContext::IsSuspended() +{ + return m_Canvas.IsSuspended(); +} + +bool ed::EditorContext::IsActive() +{ + return m_IsWindowActive; +} + +ed::Pin* ed::EditorContext::CreatePin(PinId id, PinKind kind) +{ + IM_ASSERT(nullptr == FindObject(id)); + auto pin = new Pin(this, id, kind); + m_Pins.push_back({id, pin}); + std::sort(m_Pins.begin(), m_Pins.end()); + return pin; +} + +ed::Node* ed::EditorContext::CreateNode(NodeId id) +{ + IM_ASSERT(nullptr == FindObject(id)); + auto node = new Node(this, id); + m_Nodes.push_back({id, node}); + //std::sort(Nodes.begin(), Nodes.end()); + + auto settings = m_Settings.FindNode(id); + if (!settings) + settings = m_Settings.AddNode(id); + + if (!settings->m_WasUsed) + { + settings->m_WasUsed = true; + RestoreNodeState(node); + } + + node->m_Bounds.Min = settings->m_Location; + node->m_Bounds.Max = node->m_Bounds.Min; + node->m_Bounds.Min = ImFloor(node->m_Bounds.Min); node->m_Bounds.Max = ImFloor(node->m_Bounds.Max); // ImRect::Floor removed upstream in ImGui + + if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0) + { + node->m_Type = NodeType::Group; + node->m_GroupBounds.Min = settings->m_Location; + node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize; + node->m_GroupBounds.Min = ImFloor(node->m_GroupBounds.Min); node->m_GroupBounds.Max = ImFloor(node->m_GroupBounds.Max); // ImRect::Floor removed upstream in ImGui + } + + node->m_IsLive = false; + + return node; +} + +ed::Link* ed::EditorContext::CreateLink(LinkId id) +{ + IM_ASSERT(nullptr == FindObject(id)); + auto link = new Link(this, id); + m_Links.push_back({id, link}); + std::sort(m_Links.begin(), m_Links.end()); + + return link; +} + +template +static inline auto FindItemInLinear(C& container, Id id) +{ +# if defined(_DEBUG) + auto start = container.data(); + auto end = container.data() + container.size(); + for (auto it = start; it < end; ++it) + if ((*it).m_ID == id) + return it->m_Object; +# else + for (auto item : container) + if (item.m_ID == id) + return item.m_Object; +# endif + + return static_cast(nullptr); +} + +template +static inline auto FindItemIn(C& container, Id id) +{ +//# if defined(_DEBUG) +// auto start = container.data(); +// auto end = container.data() + container.size(); +// for (auto it = start; it < end; ++it) +// if ((*it)->ID == id) +// return *it; +//# else +// for (auto item : container) +// if (item->ID == id) +// return item; +//# endif + auto key = typename C::value_type{ id, nullptr }; + auto first = container.cbegin(); + auto last = container.cend(); + auto it = std::lower_bound(first, last, key); + if (it != last && (key.m_ID == it->m_ID)) + return it->m_Object; + else + return static_castm_Object)>(nullptr); +} + +ed::Node* ed::EditorContext::FindNode(NodeId id) +{ + return FindItemInLinear(m_Nodes, id); +} + +ed::Pin* ed::EditorContext::FindPin(PinId id) +{ + return FindItemIn(m_Pins, id); +} + +ed::Link* ed::EditorContext::FindLink(LinkId id) +{ + return FindItemIn(m_Links, id); +} + +ed::Object* ed::EditorContext::FindObject(ObjectId id) +{ + if (id.IsNodeId()) + return FindNode(id.AsNodeId()); + else if (id.IsLinkId()) + return FindLink(id.AsLinkId()); + else if (id.IsPinId()) + return FindPin(id.AsPinId()); + else + return nullptr; +} + +ed::Node* ed::EditorContext::GetNode(NodeId id) +{ + auto node = FindNode(id); + if (!node) + node = CreateNode(id); + return node; +} + +ed::Pin* ed::EditorContext::GetPin(PinId id, PinKind kind) +{ + if (auto pin = FindPin(id)) + { + pin->m_Kind = kind; + return pin; + } + else + return CreatePin(id, kind); +} + +ed::Link* ed::EditorContext::GetLink(LinkId id) +{ + if (auto link = FindLink(id)) + return link; + else + return CreateLink(id); +} + +void ed::EditorContext::LoadSettings() +{ + ed::Settings::Parse(m_Config.Load(), m_Settings); + + m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll; + m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom; +} + +void ed::EditorContext::SaveSettings() +{ + m_Config.BeginSave(); + + for (auto& node : m_Nodes) + { + auto settings = m_Settings.FindNode(node->m_ID); + settings->m_Location = node->m_Bounds.Min; + settings->m_Size = node->m_Bounds.GetSize(); + if (IsGroup(node)) + settings->m_GroupSize = node->m_GroupBounds.GetSize(); + + if (!node->m_RestoreState && settings->m_IsDirty && m_Config.SaveNodeSettings) + { + if (m_Config.SaveNode(node->m_ID, settings->Serialize().dump(), settings->m_DirtyReason)) + settings->ClearDirty(); + } + } + + m_Settings.m_Selection.resize(0); + for (auto& object : m_SelectedObjects) + m_Settings.m_Selection.push_back(object->ID()); + + m_Settings.m_ViewScroll = m_NavigateAction.m_Scroll; + m_Settings.m_ViewZoom = m_NavigateAction.m_Zoom; + + if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason)) + m_Settings.ClearDirty(); + + m_Config.EndSave(); +} + +void ed::EditorContext::MakeDirty(SaveReasonFlags reason) +{ + m_Settings.MakeDirty(reason); +} + +void ed::EditorContext::MakeDirty(SaveReasonFlags reason, Node* node) +{ + m_Settings.MakeDirty(reason, node); +} + +ed::Link* ed::EditorContext::FindLinkAt(const ImVec2& p) +{ + for (auto& link : m_Links) + if (link->TestHit(p, c_LinkSelectThickness)) + return link; + + return nullptr; +} + +ImU32 ed::EditorContext::GetColor(StyleColor colorIndex) const +{ + return ImColor(m_Style.Colors[colorIndex]); +} + +ImU32 ed::EditorContext::GetColor(StyleColor colorIndex, float alpha) const +{ + auto color = m_Style.Colors[colorIndex]; + return ImColor(color.x, color.y, color.z, color.w * alpha); +} + +void ed::EditorContext::RegisterAnimation(Animation* animation) +{ + m_LiveAnimations.push_back(animation); +} + +void ed::EditorContext::UnregisterAnimation(Animation* animation) +{ + auto it = std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation); + if (it != m_LiveAnimations.end()) + m_LiveAnimations.erase(it); +} + +void ed::EditorContext::UpdateAnimations() +{ + m_LastLiveAnimations = m_LiveAnimations; + + for (auto animation : m_LastLiveAnimations) + { + const bool isLive = (std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation) != m_LiveAnimations.end()); + + if (isLive) + animation->Update(); + } +} + +void ed::EditorContext::Flow(Link* link) +{ + m_FlowAnimationController.Flow(link); +} + +void ed::EditorContext::SetUserContext(bool globalSpace) +{ + const auto mousePos = ImGui::GetMousePos(); + + // Move drawing cursor to mouse location and prepare layer for + // content added by user. + if (globalSpace) + ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); + else + ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); + //ImGui::SetCursorScreenPos(ImFloor(mousePos)); + //ImGui::SetCursorScreenPos(ImVec2(floorf(mousePos.x), floorf(mousePos.y))); + + auto drawList = ImGui::GetWindowDrawList(); + drawList->ChannelsSetCurrent(c_UserChannel_Content); + + // #debug + // drawList->AddCircleFilled(ImGui::GetMousePos(), 4, IM_COL32(0, 255, 0, 255)); +} + +void ed::EditorContext::EnableShortcuts(bool enable) +{ + m_ShortcutsEnabled = enable; +} + +bool ed::EditorContext::AreShortcutsEnabled() +{ + return m_ShortcutsEnabled; +} + +ed::Control ed::EditorContext::BuildControl(bool allowOffscreen) +{ + if (!allowOffscreen && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + return Control(nullptr, nullptr, nullptr, nullptr, false, false, false, false); + + const auto mousePos = ImGui::GetMousePos(); + + // UsdLayerManager local patch: see m_PressedNode's declaration for why. + // Computed once on the press frame (not every frame) — cheap, and must be + // the raw geometric bounds test rather than anything ImGui-hover-derived, + // since the whole point is to be robust to a hover/active mis-attribution + // that can persist for an entire gesture, not just one frame. A Group's + // hollow interior deliberately isn't captured here: FindNodeAt() finds a + // Group by its full outer bounds, but this codebase's only other + // FindNodeAt() caller already special-cases Group nodes the same way, + // since Group interiors must stay marquee-selectable. + if (ImGui::IsMouseClicked(0)) + { + // Was this press inside the visible canvas at all? A press on the host + // window's title bar (window move) transforms to a canvas-local point + // outside the view rect — and, since that transform is unclamped, can + // even land inside a node that's scrolled partly off-screen, so this + // gate must come first: FindNodeAt() is only meaningful for an + // in-canvas press. + m_PressedInCanvas = m_Canvas.ViewRect().Contains(mousePos); + // A pin sits on top of its node body; pressing a pin must start a + // connection (CreateItemAction), not move the node (DragAction), so + // resolve the pin first and, when found, suppress the node press. + m_PressedPin = m_PressedInCanvas ? FindPinAt(mousePos) : nullptr; + auto pressedNode = (m_PressedInCanvas && !m_PressedPin) ? FindNodeAt(mousePos) : nullptr; + m_PressedNode = (pressedNode != nullptr && !IsGroup(pressedNode)) ? pressedNode : nullptr; + } + else if (!ImGui::IsMouseDown(0) && !ImGui::IsMouseReleased(0)) + { + // Mouse has been up for at least a full frame — drop stale press + // state. This must NOT clear on the release frame itself + // (IsMouseReleased): BuildControl() runs before that frame's + // Accept()/Process() calls in End(), and SelectAction::Accept()'s + // click-select fallback still needs m_PressedNode on release. But it + // MUST clear by the next idle frame, otherwise a subsequent title-bar + // drag — whose press IsMouseClicked(0) may not be observed from inside + // the canvas child window — would inherit the previous in-canvas node + // press and let DragAction's fallback start a phantom drag (which in + // turn re-enables MoveOverEdge and pans the graph during the window + // move). + m_PressedNode = nullptr; + m_PressedPin = nullptr; + m_PressedInCanvas = false; + } + + // Expand clip rectangle to always contain cursor + auto editorRect = m_Canvas.ViewRect(); + auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos); + if (isMouseOffscreen) + { + // Extend clip rect to capture off-screen mouse cursor + editorRect.Add(ImFloor(mousePos)); + editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y))); + + ImGui::PushClipRect(editorRect.Min, editorRect.Max, false); + } + + Object* hotObject = nullptr; + Object* activeObject = nullptr; + Object* clickedObject = nullptr; + Object* doubleClickedObject = nullptr; + + // Emits invisible button and returns true if it is clicked. + auto emitInteractiveArea = [](ObjectId id, const ImRect& rect) + { + char idString[33] = { 0 }; // itoa can output 33 bytes maximum + snprintf(idString, 32, "%p", id.AsPointer()); + ImGui::SetCursorScreenPos(rect.Min); + + // debug + //if (id < 0) return ImGui::Button(idString, to_imvec(rect.size)); + + auto result = ImGui::InvisibleButton(idString, rect.GetSize()); + + // #debug + //ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64)); + + return result; + }; + + // Check input interactions over area. + auto checkInteractionsInArea = [&emitInteractiveArea, &hotObject, &activeObject, &clickedObject, &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object) + { + if (emitInteractiveArea(id, rect)) + clickedObject = object; + if (!doubleClickedObject && ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) + doubleClickedObject = object; + + if (!hotObject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + hotObject = object; + + if (ImGui::IsItemActive()) + activeObject = object; + }; + + // Process live nodes and pins. + for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); nodeIt != nodeItEnd; ++nodeIt) + { + auto node = *nodeIt; + + if (!node->m_IsLive) continue; + + // Check for interactions with live pins in node before + // processing node itself. Pins does not overlap each other + // and all are within node bounds. + for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) + { + if (!pin->m_IsLive) continue; + + checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin); + } + + // Check for interactions with node. + if (node->m_Type == NodeType::Group) + { + // Node with a hole + ImGui::PushID(node->m_ID.AsPointer()); + + static const NodeRegion c_Regions[] = + { + NodeRegion::TopLeft, + NodeRegion::TopRight, + NodeRegion::BottomLeft, + NodeRegion::BottomRight, + NodeRegion::Top, + NodeRegion::Bottom, + NodeRegion::Left, + NodeRegion::Right, + NodeRegion::Header, + }; + + for (auto region : c_Regions) + { + auto bounds = node->GetRegionBounds(region); + if (ImRect_IsEmpty(bounds)) + continue; + checkInteractionsInArea(NodeId(static_cast(region)), bounds, node); + } + + ImGui::PopID(); + } + else + checkInteractionsInArea(node->m_ID, node->m_Bounds, node); + } + + // UsdLayerManager local patch: geometric pin fallbacks. ImGui's per-item + // hover/active attribution fails for pins in this embedding (edge clipping + // or losing the hover race to the node body / background), so a pin the + // cursor is over may not have set hotObject (breaking hover-highlight and + // the drop target of a new link), and a pin the press landed on may not + // have set activeObject (so a drag can't start a connection). Resolve both + // by geometry, giving a pin precedence over the node body it sits on + // (matching the normal pin-before-node interaction order). Confined to + // in-canvas positions since the canvas->local transform is unclamped. + if (editorRect.Contains(mousePos) && (!hotObject || hotObject->AsNode())) + { + if (Pin* p = FindPinAt(mousePos)) + hotObject = p; + } + if (!activeObject && m_PressedPin && ImGui::IsMouseDragging(0)) + activeObject = m_PressedPin; + + // Links are not regular widgets and must be done manually since + // ImGui does not support interactive elements with custom hit maps. + // + // Links can steal input from background. + + // Links are just over background. So if anything else + // is hovered we can skip them. + if (nullptr == hotObject) + hotObject = FindLinkAt(mousePos); + + // Check for interaction with background. + auto backgroundClicked = emitInteractiveArea(NodeId(0), editorRect); + auto backgroundDoubleClicked = !doubleClickedObject && ImGui::IsItemHovered() ? ImGui::IsMouseDoubleClicked(0) : false; + auto isBackgroundActive = ImGui::IsItemActive(); + auto isBackgroundHot = !hotObject; + auto isDragging = ImGui::IsMouseDragging(0, 1) || ImGui::IsMouseDragging(1, 1) || ImGui::IsMouseDragging(2, 1); + + if (backgroundDoubleClicked) + backgroundClicked = false; + + if (isMouseOffscreen) + ImGui::PopClipRect(); + + // Process link input using background interactions. + auto hotLink = hotObject ? hotObject->AsLink() : nullptr; + + // ImGui take care of tracking active items. With link + // we must do this ourself. + if (!isDragging && isBackgroundActive && hotLink && !m_LastActiveLink) + m_LastActiveLink = hotLink; + if (isBackgroundActive && m_LastActiveLink) + { + activeObject = m_LastActiveLink; + isBackgroundActive = false; + } + else if (!isBackgroundActive && m_LastActiveLink) + m_LastActiveLink = nullptr; + + // Steal click from backgrounds if link is hovered. + if (!isDragging && backgroundClicked && hotLink) + { + clickedObject = hotLink; + backgroundClicked = false; + } + + // Steal double-click from backgrounds if link is hovered. + if (!isDragging && backgroundDoubleClicked && hotLink) + { + doubleClickedObject = hotLink; + backgroundDoubleClicked = false; + } + + // UsdLayerManager local patch: dump the editor's own view of a click (and + // the frames right after, while LMB is held) so the application log shows + // exactly why a press hit (or missed) a node — including the m_CurrentAction + // decision, which BuildControl's caller (End()) makes right after this + // returns and this dump can't see directly, so log control.* here and + // correlate with the [ed] action: line End() prints separately. + if (g_AxNodeEditorDebugLog && (ImGui::IsMouseClicked(0) || ImGui::IsMouseDown(0))) + { + auto& g = *ImGui::GetCurrentContext(); + auto& io = ImGui::GetIO(); + auto* wnd = ImGui::GetCurrentWindow(); + char buf[512]; + + snprintf(buf, sizeof(buf), + "[ed] %s: io.MousePos local (%.1f, %.1f), hoveredWindow==canvas %d, " + "HoveredId 0x%08X, ActiveId 0x%08X, clip (%.1f, %.1f)-(%.1f, %.1f)", + ImGui::IsMouseClicked(0) ? "click" : "held ", + io.MousePos.x, io.MousePos.y, + g.HoveredWindow == wnd ? 1 : 0, + g.HoveredId, g.ActiveId, + wnd->ClipRect.Min.x, wnd->ClipRect.Min.y, + wnd->ClipRect.Max.x, wnd->ClipRect.Max.y); + g_AxNodeEditorDebugLog(buf); + + for (auto node : m_Nodes) + { + snprintf(buf, sizeof(buf), + "[ed] node %p live=%d bounds (%.1f, %.1f)-(%.1f, %.1f) contains=%d", + node->m_ID.AsPointer(), node->m_IsLive ? 1 : 0, + node->m_Bounds.Min.x, node->m_Bounds.Min.y, + node->m_Bounds.Max.x, node->m_Bounds.Max.y, + node->m_Bounds.Contains(io.MousePos) ? 1 : 0); + g_AxNodeEditorDebugLog(buf); + } + + auto objDesc = [](Object* object) -> const char* + { + if (!object) return "none"; + else if (object->AsNode()) return "node"; + else if (object->AsPin()) return "pin"; + else if (object->AsLink()) return "link"; + else return "other"; + }; + snprintf(buf, sizeof(buf), + "[ed] result: hot=%s %p active=%s %p clicked=%s %p bgHot=%d bgActive=%d bgClicked=%d", + objDesc(hotObject), hotObject ? hotObject->ID().AsPointer() : nullptr, + objDesc(activeObject), activeObject ? activeObject->ID().AsPointer() : nullptr, + objDesc(clickedObject), clickedObject ? clickedObject->ID().AsPointer() : nullptr, + isBackgroundHot ? 1 : 0, isBackgroundActive ? 1 : 0, backgroundClicked ? 1 : 0); + g_AxNodeEditorDebugLog(buf); + } + + return Control(hotObject, activeObject, clickedObject, doubleClickedObject, + isBackgroundHot, isBackgroundActive, backgroundClicked, backgroundDoubleClicked); +} + +void ed::EditorContext::ShowMetrics(const Control& control) +{ + auto& io = ImGui::GetIO(); + + auto getObjectName = [](Object* object) + { + if (!object) return ""; + else if (object->AsNode()) return "Node"; + else if (object->AsPin()) return "Pin"; + else if (object->AsLink()) return "Link"; + else return ""; + }; + + auto getHotObjectName = [&control, &getObjectName]() + { + if (control.HotObject) + return getObjectName(control.HotObject); + else if (control.BackgroundHot) + return "Background"; + else + return ""; + }; + + auto getActiveObjectName = [&control, &getObjectName]() + { + if (control.ActiveObject) + return getObjectName(control.ActiveObject); + else if (control.BackgroundActive) + return "Background"; + else + return ""; + }; + + auto liveNodeCount = (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; }); + auto livePinCount = (int)std::count_if(m_Pins.begin(), m_Pins.end(), [](Pin* pin) { return pin->m_IsLive; }); + auto liveLinkCount = (int)std::count_if(m_Links.begin(), m_Links.end(), [](Link* link) { return link->m_IsLive; }); + + auto canvasRect = m_Canvas.Rect(); + auto viewRect = m_Canvas.ViewRect(); + auto localMousePos = m_Canvas.ToLocal(io.MousePos); + auto globalMousePos = io.MousePos; + + ImGui::SetCursorScreenPos(canvasRect.Min + ImVec2(5, 5)); + ImGui::BeginGroup(); + ImGui::Text("Is Editor Active: %s", ImGui::IsWindowHovered() ? "true" : "false"); + ImGui::Text("View Position: { x=%g y=%g }", viewRect.Min.x, viewRect.Min.y); + ImGui::Text("View Size: { w=%g h=%g }", viewRect.GetWidth(), viewRect.GetHeight()); + ImGui::Text("Canvas Size: { w=%g h=%g }", canvasRect.GetWidth(), canvasRect.GetHeight()); + ImGui::Text("Mouse: { x=%.0f y=%.0f } global: { x=%g y=%g }", localMousePos.x, localMousePos.y, globalMousePos.x, globalMousePos.y); + ImGui::Text("Live Nodes: %d", liveNodeCount); + ImGui::Text("Live Pins: %d", livePinCount); + ImGui::Text("Live Links: %d", liveLinkCount); + ImGui::Text("Hot Object: %s (%p)", getHotObjectName(), control.HotObject ? control.HotObject->ID().AsPointer() : nullptr); + if (auto node = control.HotObject ? control.HotObject->AsNode() : nullptr) + { + ImGui::SameLine(); + ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight()); + } + ImGui::Text("Active Object: %s (%p)", getActiveObjectName(), control.ActiveObject ? control.ActiveObject->ID().AsPointer() : nullptr); + if (auto node = control.ActiveObject ? control.ActiveObject->AsNode() : nullptr) + { + ImGui::SameLine(); + ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight()); + } + ImGui::Text("Action: %s", m_CurrentAction ? m_CurrentAction->GetName() : ""); + ImGui::Text("Action Is Dragging: %s", m_CurrentAction && m_CurrentAction->IsDragging() ? "Yes" : "No"); + m_NavigateAction.ShowMetrics(); + m_SizeAction.ShowMetrics(); + m_DragAction.ShowMetrics(); + m_SelectAction.ShowMetrics(); + m_ContextMenuAction.ShowMetrics(); + m_CreateItemAction.ShowMetrics(); + m_DeleteItemsAction.ShowMetrics(); + ImGui::EndGroup(); +} + + + + +//------------------------------------------------------------------------------ +// +// Node Settings +// +//------------------------------------------------------------------------------ +void ed::NodeSettings::ClearDirty() +{ + m_IsDirty = false; + m_DirtyReason = SaveReasonFlags::None; +} + +void ed::NodeSettings::MakeDirty(SaveReasonFlags reason) +{ + m_IsDirty = true; + m_DirtyReason = m_DirtyReason | reason; +} + +ed::json::value ed::NodeSettings::Serialize() +{ + json::value result; + result["location"]["x"] = m_Location.x; + result["location"]["y"] = m_Location.y; + + if (m_GroupSize.x > 0 || m_GroupSize.y > 0) + { + result["group_size"]["x"] = m_GroupSize.x; + result["group_size"]["y"] = m_GroupSize.y; + } + + return result; +} + +bool ed::NodeSettings::Parse(const std::string& string, NodeSettings& settings) +{ + auto settingsValue = json::value::parse(string); + if (settingsValue.is_discarded()) + return false; + + return Parse(settingsValue, settings); +} + +bool ed::NodeSettings::Parse(const json::value& data, NodeSettings& result) +{ + if (!data.is_object()) + return false; + + auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool + { + if (v.is_object()) + { + auto xValue = v["x"]; + auto yValue = v["y"]; + + if (xValue.is_number() && yValue.is_number()) + { + result.x = static_cast(xValue.get()); + result.y = static_cast(yValue.get()); + + return true; + } + } + + return false; + }; + + if (!tryParseVector(data["location"], result.m_Location)) + return false; + + if (data.contains("group_size") && !tryParseVector(data["group_size"], result.m_GroupSize)) + return false; + + return true; +} + + + + +//------------------------------------------------------------------------------ +// +// Settings +// +//------------------------------------------------------------------------------ +ed::NodeSettings* ed::Settings::AddNode(NodeId id) +{ + m_Nodes.push_back(NodeSettings(id)); + return &m_Nodes.back(); +} + +ed::NodeSettings* ed::Settings::FindNode(NodeId id) +{ + for (auto& settings : m_Nodes) + if (settings.m_ID == id) + return &settings; + + return nullptr; +} + +void ed::Settings::ClearDirty(Node* node) +{ + if (node) + { + auto settings = FindNode(node->m_ID); + IM_ASSERT(settings); + settings->ClearDirty(); + } + else + { + m_IsDirty = false; + m_DirtyReason = SaveReasonFlags::None; + + for (auto& node : m_Nodes) + node.ClearDirty(); + } +} + +void ed::Settings::MakeDirty(SaveReasonFlags reason, Node* node) +{ + m_IsDirty = true; + m_DirtyReason = m_DirtyReason | reason; + + if (node) + { + auto settings = FindNode(node->m_ID); + IM_ASSERT(settings); + + settings->MakeDirty(reason); + } +} + +std::string ed::Settings::Serialize() +{ + json::value result; + + auto serializeObjectId = [](ObjectId id) + { + auto value = std::to_string(reinterpret_cast(id.AsPointer())); + switch (id.Type()) + { + default: + case NodeEditor::Detail::ObjectType::None: return value; + case NodeEditor::Detail::ObjectType::Node: return "node:" + value; + case NodeEditor::Detail::ObjectType::Link: return "link:" + value; + case NodeEditor::Detail::ObjectType::Pin: return "pin:" + value; + } + }; + + auto& nodes = result["nodes"]; + for (auto& node : m_Nodes) + { + if (node.m_WasUsed) + nodes[serializeObjectId(node.m_ID)] = node.Serialize(); + } + + auto& selection = result["selection"]; + for (auto& id : m_Selection) + selection.push_back(serializeObjectId(id)); + + auto& view = result["view"]; + view["scroll"]["x"] = m_ViewScroll.x; + view["scroll"]["y"] = m_ViewScroll.y; + view["zoom"] = m_ViewZoom; + + return result.dump(); +} + +bool ed::Settings::Parse(const std::string& string, Settings& settings) +{ + Settings result = settings; + + auto settingsValue = json::value::parse(string); + if (settingsValue.is_discarded()) + return false; + + if (!settingsValue.is_object()) + return false; + + auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool + { + if (v.is_object() && v.contains("x") && v.contains("y")) + { + auto xValue = v["x"]; + auto yValue = v["y"]; + + if (xValue.is_number() && yValue.is_number()) + { + result.x = static_cast(xValue.get()); + result.y = static_cast(yValue.get()); + + return true; + } + } + + return false; + }; + + auto deserializeObjectId = [](const std::string& str) + { + auto separator = str.find_first_of(':'); + auto idStart = str.c_str() + ((separator != std::string::npos) ? separator + 1 : 0); + auto id = reinterpret_cast(strtoull(idStart, nullptr, 16)); + if (str.compare(0, separator, "node") == 0) + return ObjectId(NodeId(id)); + else if (str.compare(0, separator, "link") == 0) + return ObjectId(LinkId(id)); + else if (str.compare(0, separator, "pin") == 0) + return ObjectId(PinId(id)); + else + // fallback to old format + return ObjectId(NodeId(id)); //return ObjectId(); + }; + + //auto& settingsObject = settingsValue.get(); + + auto& nodesValue = settingsValue["nodes"]; + if (nodesValue.is_object()) + { + for (auto& node : nodesValue.get()) + { + auto id = deserializeObjectId(node.first.c_str()).AsNodeId(); + + auto settings = result.FindNode(id); + if (!settings) + settings = result.AddNode(id); + + NodeSettings::Parse(node.second, *settings); + } + } + + auto& selectionValue = settingsValue["selection"]; + if (selectionValue.is_array()) + { + const auto selectionArray = selectionValue.get(); + + result.m_Selection.reserve(selectionArray.size()); + result.m_Selection.resize(0); + for (auto& selection : selectionArray) + { + if (selection.is_string()) + result.m_Selection.push_back(deserializeObjectId(selection.get())); + } + } + + auto& viewValue = settingsValue["view"]; + if (viewValue.is_object()) + { + auto& viewScrollValue = viewValue["scroll"]; + auto& viewZoomValue = viewValue["zoom"]; + + if (!tryParseVector(viewScrollValue, result.m_ViewScroll)) + result.m_ViewScroll = ImVec2(0, 0); + + result.m_ViewZoom = viewZoomValue.is_number() ? static_cast(viewZoomValue.get()) : 1.0f; + } + + settings = std::move(result); + + return true; +} + + + +//------------------------------------------------------------------------------ +// +// Animation +// +//------------------------------------------------------------------------------ +ed::Animation::Animation(EditorContext* editor): + Editor(editor), + m_State(Stopped), + m_Time(0.0f), + m_Duration(0.0f) +{ +} + +ed::Animation::~Animation() +{ + Stop(); +} + +void ed::Animation::Play(float duration) +{ + if (IsPlaying()) + Stop(); + + m_State = Playing; + if (duration < 0) + duration = 0.0f; + + m_Time = 0.0f; + m_Duration = duration; + + OnPlay(); + + Editor->RegisterAnimation(this); + + if (duration == 0.0f) + Stop(); +} + +void ed::Animation::Stop() +{ + if (!IsPlaying()) + return; + + m_State = Stopped; + + Editor->UnregisterAnimation(this); + + OnStop(); +} + +void ed::Animation::Finish() +{ + if (!IsPlaying()) + return; + + OnFinish(); + + Stop(); +} + +void ed::Animation::Update() +{ + if (!IsPlaying()) + return; + + m_Time += ImMax(0.0f, ImGui::GetIO().DeltaTime); + if (m_Time < m_Duration) + { + const float progress = GetProgress(); + OnUpdate(progress); + } + else + { + OnFinish(); + Stop(); + } +} + + + + +//------------------------------------------------------------------------------ +// +// Navigate Animation +// +//------------------------------------------------------------------------------ +ed::NavigateAnimation::NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction): + Animation(editor), + Action(scrollAction) +{ +} + +void ed::NavigateAnimation::NavigateTo(const ImRect& target, float duration) +{ + Stop(); + + m_Start = Action.GetViewRect(); + m_Target = target; + + // Skip tiny animations + auto minoffset = m_Target.Min - m_Start.Min; + auto maxOffset = m_Target.Max - m_Start.Max; + auto epsilon = 1e-4f; + if (ImFabs(minoffset.x) < epsilon && ImFabs(minoffset.y) < epsilon && + ImFabs(maxOffset.x) < epsilon && ImFabs(maxOffset.y) < epsilon) + { + duration = 0; + } + + Play(duration); +} + +void ed::NavigateAnimation::OnUpdate(float progress) +{ + ImRect current; + current.Min = ImEasing::EaseOutQuad(m_Start.Min, m_Target.Min - m_Start.Min, progress); + current.Max = ImEasing::EaseOutQuad(m_Start.Max, m_Target.Max - m_Start.Max, progress); + Action.SetViewRect(current); +} + +void ed::NavigateAnimation::OnStop() +{ + Editor->MakeDirty(SaveReasonFlags::Navigation); +} + +void ed::NavigateAnimation::OnFinish() +{ + Action.SetViewRect(m_Target); + + Editor->MakeDirty(SaveReasonFlags::Navigation); +} + + + + +//------------------------------------------------------------------------------ +// +// Flow Animation +// +//------------------------------------------------------------------------------ +ed::FlowAnimation::FlowAnimation(FlowAnimationController* controller): + Animation(controller->Editor), + Controller(controller), + m_Link(nullptr), + m_Offset(0.0f), + m_PathLength(0.0f) +{ +} + +void ed::FlowAnimation::Flow(ed::Link* link, float markerDistance, float speed, float duration) +{ + Stop(); + + if (m_Link != link) + { + m_Offset = 0.0f; + ClearPath(); + } + + if (m_MarkerDistance != markerDistance) + ClearPath(); + + m_MarkerDistance = markerDistance; + m_Speed = speed; + m_Link = link; + + Play(duration); +} + +void ed::FlowAnimation::Draw(ImDrawList* drawList) +{ + if (!IsPlaying() || !IsLinkValid() || !m_Link->IsVisible()) + return; + + if (!IsPathValid()) + UpdatePath(); + + m_Offset = fmodf(m_Offset, m_MarkerDistance); + + const auto progress = GetProgress(); + + const auto flowAlpha = 1.0f - progress * progress; + const auto flowColor = Editor->GetColor(StyleColor_Flow, flowAlpha); + //const auto flowPath = Link->GetCurve(); + + m_Link->Draw(drawList, flowColor, 2.0f); + + if (IsPathValid()) + { + //Offset = 0; + + const auto markerAlpha = powf(1.0f - progress, 0.35f); + const auto markerRadius = 4.0f * (1.0f - progress) + 2.0f; + const auto markerColor = Editor->GetColor(StyleColor_FlowMarker, markerAlpha); + + for (float d = m_Offset; d < m_PathLength; d += m_MarkerDistance) + drawList->AddCircleFilled(SamplePath(d), markerRadius, markerColor); + } +} + +bool ed::FlowAnimation::IsLinkValid() const +{ + return m_Link && m_Link->m_IsLive; +} + +bool ed::FlowAnimation::IsPathValid() const +{ + return m_Path.size() > 1 && m_PathLength > 0.0f && m_Link->m_Start == m_LastStart && m_Link->m_End == m_LastEnd; +} + +void ed::FlowAnimation::UpdatePath() +{ + if (!IsLinkValid()) + { + ClearPath(); + return; + } + + const auto curve = m_Link->GetCurve(); + + m_LastStart = m_Link->m_Start; + m_LastEnd = m_Link->m_End; + m_PathLength = ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); + + auto collectPointsCallback = [this](ImCubicBezierFixedStepSample& result) + { + m_Path.push_back(CurvePoint{ result.Length, result.Point }); + }; + + const auto step = ImMax(m_MarkerDistance * 0.5f, 15.0f); + + m_Path.resize(0); + ImCubicBezierFixedStep(collectPointsCallback, curve, step, false, 0.5f, 0.001f); +} + +void ed::FlowAnimation::ClearPath() +{ + vector().swap(m_Path); + m_PathLength = 0.0f; +} + +ImVec2 ed::FlowAnimation::SamplePath(float distance) +{ + //distance = ImMax(0.0f, std::min(distance, PathLength)); + + auto endPointIt = std::find_if(m_Path.begin(), m_Path.end(), [distance](const CurvePoint& p) { return distance < p.Distance; }); + if (endPointIt == m_Path.end()) + endPointIt = m_Path.end() - 1; + else if (endPointIt == m_Path.begin()) + endPointIt = m_Path.begin() + 1; + + const auto& start = endPointIt[-1]; + const auto& end = *endPointIt; + const auto t = (distance - start.Distance) / (end.Distance - start.Distance); + + return start.Point + (end.Point - start.Point) * t; +} + +void ed::FlowAnimation::OnUpdate(float progress) +{ + m_Offset += m_Speed * ImGui::GetIO().DeltaTime; +} + +void ed::FlowAnimation::OnStop() +{ + Controller->Release(this); +} + + + + +//------------------------------------------------------------------------------ +// +// Flow Animation Controller +// +//------------------------------------------------------------------------------ +ed::FlowAnimationController::FlowAnimationController(EditorContext* editor): + AnimationController(editor) +{ +} + +ed::FlowAnimationController::~FlowAnimationController() +{ + for (auto animation : m_Animations) + delete animation; +} + +void ed::FlowAnimationController::Flow(Link* link) +{ + if (!link || !link->m_IsLive) + return; + + auto& editorStyle = GetStyle(); + + auto animation = GetOrCreate(link); + + animation->Flow(link, editorStyle.FlowMarkerDistance, editorStyle.FlowSpeed, editorStyle.FlowDuration); +} + +void ed::FlowAnimationController::Draw(ImDrawList* drawList) +{ + if (m_Animations.empty()) + return; + + drawList->ChannelsSetCurrent(c_LinkChannel_Flow); + + for (auto animation : m_Animations) + animation->Draw(drawList); +} + +ed::FlowAnimation* ed::FlowAnimationController::GetOrCreate(Link* link) +{ + // Return live animation which match target link + { + auto animationIt = std::find_if(m_Animations.begin(), m_Animations.end(), [link](FlowAnimation* animation) { return animation->m_Link == link; }); + if (animationIt != m_Animations.end()) + return *animationIt; + } + + // There are no live animations for target link, try to reuse inactive old one + if (!m_FreePool.empty()) + { + auto animation = m_FreePool.back(); + m_FreePool.pop_back(); + return animation; + } + + // Cache miss, allocate new one + auto animation = new FlowAnimation(this); + m_Animations.push_back(animation); + + return animation; +} + +void ed::FlowAnimationController::Release(FlowAnimation* animation) +{ +} + + + +//------------------------------------------------------------------------------ +// +// Navigate Action +// +//------------------------------------------------------------------------------ +const float ed::NavigateAction::s_ZoomLevels[] = +{ + 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f, 1.25f, 1.50f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f +}; + +const int ed::NavigateAction::s_ZoomLevelCount = sizeof(s_ZoomLevels) / sizeof(*s_ZoomLevels); + +ed::NavigateAction::NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas): + EditorAction(editor), + m_IsActive(false), + m_Zoom(1), + m_Scroll(0, 0), + m_ScrollStart(0, 0), + m_ScrollDelta(0, 0), + m_Canvas(canvas), + m_WindowScreenPos(0, 0), + m_WindowScreenSize(0, 0), + m_Animation(editor, *this), + m_Reason(NavigationReason::Unknown), + m_LastSelectionId(0), + m_LastObject(nullptr), + m_MovingOverEdge(false), + m_MoveOffset(0, 0) +{ +} + +ed::EditorAction::AcceptResult ed::NavigateAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return False; + + if (ImGui::IsWindowHovered() /*&& !ImGui::IsAnyItemActive()*/ && ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f)) + { + m_IsActive = true; + m_ScrollStart = m_Scroll; + m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex); + m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; + } + + auto& io = ImGui::GetIO(); + + if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(GetKeyIndexForF()) && Editor->AreShortcutsEnabled()) + { + const auto allowZoomIn = io.KeyShift; + + auto findHotObjectToZoom = [this, &control, &io]() -> Object* + { + if (control.HotObject) + { + if (auto pin = control.HotObject->AsPin()) + return pin->m_Node; + else + return control.HotObject; + } + else if (control.BackgroundHot) + { + auto node = Editor->FindNodeAt(io.MousePos); + if (IsGroup(node)) + return node; + } + + return nullptr; + }; + + bool navigateToContent = false; + if (!Editor->GetSelectedObjects().empty()) + { + if (m_Reason != NavigationReason::Selection || m_LastSelectionId != Editor->GetSelectionId() || allowZoomIn) + { + m_LastSelectionId = Editor->GetSelectionId(); + NavigateTo(Editor->GetSelectionBounds(), allowZoomIn, -1.0f, NavigationReason::Selection); + } + else + navigateToContent = true; + } + else if(auto hotObject = findHotObjectToZoom()) + { + if (m_Reason != NavigationReason::Object || m_LastObject != hotObject || allowZoomIn) + { + m_LastObject = hotObject; + auto bounds = hotObject->GetBounds(); + NavigateTo(bounds, allowZoomIn, -1.0f, NavigationReason::Object); + } + else + navigateToContent = true; + } + else + navigateToContent = true; + + if (navigateToContent) + NavigateTo(Editor->GetContentBounds(), true, -1.0f, NavigationReason::Content); + } + + // // #debug + // if (auto drawList = ImGui::GetWindowDrawList()) + // drawList->AddCircleFilled(io.MousePos, 4.0f, IM_COL32(255, 0, 255, 255)); + + if (HandleZoom(control)) + return True; + + return m_IsActive ? True : False; +} + +bool ed::NavigateAction::Process(const Control& control) +{ + if (!m_IsActive) + return false; + + if (ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f)) + { + m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex); + m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; + +// if (IsActive && Animation.IsPlaying()) +// Animation.Target = Animation.Target - ScrollDelta * Animation.TargetZoom; + } + else + { + if (m_Scroll != m_ScrollStart) + Editor->MakeDirty(SaveReasonFlags::Navigation); + + m_IsActive = false; + } + + // #TODO: Handle zoom while scrolling + // HandleZoom(control); + + return m_IsActive; +} + +bool ed::NavigateAction::HandleZoom(const Control& control) +{ + const auto currentAction = Editor->GetCurrentAction(); + const auto allowOffscreen = currentAction && currentAction->IsDragging(); + + auto& io = ImGui::GetIO(); + + if (!io.MouseWheel || (!allowOffscreen && !ImGui::IsWindowHovered()))// && !ImGui::IsAnyItemActive()) + return false; + + auto savedScroll = m_Scroll; + auto savedZoom = m_Zoom; + + m_Animation.Finish(); + + auto mousePos = io.MousePos; + auto steps = (int)io.MouseWheel; + auto newZoom = MatchZoom(steps, s_ZoomLevels[steps < 0 ? 0 : s_ZoomLevelCount - 1]); + + auto oldView = GetView(); + m_Zoom = newZoom; + auto newView = GetView(); + + auto screenPos = m_Canvas.FromLocal(mousePos, oldView); + auto canvasPos = m_Canvas.ToLocal(screenPos, newView); + + auto offset = (canvasPos - mousePos) * m_Zoom; + auto targetScroll = m_Scroll - offset; + + if (m_Scroll != savedScroll || m_Zoom != savedZoom) + { + m_Scroll = savedScroll; + m_Zoom = savedZoom; + + Editor->MakeDirty(SaveReasonFlags::Navigation); + } + + auto targetRect = m_Canvas.CalcViewRect(ImGuiEx::CanvasView(-targetScroll, newZoom)); + + NavigateTo(targetRect, c_MouseZoomDuration, NavigationReason::MouseZoom); + + return true; +} + +void ed::NavigateAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Scroll: { x=%g y=%g }", m_Scroll.x, m_Scroll.y); + ImGui::Text(" Zoom: %g", m_Zoom); +} + +void ed::NavigateAction::NavigateTo(const ImRect& bounds, bool zoomIn, float duration, NavigationReason reason) +{ + if (ImRect_IsEmpty(bounds)) + return; + + if (!zoomIn) + { + auto viewRect = m_Canvas.ViewRect(); + auto viewRectCenter = viewRect.GetCenter(); + auto targetCenter = bounds.GetCenter(); + + viewRect.Translate(targetCenter - viewRectCenter); + + NavigateTo(viewRect, GetStyle().ScrollDuration, reason); + } + else + { + // Grow rect by 5% to leave some reasonable margin + // from the edges of the canvas. + auto rect = bounds; + auto extend = ImMax(rect.GetWidth(), rect.GetHeight()); + rect.Expand(extend * c_NavigationZoomMargin * 0.5f); + + NavigateTo(rect, GetStyle().ScrollDuration, reason); + } +} + +void ed::NavigateAction::NavigateTo(const ImRect& target, float duration, NavigationReason reason) +{ + m_Reason = reason; + + m_Animation.NavigateTo(target, duration); +} + +void ed::NavigateAction::StopNavigation() +{ + m_Animation.Stop(); +} + +void ed::NavigateAction::FinishNavigation() +{ + m_Animation.Finish(); +} + +bool ed::NavigateAction::MoveOverEdge() +{ + // Don't interrupt non-edge animations + if (m_Animation.IsPlaying()) + return false; + + auto& io = ImGui::GetIO(); + const auto screenRect = m_Canvas.ViewRect(); + const auto screenMousePos = io.MousePos; + + // Mouse is over screen, do nothing + if (screenRect.Contains(screenMousePos)) + return false; + + const auto screenPointOnEdge = ImRect_ClosestPoint(screenRect, screenMousePos, true); + const auto direction = screenPointOnEdge - screenMousePos; + const auto offset = -direction * io.DeltaTime * 10.0f; + + m_Scroll = m_Scroll + offset; + + m_MoveOffset = offset; + m_MovingOverEdge = true; + + return true; +} + +void ed::NavigateAction::StopMoveOverEdge() +{ + if (m_MovingOverEdge) + { + Editor->MakeDirty(SaveReasonFlags::Navigation); + + m_MoveOffset = ImVec2(0, 0); + m_MovingOverEdge = false; + } +} + +void ed::NavigateAction::SetWindow(ImVec2 position, ImVec2 size) +{ + m_WindowScreenPos = position; + m_WindowScreenSize = size; +} + +ImGuiEx::CanvasView ed::NavigateAction::GetView() const +{ + return ImGuiEx::CanvasView(-m_Scroll, m_Zoom); +} + +ImVec2 ed::NavigateAction::GetViewOrigin() const +{ + return -m_Scroll; +} + +float ed::NavigateAction::GetViewScale() const +{ + return m_Zoom; +} + +void ed::NavigateAction::SetViewRect(const ImRect& rect) +{ + auto view = m_Canvas.CalcCenterView(rect); + m_Scroll = -view.Origin; + m_Zoom = view.Scale; +} + +ImRect ed::NavigateAction::GetViewRect() const +{ + return m_Canvas.CalcViewRect(GetView()); +} + +float ed::NavigateAction::MatchZoom(int steps, float fallbackZoom) +{ + auto currentZoomIndex = MatchZoomIndex(steps); + if (currentZoomIndex < 0) + return fallbackZoom; + + auto currentZoom = s_ZoomLevels[currentZoomIndex]; + if (fabsf(currentZoom - m_Zoom) > 0.001f) + return currentZoom; + + auto newIndex = currentZoomIndex + steps; + if (newIndex >= 0 && newIndex < s_ZoomLevelCount) + return s_ZoomLevels[newIndex]; + else + return fallbackZoom; +} + +int ed::NavigateAction::MatchZoomIndex(int direction) +{ + int bestIndex = -1; + float bestDistance = 0.0f; + + for (int i = 0; i < s_ZoomLevelCount; ++i) + { + auto distance = fabsf(s_ZoomLevels[i] - m_Zoom); + if (distance < bestDistance || bestIndex < 0) + { + bestDistance = distance; + bestIndex = i; + } + } + + if (bestDistance > 0.001f) + { + if (direction > 0) + { + ++bestIndex; + + if (bestIndex >= s_ZoomLevelCount) + bestIndex = s_ZoomLevelCount - 1; + } + else if (direction < 0) + { + --bestIndex; + + if (bestIndex < 0) + bestIndex = 0; + } + } + + return bestIndex; +} + + + + +//------------------------------------------------------------------------------ +// +// Size Action +// +//------------------------------------------------------------------------------ +ed::SizeAction::SizeAction(EditorContext* editor): + EditorAction(editor), + m_IsActive(false), + m_Clean(false), + m_SizedNode(nullptr), + m_Pivot(NodeRegion::None), + m_Cursor(ImGuiMouseCursor_Arrow) +{ +} + +ed::EditorAction::AcceptResult ed::SizeAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return False; + + if (control.ActiveNode && IsGroup(control.ActiveNode) && ImGui::IsMouseDragging(0, 0)) + { + //const auto mousePos = to_point(ImGui::GetMousePos()); + //const auto closestPoint = control.ActiveNode->Bounds.get_closest_point_hollow(mousePos, static_cast(control.ActiveNode->Rounding)); + + auto pivot = GetRegion(control.ActiveNode); + if (pivot != NodeRegion::Header && pivot != NodeRegion::Center) + { + m_StartBounds = control.ActiveNode->m_Bounds; + m_StartGroupBounds = control.ActiveNode->m_GroupBounds; + m_LastSize = control.ActiveNode->m_Bounds.GetSize(); + m_MinimumSize = ImVec2(0, 0); + m_LastDragOffset = ImVec2(0, 0); + m_Pivot = pivot; + m_Cursor = ChooseCursor(m_Pivot); + m_SizedNode = control.ActiveNode; + m_IsActive = true; + } + } + else if (control.HotNode && IsGroup(control.HotNode)) + { + m_Cursor = ChooseCursor(GetRegion(control.HotNode)); + return Possible; + } + + return m_IsActive ? True : False; +} + +bool ed::SizeAction::Process(const Control& control) +{ + if (m_Clean) + { + m_Clean = false; + + if (m_SizedNode->m_Bounds.Min != m_StartBounds.Min || m_SizedNode->m_GroupBounds.Min != m_StartGroupBounds.Min) + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_SizedNode); + + if (m_SizedNode->m_Bounds.GetSize() != m_StartBounds.GetSize() || m_SizedNode->m_GroupBounds.GetSize() != m_StartGroupBounds.GetSize()) + Editor->MakeDirty(SaveReasonFlags::Size | SaveReasonFlags::User, m_SizedNode); + + m_SizedNode = nullptr; + } + + if (!m_IsActive) + return false; + + if (control.ActiveNode == m_SizedNode) + { + const auto dragOffset = (control.ActiveNode == m_SizedNode) ? ImGui::GetMouseDragDelta(0, 0.0f) : m_LastDragOffset; + m_LastDragOffset = dragOffset; + + if (m_MinimumSize.x == 0.0f && m_LastSize.x != m_SizedNode->m_Bounds.GetWidth()) + m_MinimumSize.x = m_SizedNode->m_Bounds.GetWidth(); + if (m_MinimumSize.y == 0.0f && m_LastSize.y != m_SizedNode->m_Bounds.GetHeight()) + m_MinimumSize.y = m_SizedNode->m_Bounds.GetHeight(); + + auto minimumSize = ImMax(m_MinimumSize, m_StartBounds.GetSize() - m_StartGroupBounds.GetSize()); + + + auto newBounds = m_StartBounds; + + if ((m_Pivot & NodeRegion::Top) == NodeRegion::Top) + newBounds.Min.y = ImMin(newBounds.Max.y - minimumSize.y, Editor->AlignPointToGrid(newBounds.Min.y + dragOffset.y)); + if ((m_Pivot & NodeRegion::Bottom) == NodeRegion::Bottom) + newBounds.Max.y = ImMax(newBounds.Min.y + minimumSize.y, Editor->AlignPointToGrid(newBounds.Max.y + dragOffset.y)); + if ((m_Pivot & NodeRegion::Left) == NodeRegion::Left) + newBounds.Min.x = ImMin(newBounds.Max.x - minimumSize.x, Editor->AlignPointToGrid(newBounds.Min.x + dragOffset.x)); + if ((m_Pivot & NodeRegion::Right) == NodeRegion::Right) + newBounds.Max.x = ImMax(newBounds.Min.x + minimumSize.x, Editor->AlignPointToGrid(newBounds.Max.x + dragOffset.x)); + + newBounds.Min = ImFloor(newBounds.Min); newBounds.Max = ImFloor(newBounds.Max); // ImRect::Floor removed upstream in ImGui + + m_LastSize = newBounds.GetSize(); + + m_SizedNode->m_Bounds = newBounds; + m_SizedNode->m_GroupBounds = newBounds; + m_SizedNode->m_GroupBounds.Min.x -= m_StartBounds.Min.x - m_StartGroupBounds.Min.x; + m_SizedNode->m_GroupBounds.Min.y -= m_StartBounds.Min.y - m_StartGroupBounds.Min.y; + m_SizedNode->m_GroupBounds.Max.x -= m_StartBounds.Max.x - m_StartGroupBounds.Max.x; + m_SizedNode->m_GroupBounds.Max.y -= m_StartBounds.Max.y - m_StartGroupBounds.Max.y; + } + else if (!control.ActiveNode) + { + m_Clean = true; + m_IsActive = false; + return true; + } + + return m_IsActive; +} + +void ed::SizeAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + auto getObjectName = [](Object* object) + { + if (!object) return ""; + else if (object->AsNode()) return "Node"; + else if (object->AsPin()) return "Pin"; + else if (object->AsLink()) return "Link"; + else return ""; + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Node: %s (%p)", getObjectName(m_SizedNode), m_SizedNode ? m_SizedNode->m_ID.AsPointer() : nullptr); + if (m_SizedNode && m_IsActive) + { + ImGui::Text(" Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_Bounds.Min.x, m_SizedNode->m_Bounds.Min.y, m_SizedNode->m_Bounds.GetWidth(), m_SizedNode->m_Bounds.GetHeight()); + ImGui::Text(" Group Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_GroupBounds.Min.x, m_SizedNode->m_GroupBounds.Min.y, m_SizedNode->m_GroupBounds.GetWidth(), m_SizedNode->m_GroupBounds.GetHeight()); + ImGui::Text(" Start Bounds: { x=%g y=%g w=%g h=%g }", m_StartBounds.Min.x, m_StartBounds.Min.y, m_StartBounds.GetWidth(), m_StartBounds.GetHeight()); + ImGui::Text(" Start Group Bounds: { x=%g y=%g w=%g h=%g }", m_StartGroupBounds.Min.x, m_StartGroupBounds.Min.y, m_StartGroupBounds.GetWidth(), m_StartGroupBounds.GetHeight()); + ImGui::Text(" Minimum Size: { w=%g h=%g }", m_MinimumSize.x, m_MinimumSize.y); + ImGui::Text(" Last Size: { w=%g h=%g }", m_LastSize.x, m_LastSize.y); + } +} + +ed::NodeRegion ed::SizeAction::GetRegion(Node* node) +{ + return node->GetRegion(ImGui::GetMousePos()); +} + +ImGuiMouseCursor ed::SizeAction::ChooseCursor(NodeRegion region) +{ + switch (region) + { + default: + case NodeRegion::Center: + return ImGuiMouseCursor_Arrow; + + case NodeRegion::Top: + case NodeRegion::Bottom: + return ImGuiMouseCursor_ResizeNS; + + case NodeRegion::Left: + case NodeRegion::Right: + return ImGuiMouseCursor_ResizeEW; + + case NodeRegion::TopLeft: + case NodeRegion::BottomRight: + return ImGuiMouseCursor_ResizeNWSE; + + case NodeRegion::TopRight: + case NodeRegion::BottomLeft: + return ImGuiMouseCursor_ResizeNESW; + } +} + + + + +//------------------------------------------------------------------------------ +// +// Drag Action +// +//------------------------------------------------------------------------------ +ed::DragAction::DragAction(EditorContext* editor): + EditorAction(editor), + m_IsActive(false), + m_Clear(false), + m_DraggedObject(nullptr) +{ +} + +ed::EditorAction::AcceptResult ed::DragAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return False; + + // UsdLayerManager local patch: control.ActiveObject is ImGui's own + // per-frame hover/active resolution, which normally correctly identifies + // and keeps identifying the node the mouse-down landed on. In this + // project's embedding (a node-editor canvas nested inside an extra + // ImGui child window), some specific nodes have that resolution fail — + // BackgroundActive instead of the node — every single frame of the + // gesture, not just transiently, so there is no later frame to + // self-correct into. Fall back to the node the press geometrically + // landed on (Editor->GetPressedNode(), computed once in BuildControl() + // independent of ImGui's hover state) so the drag can still start. + Object* activeObject = control.ActiveObject; + if (!activeObject && Editor->GetPressedNode() && ImGui::IsMouseDragging(0)) + activeObject = Editor->GetPressedNode(); + + if (activeObject && ImGui::IsMouseDragging(0)) + { + if (!activeObject->AcceptDrag()) + return False; + + m_DraggedObject = activeObject; + + m_Objects.resize(0); + m_Objects.push_back(m_DraggedObject); + + if (Editor->IsSelected(m_DraggedObject)) + { + for (auto selectedObject : Editor->GetSelectedObjects()) + if (auto selectedNode = selectedObject->AsNode()) + if (selectedNode != m_DraggedObject && selectedNode->AcceptDrag()) + m_Objects.push_back(selectedNode); + } + + auto& io = ImGui::GetIO(); + if (!io.KeyShift) + { + std::vector groupedNodes; + for (auto object : m_Objects) + if (auto node = object->AsNode()) + node->GetGroupedNodes(groupedNodes, true); + + auto isAlreadyPicked = [this](Node* node) + { + return std::find(m_Objects.begin(), m_Objects.end(), node) != m_Objects.end(); + }; + + for (auto candidate : groupedNodes) + if (!isAlreadyPicked(candidate) && candidate->AcceptDrag()) + m_Objects.push_back(candidate); + } + + m_IsActive = true; + } + else if (control.HotNode && IsGroup(control.HotNode) && control.HotNode->GetRegion(ImGui::GetMousePos()) == NodeRegion::Header) + { + return Possible; + } + + return m_IsActive ? True : False; +} + +bool ed::DragAction::Process(const Control& control) +{ + if (m_Clear) + { + m_Clear = false; + + for (auto object : m_Objects) + { + if (object->EndDrag()) + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, object->AsNode()); + } + + m_Objects.resize(0); + + m_DraggedObject = nullptr; + } + + if (!m_IsActive) + return false; + + // UsdLayerManager local patch: see Accept()'s fallback comment. The same + // per-frame ImGui attribution failure that Accept() falls back for can + // recur on later frames of the same gesture too — if we only trusted + // control.ActiveObject here, one such frame would hit the "nothing is + // active" branch below and terminate a drag that Accept() only just + // started via the fallback, before any movement happened. Keep + // continuing the drag as long as the mouse is still physically down and + // Editor->GetPressedNode() (cleared only on release, see BuildControl()) + // still names this exact dragged node. + const bool stillDraggingViaFallback = + !control.ActiveObject && ImGui::IsMouseDown(0) && + Editor->GetPressedNode() == m_DraggedObject; + + if (control.ActiveObject == m_DraggedObject || stillDraggingViaFallback) + { + auto dragOffset = ImGui::GetMouseDragDelta(0, 0.0f); + + auto draggedOrigin = m_DraggedObject->DragStartLocation(); + auto alignPivot = ImVec2(0, 0); + + // TODO: Move this experimental alignment to closes pivot out of internals to node API + if (auto draggedNode = m_DraggedObject->AsNode()) + { + float x = FLT_MAX; + float y = FLT_MAX; + + auto testPivot = [this, &x, &y, &draggedOrigin, &dragOffset, &alignPivot](const ImVec2& pivot) + { + auto initial = draggedOrigin + dragOffset + pivot; + auto candidate = Editor->AlignPointToGrid(initial) - draggedOrigin - pivot; + + if (ImFabs(candidate.x) < ImFabs(ImMin(x, FLT_MAX))) + { + x = candidate.x; + alignPivot.x = pivot.x; + } + + if (ImFabs(candidate.y) < ImFabs(ImMin(y, FLT_MAX))) + { + y = candidate.y; + alignPivot.y = pivot.y; + } + }; + + for (auto pin = draggedNode->m_LastPin; pin; pin = pin->m_PreviousPin) + { + auto pivot = pin->m_Pivot.GetCenter() - draggedNode->m_Bounds.Min; + testPivot(pivot); + } + + //testPivot(point(0, 0)); + } + + auto alignedOffset = Editor->AlignPointToGrid(draggedOrigin + dragOffset + alignPivot) - draggedOrigin - alignPivot; + + if (!ImGui::GetIO().KeyAlt) + dragOffset = alignedOffset; + + for (auto object : m_Objects) + object->UpdateDrag(dragOffset); + } + else if (!control.ActiveObject) + { + m_Clear = true; + + m_IsActive = false; + return true; + } + + return m_IsActive; +} + +void ed::DragAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + auto getObjectName = [](Object* object) + { + if (!object) return ""; + else if (object->AsNode()) return "Node"; + else if (object->AsPin()) return "Pin"; + else if (object->AsLink()) return "Link"; + else return ""; + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Node: %s (%p)", getObjectName(m_DraggedObject), m_DraggedObject ? m_DraggedObject->ID().AsPointer() : nullptr); +} + + + + +//------------------------------------------------------------------------------ +// +// Select Action +// +//------------------------------------------------------------------------------ +ed::SelectAction::SelectAction(EditorContext* editor): + EditorAction(editor), + m_IsActive(false), + m_SelectGroups(false), + m_SelectLinkMode(false), + m_CommitSelection(false), + m_StartPoint(), + m_Animation(editor) +{ +} + +ed::EditorAction::AcceptResult ed::SelectAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return False; + + auto& io = ImGui::GetIO(); + m_SelectGroups = io.KeyShift; + m_SelectLinkMode = io.KeyAlt; + + m_SelectedObjectsAtStart.clear(); + + // UsdLayerManager local patch: the node the press geometrically landed + // on (clip-independent, like FindNodesInRect), resolved only on the + // release frame so it matches native click timing. This whole embedding + // (node-editor canvas nested in an extra ImGui child window) has ImGui's + // per-item click/hover attribution fail for node InvisibleButtons — + // control.ClickedNode is null on every frame, so the native click-select + // path below never selects anything, while rect-select (pure geometry) + // always works. Using this as the authoritative click target restores + // single-click select for every node regardless of ImGui hover state or + // where the node sits relative to the canvas clip rect. + Node* geoPressedNode = ImGui::IsMouseReleased(0) ? Editor->GetPressedNode() : nullptr; + + // A rect-select gesture may only start on a genuine empty-canvas press. + // Guards against a node press being misread as a background drag and + // permanently latching a rubber-band (this branch owns every frame once + // m_IsActive is set). + if (control.BackgroundActive && ImGui::IsMouseDragging(0, 1) + && !Editor->GetPressedNode() && !Editor->GetPressedPin()) + { + m_IsActive = true; + m_StartPoint = ImGui::GetMousePos(); + m_EndPoint = m_StartPoint; + + // Links and nodes cannot be selected together + if ((m_SelectLinkMode && Editor->IsAnyNodeSelected()) || + (!m_SelectLinkMode && Editor->IsAnyLinkSelected())) + { + Editor->ClearSelection(); + } + + if (io.KeyCtrl) + m_SelectedObjectsAtStart = Editor->GetSelectedObjects(); + } + // Empty-canvas click clears selection — but only when the press really + // was on empty canvas; a geometric node press takes precedence even if + // ImGui misattributed the click to the background. + else if (control.BackgroundClicked && !geoPressedNode) + { + Editor->ClearSelection(); + } + else + { + Object* clickedObject = control.ClickedNode ? static_cast(control.ClickedNode) : static_cast(control.ClickedLink); + + // Fall back to the geometric press target when ImGui's native click + // detection comes up empty (which, in this embedding, is always). + if (!clickedObject) + clickedObject = geoPressedNode; + + if (clickedObject) + { + // Links and nodes cannot be selected together + if ((clickedObject->AsLink() && Editor->IsAnyNodeSelected()) || + (clickedObject->AsNode() && Editor->IsAnyLinkSelected())) + { + Editor->ClearSelection(); + } + + if (io.KeyCtrl) + Editor->ToggleObjectSelection(clickedObject); + else + Editor->SetSelectedObject(clickedObject); + } + } + + if (m_IsActive) + m_Animation.Stop(); + + return m_IsActive ? True : False; +} + +bool ed::SelectAction::Process(const Control& control) +{ + if (m_CommitSelection) + { + Editor->ClearSelection(); + for (auto object : m_CandidateObjects) + Editor->SelectObject(object); + + m_CandidateObjects.clear(); + + m_CommitSelection = false; + } + + if (!m_IsActive) + return false; + + if (ImGui::IsMouseDragging(0, 0)) + { + m_EndPoint = ImGui::GetMousePos(); + + auto topLeft = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y)); + auto bottomRight = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y)); + auto rect = ImRect(topLeft, bottomRight); + if (rect.GetWidth() <= 0) + rect.Max.x = rect.Min.x + 1; + if (rect.GetHeight() <= 0) + rect.Max.y = rect.Min.y + 1; + + vector nodes; + vector links; + + if (m_SelectLinkMode) + { + Editor->FindLinksInRect(rect, links); + m_CandidateObjects.assign(links.begin(), links.end()); + } + else + { + Editor->FindNodesInRect(rect, nodes); + m_CandidateObjects.assign(nodes.begin(), nodes.end()); + + if (m_SelectGroups) + { + auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return !IsGroup(object->AsNode()); }); + m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); + } + else + { + auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return IsGroup(object->AsNode()); }); + m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); + } + } + + m_CandidateObjects.insert(m_CandidateObjects.end(), m_SelectedObjectsAtStart.begin(), m_SelectedObjectsAtStart.end()); + std::sort(m_CandidateObjects.begin(), m_CandidateObjects.end()); + m_CandidateObjects.erase(std::unique(m_CandidateObjects.begin(), m_CandidateObjects.end()), m_CandidateObjects.end()); + } + else + { + m_IsActive = false; + + m_Animation.Play(c_SelectionFadeOutDuration); + + m_CommitSelection = true; + + return true; + } + + return m_IsActive; +} + +void ed::SelectAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); +} + +void ed::SelectAction::Draw(ImDrawList* drawList) +{ + if (!m_IsActive && !m_Animation.IsPlaying()) + return; + + const auto alpha = m_Animation.IsPlaying() ? ImEasing::EaseOutQuad(1.0f, -1.0f, m_Animation.GetProgress()) : 1.0f; + + const auto fillColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRect : StyleColor_NodeSelRect, alpha); + const auto outlineColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRectBorder : StyleColor_NodeSelRectBorder, alpha); + + drawList->ChannelsSetCurrent(c_BackgroundChannel_SelectionRect); + + auto min = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y)); + auto max = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y)); + + drawList->AddRectFilled(min, max, fillColor); + FringeScaleScope fringe(1.0f); + drawList->AddRect(min, max, outlineColor); +} + + + + +//------------------------------------------------------------------------------ +// +// Context Menu Action +// +//------------------------------------------------------------------------------ +ed::ContextMenuAction::ContextMenuAction(EditorContext* editor): + EditorAction(editor), + m_CandidateMenu(Menu::None), + m_CurrentMenu(Menu::None), + m_ContextId() +{ +} + +ed::EditorAction::AcceptResult ed::ContextMenuAction::Accept(const Control& control) +{ + const auto isPressed = ImGui::IsMouseClicked(1); + const auto isReleased = ImGui::IsMouseReleased(1); + const auto isDragging = ImGui::IsMouseDragging(1); + + if (isPressed || isReleased || isDragging) + { + Menu candidateMenu = ContextMenuAction::None; + ObjectId contextId; + + if (auto hotObejct = control.HotObject) + { + if (hotObejct->AsNode()) + candidateMenu = Node; + else if (hotObejct->AsPin()) + candidateMenu = Pin; + else if (hotObejct->AsLink()) + candidateMenu = Link; + + if (candidateMenu != None) + contextId = hotObejct->ID(); + } + else if (control.BackgroundHot) + candidateMenu = Background; + + if (isPressed) + { + m_CandidateMenu = candidateMenu; + m_ContextId = contextId; + return Possible; + } + else if (isReleased && m_CandidateMenu == candidateMenu && m_ContextId == contextId) + { + m_CurrentMenu = m_CandidateMenu; + m_CandidateMenu = ContextMenuAction::None; + return True; + } + else + { + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); + return False; + } + } + + return False; +} + +bool ed::ContextMenuAction::Process(const Control& control) +{ + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); + return false; +} + +void ed::ContextMenuAction::Reject() +{ + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); +} + +void ed::ContextMenuAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + auto getMenuName = [](Menu menu) + { + switch (menu) + { + default: + case None: return "None"; + case Node: return "Node"; + case Pin: return "Pin"; + case Link: return "Link"; + case Background: return "Background"; + } + }; + + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Menu: %s", getMenuName(m_CurrentMenu)); +} + +bool ed::ContextMenuAction::ShowNodeContextMenu(NodeId* nodeId) +{ + if (m_CurrentMenu != Node) + return false; + + *nodeId = m_ContextId.AsNodeId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowPinContextMenu(PinId* pinId) +{ + if (m_CurrentMenu != Pin) + return false; + + *pinId = m_ContextId.AsPinId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowLinkContextMenu(LinkId* linkId) +{ + if (m_CurrentMenu != Link) + return false; + + *linkId = m_ContextId.AsLinkId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowBackgroundContextMenu() +{ + if (m_CurrentMenu != Background) + return false; + + Editor->SetUserContext(); + return true; +} + + + + +//------------------------------------------------------------------------------ +// +// Cut/Copy/Paste Action +// +//------------------------------------------------------------------------------ +ed::ShortcutAction::ShortcutAction(EditorContext* editor): + EditorAction(editor), + m_IsActive(false), + m_InAction(false), + m_CurrentAction(Action::None), + m_Context() +{ +} + +ed::EditorAction::AcceptResult ed::ShortcutAction::Accept(const Control& control) +{ + if (!Editor->IsActive() || !Editor->AreShortcutsEnabled()) + return False; + + Action candidateAction = None; + + auto& io = ImGui::GetIO(); + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_X)) + candidateAction = Cut; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_C)) + candidateAction = Copy; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_V)) + candidateAction = Paste; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(GetKeyIndexForD())) + candidateAction = Duplicate; + if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_Space)) + candidateAction = CreateNode; + + if (candidateAction != None) + { + if (candidateAction != Paste && candidateAction != CreateNode) + { + auto& selection = Editor->GetSelectedObjects(); + if (!selection.empty()) + { + // #TODO: Find a way to simplify logic. + + m_Context.assign(selection.begin(), selection.end()); + + // Expand groups + vector extra; + for (auto object : m_Context) + { + auto node = object->AsNode(); + if (IsGroup(node)) + node->GetGroupedNodes(extra, true); + } + + // Apply groups and remove duplicates + if (!extra.empty()) + { + m_Context.insert(m_Context.end(), extra.begin(), extra.end()); + std::sort(m_Context.begin(), m_Context.end()); + m_Context.erase(std::unique(m_Context.begin(), m_Context.end()), m_Context.end()); + } + } + else if (control.HotObject && control.HotObject->IsSelectable() && !IsGroup(control.HotObject->AsNode())) + { + m_Context.push_back(control.HotObject); + } + + if (m_Context.empty()) + return False; + + // Does copying only links make sense? + //const auto hasOnlyLinks = std::all_of(Context.begin(), Context.end(), [](Object* object) { return object->AsLink() != nullptr; }); + //if (hasOnlyLinks) + // return False; + + // If no links are selected, pick all links between nodes within context + const auto hasAnyLinks = std::any_of(m_Context.begin(), m_Context.end(), [](Object* object) { return object->AsLink() != nullptr; }); + if (!hasAnyLinks && m_Context.size() > 1) // one node cannot make connection to anything + { + // Collect nodes in sorted vector viable for binary search + std::vector> nodes; + + nodes.reserve(m_Context.size()); + std::for_each(m_Context.begin(), m_Context.end(), [&nodes](Object* object) { if (auto node = object->AsNode()) nodes.push_back({node->m_ID, node}); }); + + std::sort(nodes.begin(), nodes.end()); + + auto isNodeInContext = [&nodes](NodeId nodeId) + { + return std::binary_search(nodes.begin(), nodes.end(), ObjectWrapper{nodeId, nullptr}); + }; + + // Collect links connected to nodes and drop those reaching out of context + std::vector links; + + for (auto node : nodes) + Editor->FindLinksForNode(node.m_ID, links, true); + + // Remove duplicates + std::sort(links.begin(), links.end()); + links.erase(std::unique(links.begin(), links.end()), links.end()); + + // Drop out of context links + links.erase(std::remove_if(links.begin(), links.end(), [&isNodeInContext](Link* link) + { + return !isNodeInContext(link->m_StartPin->m_Node->m_ID) || !isNodeInContext(link->m_EndPin->m_Node->m_ID); + }), links.end()); + + // Append links and remove duplicates + m_Context.insert(m_Context.end(), links.begin(), links.end()); + } + } + else + m_Context.resize(0); + + m_IsActive = true; + m_CurrentAction = candidateAction; + + return True; + } + + return False; +} + +bool ed::ShortcutAction::Process(const Control& control) +{ + m_IsActive = false; + m_CurrentAction = None; + m_Context.resize(0); + return false; +} + +void ed::ShortcutAction::Reject() +{ + m_IsActive = false; + m_CurrentAction = None; + m_Context.resize(0); +} + +void ed::ShortcutAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + auto getActionName = [](Action action) + { + switch (action) + { + default: + case None: return "None"; + case Cut: return "Cut"; + case Copy: return "Copy"; + case Paste: return "Paste"; + case Duplicate: return "Duplicate"; + case CreateNode: return "CreateNode"; + } + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Action: %s", getActionName(m_CurrentAction)); +} + +bool ed::ShortcutAction::Begin() +{ + if (m_IsActive) + m_InAction = true; + return m_IsActive; +} + +void ed::ShortcutAction::End() +{ + if (m_IsActive) + m_InAction = false; +} + +bool ed::ShortcutAction::AcceptCut() +{ + IM_ASSERT(m_InAction); + return m_CurrentAction == Cut; +} + +bool ed::ShortcutAction::AcceptCopy() +{ + IM_ASSERT(m_InAction); + return m_CurrentAction == Copy; +} + +bool ed::ShortcutAction::AcceptPaste() +{ + IM_ASSERT(m_InAction); + return m_CurrentAction == Paste; +} + +bool ed::ShortcutAction::AcceptDuplicate() +{ + IM_ASSERT(m_InAction); + return m_CurrentAction == Duplicate; +} + +bool ed::ShortcutAction::AcceptCreateNode() +{ + IM_ASSERT(m_InAction); + return m_CurrentAction == CreateNode; +} + + + + +//------------------------------------------------------------------------------ +// +// Create Item Action +// +//------------------------------------------------------------------------------ +ed::CreateItemAction::CreateItemAction(EditorContext* editor): + EditorAction(editor), + m_InActive(false), + m_NextStage(None), + m_CurrentStage(None), + m_ItemType(NoItem), + m_UserAction(Unknown), + m_LinkColor(IM_COL32_WHITE), + m_LinkThickness(1.0f), + m_LinkStart(nullptr), + m_LinkEnd(nullptr), + + m_IsActive(false), + m_DraggedPin(nullptr), + + m_IsInGlobalSpace(false) +{ +} + +ed::EditorAction::AcceptResult ed::CreateItemAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return EditorAction::False; + + if (control.ActivePin && ImGui::IsMouseDragging(0)) + { + m_DraggedPin = control.ActivePin; + DragStart(m_DraggedPin); + + Editor->ClearSelection(); + } + else if (control.HotPin) + { + return EditorAction::Possible; + } + else + return EditorAction::False; + + m_IsActive = true; + + return EditorAction::True; +} + +bool ed::CreateItemAction::Process(const Control& control) +{ + IM_ASSERT(m_IsActive); + + if (!m_IsActive) + return false; + + if (m_DraggedPin && control.ActivePin == m_DraggedPin && (m_CurrentStage == Possible)) + { + const auto draggingFromSource = (m_DraggedPin->m_Kind == PinKind::Output); + + ed::Pin cursorPin(Editor, 0, draggingFromSource ? PinKind::Input : PinKind::Output); + cursorPin.m_Pivot = ImRect(ImGui::GetMousePos(), ImGui::GetMousePos()); + cursorPin.m_Dir = -m_DraggedPin->m_Dir; + cursorPin.m_Strength = m_DraggedPin->m_Strength; + + ed::Link candidate(Editor, 0); + candidate.m_Color = m_LinkColor; + candidate.m_StartPin = draggingFromSource ? m_DraggedPin : &cursorPin; + candidate.m_EndPin = draggingFromSource ? &cursorPin : m_DraggedPin; + + ed::Pin*& freePin = draggingFromSource ? candidate.m_EndPin : candidate.m_StartPin; + + if (control.HotPin) + { + DropPin(control.HotPin); + + if (m_UserAction == UserAccept) + freePin = control.HotPin; + } + else if (control.BackgroundHot) + DropNode(); + else + DropNothing(); + + auto drawList = ImGui::GetWindowDrawList(); + drawList->ChannelsSetCurrent(c_LinkChannel_NewLink); + + candidate.UpdateEndpoints(); + candidate.Draw(drawList, m_LinkColor, m_LinkThickness); + } + else if (m_CurrentStage == Possible || !control.ActivePin) + { + if (!ImGui::IsWindowHovered()) + { + m_DraggedPin = nullptr; + DropNothing(); + } + + DragEnd(); + m_IsActive = false; + } + + return m_IsActive; +} + +void ed::CreateItemAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + auto getStageName = [](Stage stage) + { + switch (stage) + { + case None: return "None"; + case Possible: return "Possible"; + case Create: return "Create"; + default: return ""; + } + }; + + auto getActionName = [](Action action) + { + switch (action) + { + default: + case Unknown: return "Unknown"; + case UserReject: return "Reject"; + case UserAccept: return "Accept"; + } + }; + + auto getItemName = [](Type item) + { + switch (item) + { + default: + case NoItem: return "None"; + case Node: return "Node"; + case Link: return "Link"; + } + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Stage: %s", getStageName(m_CurrentStage)); + ImGui::Text(" User Action: %s", getActionName(m_UserAction)); + ImGui::Text(" Item Type: %s", getItemName(m_ItemType)); +} + +void ed::CreateItemAction::SetStyle(ImU32 color, float thickness) +{ + m_LinkColor = color; + m_LinkThickness = thickness; +} + +bool ed::CreateItemAction::Begin() +{ + IM_ASSERT(false == m_InActive); + + m_InActive = true; + m_CurrentStage = m_NextStage; + m_UserAction = Unknown; + m_LinkColor = IM_COL32_WHITE; + m_LinkThickness = 1.0f; + + if (m_CurrentStage == None) + return false; + + m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current; + + return true; +} + +void ed::CreateItemAction::End() +{ + IM_ASSERT(m_InActive); + + if (m_IsInGlobalSpace) + { + ImGui::PopClipRect(); + Editor->Resume(); + + auto currentChannel = ImGui::GetWindowDrawList()->_Splitter._Current; + if (currentChannel != m_LastChannel) + ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel); + + m_IsInGlobalSpace = false; + } + + m_InActive = false; +} + +void ed::CreateItemAction::DragStart(Pin* startPin) +{ + IM_ASSERT(!m_InActive); + + m_NextStage = Possible; + m_LinkStart = startPin; + m_LinkEnd = nullptr; +} + +void ed::CreateItemAction::DragEnd() +{ + IM_ASSERT(!m_InActive); + + if (m_CurrentStage == Possible && m_UserAction == UserAccept) + { + m_NextStage = Create; + } + else + { + m_NextStage = None; + m_ItemType = NoItem; + m_LinkStart = nullptr; + m_LinkEnd = nullptr; + } +} + +void ed::CreateItemAction::DropPin(Pin* endPin) +{ + IM_ASSERT(!m_InActive); + + m_ItemType = Link; + m_LinkEnd = endPin; +} + +void ed::CreateItemAction::DropNode() +{ + IM_ASSERT(!m_InActive); + + m_ItemType = Node; + m_LinkEnd = nullptr; +} + +void ed::CreateItemAction::DropNothing() +{ + IM_ASSERT(!m_InActive); + + m_ItemType = NoItem; + m_LinkEnd = nullptr; +} + +ed::CreateItemAction::Result ed::CreateItemAction::RejectItem() +{ + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) + return Indeterminate; + + m_UserAction = UserReject; + + return True; +} + +ed::CreateItemAction::Result ed::CreateItemAction::AcceptItem() +{ + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) + return Indeterminate; + + m_UserAction = UserAccept; + + if (m_CurrentStage == Create) + { + m_NextStage = None; + m_ItemType = NoItem; + m_LinkStart = nullptr; + m_LinkEnd = nullptr; + return True; + } + else + return False; +} + +ed::CreateItemAction::Result ed::CreateItemAction::QueryLink(PinId* startId, PinId* endId) +{ + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType != Link) + return Indeterminate; + + auto linkStartId = m_LinkStart->m_ID; + auto linkEndId = m_LinkEnd->m_ID; + + *startId = linkStartId; + *endId = linkEndId; + + Editor->SetUserContext(true); + + if (!m_IsInGlobalSpace) + { + Editor->Suspend(); + + auto rect = Editor->GetRect(); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + m_IsInGlobalSpace = true; + } + + return True; +} + +ed::CreateItemAction::Result ed::CreateItemAction::QueryNode(PinId* pinId) +{ + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType != Node) + return Indeterminate; + + *pinId = m_LinkStart ? m_LinkStart->m_ID : 0; + + Editor->SetUserContext(true); + + if (!m_IsInGlobalSpace) + { + Editor->Suspend(); + + auto rect = Editor->GetRect(); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + m_IsInGlobalSpace = true; + } + + return True; +} + + + + +//------------------------------------------------------------------------------ +// +// Delete Items Action +// +//------------------------------------------------------------------------------ +ed::DeleteItemsAction::DeleteItemsAction(EditorContext* editor): + EditorAction(editor), + m_IsActive(false), + m_InInteraction(false), + m_CurrentItemType(Unknown), + m_UserAction(Undetermined) +{ +} + +ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept(const Control& control) +{ + IM_ASSERT(!m_IsActive); + + if (m_IsActive) + return False; + + auto addDeadLinks = [this]() + { + vector links; + for (auto object : m_CandidateObjects) + { + auto node = object->AsNode(); + if (!node) + continue; + + Editor->FindLinksForNode(node->m_ID, links, true); + } + if (!links.empty()) + { + std::sort(links.begin(), links.end()); + links.erase(std::unique(links.begin(), links.end()), links.end()); + m_CandidateObjects.insert(m_CandidateObjects.end(), links.begin(), links.end()); + } + }; + + auto& io = ImGui::GetIO(); + if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete) && Editor->AreShortcutsEnabled()) + { + auto& selection = Editor->GetSelectedObjects(); + if (!selection.empty()) + { + m_CandidateObjects = selection; + addDeadLinks(); + m_IsActive = true; + return True; + } + } + else if (control.ClickedLink && io.KeyAlt) + { + m_CandidateObjects.clear(); + m_CandidateObjects.push_back(control.ClickedLink); + m_IsActive = true; + return True; + } + + else if (!m_ManuallyDeletedObjects.empty()) + { + m_CandidateObjects = m_ManuallyDeletedObjects; + m_ManuallyDeletedObjects.clear(); + addDeadLinks(); + m_IsActive = true; + return True; + } + + return m_IsActive ? True : False; +} + +bool ed::DeleteItemsAction::Process(const Control& control) +{ + if (!m_IsActive) + return false; + + if (m_CandidateObjects.empty()) + { + m_IsActive = false; + return true; + } + + return m_IsActive; +} + +void ed::DeleteItemsAction::ShowMetrics() +{ + EditorAction::ShowMetrics(); + + //auto getObjectName = [](Object* object) + //{ + // if (!object) return ""; + // else if (object->AsNode()) return "Node"; + // else if (object->AsPin()) return "Pin"; + // else if (object->AsLink()) return "Link"; + // else return ""; + //}; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + //ImGui::Text(" Node: %s (%d)", getObjectName(DeleteItemsgedNode), DeleteItemsgedNode ? DeleteItemsgedNode->ID : 0); +} + +bool ed::DeleteItemsAction::Add(Object* object) +{ + if (Editor->GetCurrentAction() != nullptr) + return false; + + m_ManuallyDeletedObjects.push_back(object); + + return true; +} + +bool ed::DeleteItemsAction::Begin() +{ + if (!m_IsActive) + return false; + + IM_ASSERT(!m_InInteraction); + m_InInteraction = true; + + m_CurrentItemType = Unknown; + m_UserAction = Undetermined; + + return m_IsActive; +} + +void ed::DeleteItemsAction::End() +{ + if (!m_IsActive) + return; + + IM_ASSERT(m_InInteraction); + m_InInteraction = false; +} + +bool ed::DeleteItemsAction::QueryLink(LinkId* linkId, PinId* startId, PinId* endId) +{ + ObjectId objectId; + if (!QueryItem(&objectId, Link)) + return false; + + if (auto id = objectId.AsLinkId()) + *linkId = id; + else + return false; + + if (startId || endId) + { + auto link = Editor->FindLink(*linkId); + if (startId) + *startId = link->m_StartPin->m_ID; + if (endId) + *endId = link->m_EndPin->m_ID; + } + + return true; +} + +bool ed::DeleteItemsAction::QueryNode(NodeId* nodeId) +{ + ObjectId objectId; + if (!QueryItem(&objectId, Node)) + return false; + + if (auto id = objectId.AsNodeId()) + *nodeId = id; + else + return false; + + return true; +} + +bool ed::DeleteItemsAction::QueryItem(ObjectId* itemId, IteratorType itemType) +{ + if (!m_InInteraction) + return false; + + if (m_CurrentItemType != itemType) + { + m_CurrentItemType = itemType; + m_CandidateItemIndex = 0; + } + else if (m_UserAction == Undetermined) + { + RejectItem(); + } + + m_UserAction = Undetermined; + + auto itemCount = (int)m_CandidateObjects.size(); + while (m_CandidateItemIndex < itemCount) + { + auto item = m_CandidateObjects[m_CandidateItemIndex]; + if (itemType == Node) + { + if (auto node = item->AsNode()) + { + *itemId = node->m_ID; + return true; + } + } + else if (itemType == Link) + { + if (auto link = item->AsLink()) + { + *itemId = link->m_ID; + return true; + } + } + + ++m_CandidateItemIndex; + } + + if (m_CandidateItemIndex == itemCount) + m_CurrentItemType = Unknown; + + return false; +} + +bool ed::DeleteItemsAction::AcceptItem() +{ + if (!m_InInteraction) + return false; + + m_UserAction = Accepted; + + RemoveItem(); + + return true; +} + +void ed::DeleteItemsAction::RejectItem() +{ + if (!m_InInteraction) + return; + + m_UserAction = Rejected; + + RemoveItem(); +} + +void ed::DeleteItemsAction::RemoveItem() +{ + auto item = m_CandidateObjects[m_CandidateItemIndex]; + m_CandidateObjects.erase(m_CandidateObjects.begin() + m_CandidateItemIndex); + + Editor->DeselectObject(item); + + if (m_CurrentItemType == Link) + Editor->NotifyLinkDeleted(item->AsLink()); +} + + + + +//------------------------------------------------------------------------------ +// +// Node Builder +// +//------------------------------------------------------------------------------ +ed::NodeBuilder::NodeBuilder(EditorContext* editor): + Editor(editor), + m_CurrentNode(nullptr), + m_CurrentPin(nullptr) +{ +} + +void ed::NodeBuilder::Begin(NodeId nodeId) +{ + IM_ASSERT(nullptr == m_CurrentNode); + + m_CurrentNode = Editor->GetNode(nodeId); + + if (m_CurrentNode->m_RestoreState) + { + Editor->RestoreNodeState(m_CurrentNode); + m_CurrentNode->m_RestoreState = false; + } + + if (m_CurrentNode->m_CenterOnScreen) + { + auto bounds = Editor->GetViewRect(); + auto offset = bounds.GetCenter() - m_CurrentNode->m_Bounds.GetCenter(); + + if (ImLengthSqr(offset) > 0) + { + if (::IsGroup(m_CurrentNode)) + { + std::vector groupedNodes; + m_CurrentNode->GetGroupedNodes(groupedNodes); + groupedNodes.push_back(m_CurrentNode); + + for (auto node : groupedNodes) + { + node->m_Bounds.Translate(ImFloor(offset)); + node->m_GroupBounds.Translate(ImFloor(offset)); + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, node); + } + } + else + { + m_CurrentNode->m_Bounds.Translate(ImFloor(offset)); + m_CurrentNode->m_GroupBounds.Translate(ImFloor(offset)); + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_CurrentNode); + } + } + + m_CurrentNode->m_CenterOnScreen = false; + } + + // Position node on screen + ImGui::SetCursorScreenPos(m_CurrentNode->m_Bounds.Min); + + auto& editorStyle = Editor->GetStyle(); + + const auto alpha = ImGui::GetStyle().Alpha; + + m_CurrentNode->m_IsLive = true; + m_CurrentNode->m_LastPin = nullptr; + m_CurrentNode->m_Color = Editor->GetColor(StyleColor_NodeBg, alpha); + m_CurrentNode->m_BorderColor = Editor->GetColor(StyleColor_NodeBorder, alpha); + m_CurrentNode->m_BorderWidth = editorStyle.NodeBorderWidth; + m_CurrentNode->m_Rounding = editorStyle.NodeRounding; + m_CurrentNode->m_GroupColor = Editor->GetColor(StyleColor_GroupBg, alpha); + m_CurrentNode->m_GroupBorderColor = Editor->GetColor(StyleColor_GroupBorder, alpha); + m_CurrentNode->m_GroupBorderWidth = editorStyle.GroupBorderWidth; + m_CurrentNode->m_GroupRounding = editorStyle.GroupRounding; + + m_IsGroup = false; + + // Grow channel list and select user channel + if (auto drawList = ImGui::GetWindowDrawList()) + { + m_CurrentNode->m_Channel = drawList->_Splitter._Count; + ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode); + drawList->ChannelsSetCurrent(m_CurrentNode->m_Channel + c_NodeContentChannel); + } + + // Begin outer group + ImGui::BeginGroup(); + + // Apply frame padding. Begin inner group if necessary. + if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) + { + ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(editorStyle.NodePadding.x, editorStyle.NodePadding.y)); + ImGui::BeginGroup(); + } +} + +void ed::NodeBuilder::End() +{ + IM_ASSERT(nullptr != m_CurrentNode); + + // Apply frame padding. This must be done in this convoluted way if outer group + // size must contain inner group padding. + auto& editorStyle = Editor->GetStyle(); + if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) + { + ImGui::EndGroup(); + ImGui::SameLine(0, editorStyle.NodePadding.z); + ImGui::Dummy(ImVec2(0, 0)); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + editorStyle.NodePadding.w); + ImGui::Dummy(ImVec2(0, 0)); // pairs SetCursorPosY; bare SetCursorPos before EndGroup() asserts on ImGui 1.89+ + } + + // End outer group. + ImGui::EndGroup(); + + m_NodeRect = ImGui_GetItemRect(); + m_NodeRect.Min = ImFloor(m_NodeRect.Min); m_NodeRect.Max = ImFloor(m_NodeRect.Max); // ImRect::Floor removed upstream in ImGui + + if (m_CurrentNode->m_Bounds.GetSize() != m_NodeRect.GetSize()) + { + m_CurrentNode->m_Bounds.Max = m_CurrentNode->m_Bounds.Min + m_NodeRect.GetSize(); + Editor->MakeDirty(SaveReasonFlags::Size, m_CurrentNode); + } + + if (m_IsGroup) + { + // Groups cannot have pins. Discard them. + for (auto pin = m_CurrentNode->m_LastPin; pin; pin = pin->m_PreviousPin) + pin->Reset(); + + m_CurrentNode->m_Type = NodeType::Group; + m_CurrentNode->m_GroupBounds = m_GroupBounds; + m_CurrentNode->m_LastPin = nullptr; + } + else + m_CurrentNode->m_Type = NodeType::Node; + + m_CurrentNode = nullptr; +} + +void ed::NodeBuilder::BeginPin(PinId pinId, PinKind kind) +{ + IM_ASSERT(nullptr != m_CurrentNode); + IM_ASSERT(nullptr == m_CurrentPin); + IM_ASSERT(false == m_IsGroup); + + auto& editorStyle = Editor->GetStyle(); + + m_CurrentPin = Editor->GetPin(pinId, kind); + m_CurrentPin->m_Node = m_CurrentNode; + + m_CurrentPin->m_IsLive = true; + m_CurrentPin->m_Color = Editor->GetColor(StyleColor_PinRect); + m_CurrentPin->m_BorderColor = Editor->GetColor(StyleColor_PinRectBorder); + m_CurrentPin->m_BorderWidth = editorStyle.PinBorderWidth; + m_CurrentPin->m_Rounding = editorStyle.PinRounding; + m_CurrentPin->m_Corners = static_cast(editorStyle.PinCorners); + m_CurrentPin->m_Radius = editorStyle.PinRadius; + m_CurrentPin->m_ArrowSize = editorStyle.PinArrowSize; + m_CurrentPin->m_ArrowWidth = editorStyle.PinArrowWidth; + m_CurrentPin->m_Dir = kind == PinKind::Output ? editorStyle.SourceDirection : editorStyle.TargetDirection; + m_CurrentPin->m_Strength = editorStyle.LinkStrength; + + m_CurrentPin->m_PreviousPin = m_CurrentNode->m_LastPin; + m_CurrentNode->m_LastPin = m_CurrentPin; + + m_PivotAlignment = editorStyle.PivotAlignment; + m_PivotSize = editorStyle.PivotSize; + m_PivotScale = editorStyle.PivotScale; + m_ResolvePinRect = true; + m_ResolvePivot = true; + + ImGui::BeginGroup(); +} + +void ed::NodeBuilder::EndPin() +{ + IM_ASSERT(nullptr != m_CurrentPin); + + ImGui::EndGroup(); + + if (m_ResolvePinRect) + m_CurrentPin->m_Bounds = ImGui_GetItemRect(); + + if (m_ResolvePivot) + { + auto& pinRect = m_CurrentPin->m_Bounds; + + if (m_PivotSize.x < 0) + m_PivotSize.x = pinRect.GetWidth(); + if (m_PivotSize.y < 0) + m_PivotSize.y = pinRect.GetHeight(); + + m_CurrentPin->m_Pivot.Min = pinRect.Min + ImMul(pinRect.GetSize(), m_PivotAlignment); + m_CurrentPin->m_Pivot.Max = m_CurrentPin->m_Pivot.Min + ImMul(m_PivotSize, m_PivotScale); + } + + m_CurrentPin = nullptr; +} + +void ed::NodeBuilder::PinRect(const ImVec2& a, const ImVec2& b) +{ + IM_ASSERT(nullptr != m_CurrentPin); + + m_CurrentPin->m_Bounds = ImRect(a, b); + m_CurrentPin->m_Bounds.Min = ImFloor(m_CurrentPin->m_Bounds.Min); m_CurrentPin->m_Bounds.Max = ImFloor(m_CurrentPin->m_Bounds.Max); // ImRect::Floor removed upstream in ImGui + m_ResolvePinRect = false; +} + +void ed::NodeBuilder::PinPivotRect(const ImVec2& a, const ImVec2& b) +{ + IM_ASSERT(nullptr != m_CurrentPin); + + m_CurrentPin->m_Pivot = ImRect(a, b); + m_ResolvePivot = false; +} + +void ed::NodeBuilder::PinPivotSize(const ImVec2& size) +{ + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotSize = size; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::PinPivotScale(const ImVec2& scale) +{ + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotScale = scale; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::PinPivotAlignment(const ImVec2& alignment) +{ + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotAlignment = alignment; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::Group(const ImVec2& size) +{ + IM_ASSERT(nullptr != m_CurrentNode); + IM_ASSERT(nullptr == m_CurrentPin); + IM_ASSERT(false == m_IsGroup); + + m_IsGroup = true; + + if (IsGroup(m_CurrentNode)) + ImGui::Dummy(m_CurrentNode->m_GroupBounds.GetSize()); + else + ImGui::Dummy(size); + + m_GroupBounds = ImGui_GetItemRect(); + m_GroupBounds.Min = ImFloor(m_GroupBounds.Min); m_GroupBounds.Max = ImFloor(m_GroupBounds.Max); // ImRect::Floor removed upstream in ImGui +} + +ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList() const +{ + return GetUserBackgroundDrawList(m_CurrentNode); +} + +ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList(Node* node) const +{ + if (node && node->m_IsLive) + { + auto drawList = ImGui::GetWindowDrawList(); + drawList->ChannelsSetCurrent(node->m_Channel + c_NodeUserBackgroundChannel); + return drawList; + } + else + return nullptr; +} + + + + +//------------------------------------------------------------------------------ +// +// Node Builder +// +//------------------------------------------------------------------------------ +ed::HintBuilder::HintBuilder(EditorContext* editor): + Editor(editor), + m_IsActive(false), + m_CurrentNode(nullptr) +{ +} + +bool ed::HintBuilder::Begin(NodeId nodeId) +{ + IM_ASSERT(nullptr == m_CurrentNode); + + auto& view = Editor->GetView(); + auto& rect = Editor->GetRect(); + + const float c_min_zoom = 0.75f; + const float c_max_zoom = 0.50f; + + if (view.Scale > 0.75f) + return false; + + auto node = Editor->FindNode(nodeId); + if (!IsGroup(node)) + return false; + + m_CurrentNode = node; + + m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current; + + Editor->Suspend(); + + const auto alpha = ImMax(0.0f, std::min(1.0f, (view.Scale - c_min_zoom) / (c_max_zoom - c_min_zoom))); + + ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + + ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + + m_IsActive = true; + + return true; +} + +void ed::HintBuilder::End() +{ + if (!m_IsActive) + return; + + ImGui::PopStyleVar(); + + ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); + ImGui::PopClipRect(); + + ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); + ImGui::PopClipRect(); + + ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel); + + Editor->Resume(); + + m_IsActive = false; + m_CurrentNode = nullptr; +} + +ImVec2 ed::HintBuilder::GetGroupMin() +{ + IM_ASSERT(nullptr != m_CurrentNode); + + return Editor->ToScreen(m_CurrentNode->m_Bounds.Min); +} + +ImVec2 ed::HintBuilder::GetGroupMax() +{ + IM_ASSERT(nullptr != m_CurrentNode); + + return Editor->ToScreen(m_CurrentNode->m_Bounds.Max); +} + +ImDrawList* ed::HintBuilder::GetForegroundDrawList() +{ + IM_ASSERT(nullptr != m_CurrentNode); + + auto drawList = ImGui::GetWindowDrawList(); + + drawList->ChannelsSetCurrent(c_UserChannel_Hints); + + return drawList; +} + +ImDrawList* ed::HintBuilder::GetBackgroundDrawList() +{ + IM_ASSERT(nullptr != m_CurrentNode); + + auto drawList = ImGui::GetWindowDrawList(); + + drawList->ChannelsSetCurrent(c_UserChannel_HintsBackground); + + return drawList; +} + + + + +//------------------------------------------------------------------------------ +// +// Style +// +//------------------------------------------------------------------------------ +void ed::Style::PushColor(StyleColor colorIndex, const ImVec4& color) +{ + ColorModifier modifier; + modifier.Index = colorIndex; + modifier.Value = Colors[colorIndex]; + m_ColorStack.push_back(modifier); + Colors[colorIndex] = color; +} + +void ed::Style::PopColor(int count) +{ + while (count > 0) + { + auto& modifier = m_ColorStack.back(); + Colors[modifier.Index] = modifier.Value; + m_ColorStack.pop_back(); + --count; + } +} + +void ed::Style::PushVar(StyleVar varIndex, float value) +{ + auto* var = GetVarFloatAddr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = ImVec4(*var, 0, 0, 0); + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PushVar(StyleVar varIndex, const ImVec2& value) +{ + auto* var = GetVarVec2Addr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = ImVec4(var->x, var->y, 0, 0); + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PushVar(StyleVar varIndex, const ImVec4& value) +{ + auto* var = GetVarVec4Addr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = *var; + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PopVar(int count) +{ + while (count > 0) + { + auto& modifier = m_VarStack.back(); + if (auto v = GetVarFloatAddr(modifier.Index)) + *v = modifier.Value.x; + else if (auto v = GetVarVec2Addr(modifier.Index)) + *v = ImVec2(modifier.Value.x, modifier.Value.y); + else if (auto v = GetVarVec4Addr(modifier.Index)) + *v = modifier.Value; + m_VarStack.pop_back(); + --count; + } +} + +const char* ed::Style::GetColorName(StyleColor colorIndex) const +{ + switch (colorIndex) + { + case StyleColor_Bg: return "Bg"; + case StyleColor_Grid: return "Grid"; + case StyleColor_NodeBg: return "NodeBg"; + case StyleColor_NodeBorder: return "NodeBorder"; + case StyleColor_HovNodeBorder: return "HoveredNodeBorder"; + case StyleColor_SelNodeBorder: return "SelNodeBorder"; + case StyleColor_NodeSelRect: return "NodeSelRect"; + case StyleColor_NodeSelRectBorder: return "NodeSelRectBorder"; + case StyleColor_HovLinkBorder: return "HoveredLinkBorder"; + case StyleColor_SelLinkBorder: return "SelLinkBorder"; + case StyleColor_LinkSelRect: return "LinkSelRect"; + case StyleColor_LinkSelRectBorder: return "LinkSelRectBorder"; + case StyleColor_PinRect: return "PinRect"; + case StyleColor_PinRectBorder: return "PinRectBorder"; + case StyleColor_Flow: return "Flow"; + case StyleColor_FlowMarker: return "FlowMarker"; + case StyleColor_GroupBg: return "GroupBg"; + case StyleColor_GroupBorder: return "GroupBorder"; + case StyleColor_Count: break; + } + + IM_ASSERT(0); + return "Unknown"; +} + +float* ed::Style::GetVarFloatAddr(StyleVar idx) +{ + switch (idx) + { + case StyleVar_NodeRounding: return &NodeRounding; + case StyleVar_NodeBorderWidth: return &NodeBorderWidth; + case StyleVar_HoveredNodeBorderWidth: return &HoveredNodeBorderWidth; + case StyleVar_SelectedNodeBorderWidth: return &SelectedNodeBorderWidth; + case StyleVar_PinRounding: return &PinRounding; + case StyleVar_PinBorderWidth: return &PinBorderWidth; + case StyleVar_LinkStrength: return &LinkStrength; + case StyleVar_ScrollDuration: return &ScrollDuration; + case StyleVar_FlowMarkerDistance: return &FlowMarkerDistance; + case StyleVar_FlowSpeed: return &FlowSpeed; + case StyleVar_FlowDuration: return &FlowDuration; + case StyleVar_PinCorners: return &PinCorners; + case StyleVar_PinRadius: return &PinRadius; + case StyleVar_PinArrowSize: return &PinArrowSize; + case StyleVar_PinArrowWidth: return &PinArrowWidth; + case StyleVar_GroupRounding: return &GroupRounding; + case StyleVar_GroupBorderWidth: return &GroupBorderWidth; + default: return nullptr; + } +} + +ImVec2* ed::Style::GetVarVec2Addr(StyleVar idx) +{ + switch (idx) + { + case StyleVar_SourceDirection: return &SourceDirection; + case StyleVar_TargetDirection: return &TargetDirection; + case StyleVar_PivotAlignment: return &PivotAlignment; + case StyleVar_PivotSize: return &PivotSize; + case StyleVar_PivotScale: return &PivotScale; + default: return nullptr; + } +} + +ImVec4* ed::Style::GetVarVec4Addr(StyleVar idx) +{ + switch (idx) + { + case StyleVar_NodePadding: return &NodePadding; + default: return nullptr; + } +} + + + + +//------------------------------------------------------------------------------ +// +// Config +// +//------------------------------------------------------------------------------ +ed::Config::Config(const ax::NodeEditor::Config* config) +{ + if (config) + *static_cast(this) = *config; +} + +std::string ed::Config::Load() +{ + std::string data; + + if (LoadSettings) + { + const auto size = LoadSettings(nullptr, UserPointer); + if (size > 0) + { + data.resize(size); + LoadSettings(const_cast(data.data()), UserPointer); + } + } + else if (SettingsFile) + { + std::ifstream file(SettingsFile); + if (file) + { + file.seekg(0, std::ios_base::end); + auto size = static_cast(file.tellg()); + file.seekg(0, std::ios_base::beg); + + data.reserve(size); + data.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + } + + return data; +} + +std::string ed::Config::LoadNode(NodeId nodeId) +{ + std::string data; + + if (LoadNodeSettings) + { + const auto size = LoadNodeSettings(nodeId, nullptr, UserPointer); + if (size > 0) + { + data.resize(size); + LoadNodeSettings(nodeId, const_cast(data.data()), UserPointer); + } + } + + return data; +} + +void ed::Config::BeginSave() +{ + if (BeginSaveSession) + BeginSaveSession(UserPointer); +} + +bool ed::Config::Save(const std::string& data, SaveReasonFlags flags) +{ + if (SaveSettings) + { + return SaveSettings(data.c_str(), data.size(), flags, UserPointer); + } + else if (SettingsFile) + { + std::ofstream settingsFile(SettingsFile); + if (settingsFile) + settingsFile << data; + + return !!settingsFile; + } + + return false; +} + +bool ed::Config::SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags) +{ + if (SaveNodeSettings) + return SaveNodeSettings(nodeId, data.c_str(), data.size(), flags, UserPointer); + + return false; +} + +void ed::Config::EndSave() +{ + if (EndSaveSession) + EndSaveSession(UserPointer); +} diff --git a/src/ui/NodeEditor/Source/imgui_node_editor_api.cpp b/src/ui/NodeEditor/Source/imgui_node_editor_api.cpp new file mode 100644 index 0000000..d468b4e --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_node_editor_api.cpp @@ -0,0 +1,637 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# include "imgui_node_editor_internal.h" +# include + + +//------------------------------------------------------------------------------ +static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr; + + +//------------------------------------------------------------------------------ +template +static int BuildIdList(C& container, I* list, int listSize, F&& accept) +{ + if (list != nullptr) + { + int count = 0; + for (auto object : container) + { + if (listSize <= 0) + break; + + if (accept(object)) + { + list[count] = I(object->ID().AsPointer()); + ++count; + --listSize; + } + } + + return count; + } + else + return static_cast(std::count_if(container.begin(), container.end(), accept)); +} + + +//------------------------------------------------------------------------------ +ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor(const Config* config) +{ + return reinterpret_cast(new ax::NodeEditor::Detail::EditorContext(config)); +} + +void ax::NodeEditor::DestroyEditor(EditorContext* ctx) +{ + if (GetCurrentEditor() == ctx) + SetCurrentEditor(nullptr); + + auto editor = reinterpret_cast(ctx); + + delete editor; +} + +void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx) +{ + s_Editor = reinterpret_cast(ctx); +} + +ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor() +{ + return reinterpret_cast(s_Editor); +} + +ax::NodeEditor::Style& ax::NodeEditor::GetStyle() +{ + return s_Editor->GetStyle(); +} + +const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex) +{ + return s_Editor->GetStyle().GetColorName(colorIndex); +} + +void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, const ImVec4& color) +{ + s_Editor->GetStyle().PushColor(colorIndex, color); +} + +void ax::NodeEditor::PopStyleColor(int count) +{ + s_Editor->GetStyle().PopColor(count); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value) +{ + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value) +{ + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value) +{ + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PopStyleVar(int count) +{ + s_Editor->GetStyle().PopVar(count); +} + +void ax::NodeEditor::Begin(const char* id, const ImVec2& size) +{ + s_Editor->Begin(id, size); +} + +void ax::NodeEditor::End() +{ + s_Editor->End(); +} + +void ax::NodeEditor::BeginNode(NodeId id) +{ + s_Editor->GetNodeBuilder().Begin(id); +} + +void ax::NodeEditor::BeginPin(PinId id, PinKind kind) +{ + s_Editor->GetNodeBuilder().BeginPin(id, kind); +} + +void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b) +{ + s_Editor->GetNodeBuilder().PinRect(a, b); +} + +void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b) +{ + s_Editor->GetNodeBuilder().PinPivotRect(a, b); +} + +void ax::NodeEditor::PinPivotSize(const ImVec2& size) +{ + s_Editor->GetNodeBuilder().PinPivotSize(size); +} + +void ax::NodeEditor::PinPivotScale(const ImVec2& scale) +{ + s_Editor->GetNodeBuilder().PinPivotScale(scale); +} + +void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment) +{ + s_Editor->GetNodeBuilder().PinPivotAlignment(alignment); +} + +void ax::NodeEditor::EndPin() +{ + s_Editor->GetNodeBuilder().EndPin(); +} + +void ax::NodeEditor::Group(const ImVec2& size) +{ + s_Editor->GetNodeBuilder().Group(size); +} + +void ax::NodeEditor::EndNode() +{ + s_Editor->GetNodeBuilder().End(); +} + +bool ax::NodeEditor::BeginGroupHint(NodeId nodeId) +{ + return s_Editor->GetHintBuilder().Begin(nodeId); +} + +ImVec2 ax::NodeEditor::GetGroupMin() +{ + return s_Editor->GetHintBuilder().GetGroupMin(); +} + +ImVec2 ax::NodeEditor::GetGroupMax() +{ + return s_Editor->GetHintBuilder().GetGroupMax(); +} + +ImDrawList* ax::NodeEditor::GetHintForegroundDrawList() +{ + return s_Editor->GetHintBuilder().GetForegroundDrawList(); +} + +ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList() +{ + return s_Editor->GetHintBuilder().GetBackgroundDrawList(); +} + +void ax::NodeEditor::EndGroupHint() +{ + s_Editor->GetHintBuilder().End(); +} + +ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId) +{ + if (auto node = s_Editor->FindNode(nodeId)) + return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node); + else + return nullptr; +} + +bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, float thickness/* = 1.0f*/) +{ + return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness); +} + +void ax::NodeEditor::Flow(LinkId linkId) +{ + if (auto link = s_Editor->FindLink(linkId)) + s_Editor->Flow(link); +} + +bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness) +{ + auto& context = s_Editor->GetItemCreator(); + + if (context.Begin()) + { + context.SetStyle(ImColor(color), thickness); + return true; + } + else + return false; +} + +bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.QueryLink(startId, endId) == Result::True; +} + +bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.QueryLink(startId, endId); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +bool ax::NodeEditor::QueryNewNode(PinId* pinId) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.QueryNode(pinId) == Result::True; +} + +bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, float thickness) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.QueryNode(pinId); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +bool ax::NodeEditor::AcceptNewItem() +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.AcceptItem() == Result::True; +} + +bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.AcceptItem(); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +void ax::NodeEditor::RejectNewItem() +{ + auto& context = s_Editor->GetItemCreator(); + + context.RejectItem(); +} + +void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness) +{ + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + if (context.RejectItem() != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); +} + +void ax::NodeEditor::EndCreate() +{ + auto& context = s_Editor->GetItemCreator(); + + context.End(); +} + +bool ax::NodeEditor::BeginDelete() +{ + auto& context = s_Editor->GetItemDeleter(); + + return context.Begin(); +} + +bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, PinId* endId) +{ + auto& context = s_Editor->GetItemDeleter(); + + return context.QueryLink(linkId, startId, endId); +} + +bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId) +{ + auto& context = s_Editor->GetItemDeleter(); + + return context.QueryNode(nodeId); +} + +bool ax::NodeEditor::AcceptDeletedItem() +{ + auto& context = s_Editor->GetItemDeleter(); + + return context.AcceptItem(); +} + +void ax::NodeEditor::RejectDeletedItem() +{ + auto& context = s_Editor->GetItemDeleter(); + + context.RejectItem(); +} + +void ax::NodeEditor::EndDelete() +{ + auto& context = s_Editor->GetItemDeleter(); + + context.End(); +} + +void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position) +{ + s_Editor->SetNodePosition(nodeId, position); +} + +ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId) +{ + return s_Editor->GetNodePosition(nodeId); +} + +ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId) +{ + return s_Editor->GetNodeSize(nodeId); +} + +void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId) +{ + if (auto node = s_Editor->FindNode(nodeId)) + node->CenterOnScreenInNextFrame(); +} + +void ax::NodeEditor::RestoreNodeState(NodeId nodeId) +{ + if (auto node = s_Editor->FindNode(nodeId)) + s_Editor->MarkNodeToRestoreState(node); +} + +void ax::NodeEditor::Suspend() +{ + s_Editor->Suspend(); +} + +void ax::NodeEditor::Resume() +{ + s_Editor->Resume(); +} + +bool ax::NodeEditor::IsSuspended() +{ + return s_Editor->IsSuspended(); +} + +bool ax::NodeEditor::IsActive() +{ + return s_Editor->IsActive(); +} + +bool ax::NodeEditor::HasSelectionChanged() +{ + return s_Editor->HasSelectionChanged(); +} + +int ax::NodeEditor::GetSelectedObjectCount() +{ + return (int)s_Editor->GetSelectedObjects().size(); +} + +int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size) +{ + return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object) + { + return object->AsNode() != nullptr; + }); +} + +int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size) +{ + return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object) + { + return object->AsLink() != nullptr; + }); +} + +void ax::NodeEditor::ClearSelection() +{ + s_Editor->ClearSelection(); +} + +void ax::NodeEditor::SelectNode(NodeId nodeId, bool append) +{ + if (auto node = s_Editor->FindNode(nodeId)) + { + if (append) + s_Editor->SelectObject(node); + else + s_Editor->SetSelectedObject(node); + } +} + +void ax::NodeEditor::SelectLink(LinkId linkId, bool append) +{ + if (auto link = s_Editor->FindLink(linkId)) + { + if (append) + s_Editor->SelectObject(link); + else + s_Editor->SetSelectedObject(link); + } +} + +void ax::NodeEditor::DeselectNode(NodeId nodeId) +{ + if (auto node = s_Editor->FindNode(nodeId)) + s_Editor->DeselectObject(node); +} + +void ax::NodeEditor::DeselectLink(LinkId linkId) +{ + if (auto link = s_Editor->FindLink(linkId)) + s_Editor->DeselectObject(link); +} + +bool ax::NodeEditor::DeleteNode(NodeId nodeId) +{ + if (auto node = s_Editor->FindNode(nodeId)) + return s_Editor->GetItemDeleter().Add(node); + else + return false; +} + +bool ax::NodeEditor::DeleteLink(LinkId linkId) +{ + if (auto link = s_Editor->FindLink(linkId)) + return s_Editor->GetItemDeleter().Add(link); + else + return false; +} + +void ax::NodeEditor::NavigateToContent(float duration) +{ + s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration); +} + +void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration) +{ + s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration); +} + +bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId) +{ + return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId); +} + +bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId) +{ + return s_Editor->GetContextMenu().ShowPinContextMenu(pinId); +} + +bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId) +{ + return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId); +} + +bool ax::NodeEditor::ShowBackgroundContextMenu() +{ + return s_Editor->GetContextMenu().ShowBackgroundContextMenu(); +} + +void ax::NodeEditor::EnableShortcuts(bool enable) +{ + s_Editor->EnableShortcuts(enable); +} + +bool ax::NodeEditor::AreShortcutsEnabled() +{ + return s_Editor->AreShortcutsEnabled(); +} + +bool ax::NodeEditor::BeginShortcut() +{ + return s_Editor->GetShortcut().Begin(); +} + +bool ax::NodeEditor::AcceptCut() +{ + return s_Editor->GetShortcut().AcceptCut(); +} + +bool ax::NodeEditor::AcceptCopy() +{ + return s_Editor->GetShortcut().AcceptCopy(); +} + +bool ax::NodeEditor::AcceptPaste() +{ + return s_Editor->GetShortcut().AcceptPaste(); +} + +bool ax::NodeEditor::AcceptDuplicate() +{ + return s_Editor->GetShortcut().AcceptDuplicate(); +} + +bool ax::NodeEditor::AcceptCreateNode() +{ + return s_Editor->GetShortcut().AcceptCreateNode(); +} + +int ax::NodeEditor::GetActionContextSize() +{ + return static_cast(s_Editor->GetShortcut().m_Context.size()); +} + +int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size) +{ + return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object) + { + return object->AsNode() != nullptr; + }); +} + +int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size) +{ + return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object) + { + return object->AsLink() != nullptr; + }); +} + +void ax::NodeEditor::EndShortcut() +{ + return s_Editor->GetShortcut().End(); +} + +float ax::NodeEditor::GetCurrentZoom() +{ + return s_Editor->GetView().InvScale; +} + +ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode() +{ + return s_Editor->GetDoubleClickedNode(); +} + +ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin() +{ + return s_Editor->GetDoubleClickedPin(); +} + +ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink() +{ + return s_Editor->GetDoubleClickedLink(); +} + +bool ax::NodeEditor::IsBackgroundClicked() +{ + return s_Editor->IsBackgroundClicked(); +} + +bool ax::NodeEditor::IsBackgroundDoubleClicked() +{ + return s_Editor->IsBackgroundDoubleClicked(); +} + +bool ax::NodeEditor::PinHadAnyLinks(PinId pinId) +{ + return s_Editor->PinHadAnyLinks(pinId); +} + +ImVec2 ax::NodeEditor::GetScreenSize() +{ + return s_Editor->GetRect().GetSize(); +} + +ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos) +{ + return s_Editor->ToCanvas(pos); +} + +ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos) +{ + return s_Editor->ToScreen(pos); +} diff --git a/src/ui/NodeEditor/Source/imgui_node_editor_internal.h b/src/ui/NodeEditor/Source/imgui_node_editor_internal.h new file mode 100644 index 0000000..f88ccb9 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_node_editor_internal.h @@ -0,0 +1,1496 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_NODE_EDITOR_INTERNAL_H__ +# define __IMGUI_NODE_EDITOR_INTERNAL_H__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_node_editor.h" + + +//------------------------------------------------------------------------------ +# include +# define IMGUI_DEFINE_MATH_OPERATORS +# include +# include "imgui_extra_math.h" +# include "imgui_bezier_math.h" +# include "imgui_canvas.h" + +# include "crude_json.h" + +# include +# include + + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { +namespace Detail { + + +//------------------------------------------------------------------------------ +namespace ed = ax::NodeEditor::Detail; +namespace json = crude_json; + + +//------------------------------------------------------------------------------ +using std::vector; +using std::string; + + +//------------------------------------------------------------------------------ +void Log(const char* fmt, ...); + + +//------------------------------------------------------------------------------ +//inline ImRect ToRect(const ax::rectf& rect); +//inline ImRect ToRect(const ax::rect& rect); +inline ImRect ImGui_GetItemRect(); + + +//------------------------------------------------------------------------------ +// https://stackoverflow.com/a/36079786 +# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ + \ + template \ + class __trait_name__ \ + { \ + using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ + struct no_type {char x[2];}; \ + using yes_type = char; \ + \ + struct base { void __member_name__() {}}; \ + struct mixin : public base, public check_type {}; \ + \ + template struct aux {}; \ + \ + template static no_type test(aux<&U::__member_name__>*); \ + template static yes_type test(...); \ + \ + public: \ + \ + static constexpr bool value = (sizeof(yes_type) == sizeof(test(0))); \ + } + +DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); + +# undef DECLARE_HAS_MEMBER + +struct FringeScaleRef +{ + // Overload is present when ImDrawList does have _FringeScale member variable. + template + static float& Get(typename std::enable_if::value, T>::type* drawList) + { + return drawList->_FringeScale; + } + + // Overload is present when ImDrawList does not have _FringeScale member variable. + template + static float& Get(typename std::enable_if::value, T>::type*) + { + static float placeholder = 1.0f; + return placeholder; + } +}; + +static inline float& ImFringeScaleRef(ImDrawList* drawList) +{ + return FringeScaleRef::Get(drawList); +} + +struct FringeScaleScope +{ + + FringeScaleScope(float scale) + : m_LastFringeScale(ImFringeScaleRef(ImGui::GetWindowDrawList())) + { + ImFringeScaleRef(ImGui::GetWindowDrawList()) = scale; + } + + ~FringeScaleScope() + { + ImFringeScaleRef(ImGui::GetWindowDrawList()) = m_LastFringeScale; + } + +private: + float m_LastFringeScale; +}; + + +//------------------------------------------------------------------------------ +enum class ObjectType +{ + None, + Node, + Link, + Pin +}; + +using ax::NodeEditor::PinKind; +using ax::NodeEditor::StyleColor; +using ax::NodeEditor::StyleVar; +using ax::NodeEditor::SaveReasonFlags; + +using ax::NodeEditor::NodeId; +using ax::NodeEditor::PinId; +using ax::NodeEditor::LinkId; + +struct ObjectId final: Details::SafePointerType +{ + using Super = Details::SafePointerType; + using Super::Super; + + ObjectId(): Super(Invalid), m_Type(ObjectType::None) {} + ObjectId(PinId pinId): Super(pinId.AsPointer()), m_Type(ObjectType::Pin) {} + ObjectId(NodeId nodeId): Super(nodeId.AsPointer()), m_Type(ObjectType::Node) {} + ObjectId(LinkId linkId): Super(linkId.AsPointer()), m_Type(ObjectType::Link) {} + + explicit operator PinId() const { return AsPinId(); } + explicit operator NodeId() const { return AsNodeId(); } + explicit operator LinkId() const { return AsLinkId(); } + + PinId AsPinId() const { IM_ASSERT(IsPinId()); return PinId(AsPointer()); } + NodeId AsNodeId() const { IM_ASSERT(IsNodeId()); return NodeId(AsPointer()); } + LinkId AsLinkId() const { IM_ASSERT(IsLinkId()); return LinkId(AsPointer()); } + + bool IsPinId() const { return m_Type == ObjectType::Pin; } + bool IsNodeId() const { return m_Type == ObjectType::Node; } + bool IsLinkId() const { return m_Type == ObjectType::Link; } + + ObjectType Type() const { return m_Type; } + +private: + ObjectType m_Type; +}; + +struct EditorContext; + +struct Node; +struct Pin; +struct Link; + +template +struct ObjectWrapper +{ + Id m_ID; + T* m_Object; + + T* operator->() { return m_Object; } + const T* operator->() const { return m_Object; } + + operator T*() { return m_Object; } + operator const T*() const { return m_Object; } + + bool operator<(const ObjectWrapper& rhs) const + { + return m_ID.AsPointer() < rhs.m_ID.AsPointer(); + } +}; + +struct Object +{ + enum DrawFlags + { + None = 0, + Hovered = 1, + Selected = 2 + }; + + inline friend DrawFlags operator|(DrawFlags lhs, DrawFlags rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } + inline friend DrawFlags operator&(DrawFlags lhs, DrawFlags rhs) { return static_cast(static_cast(lhs) & static_cast(rhs)); } + inline friend DrawFlags& operator|=(DrawFlags& lhs, DrawFlags rhs) { lhs = lhs | rhs; return lhs; } + inline friend DrawFlags& operator&=(DrawFlags& lhs, DrawFlags rhs) { lhs = lhs & rhs; return lhs; } + + EditorContext* const Editor; + + bool m_IsLive; + + Object(EditorContext* editor) + : Editor(editor) + , m_IsLive(true) + { + } + + virtual ~Object() = default; + + virtual ObjectId ID() = 0; + + bool IsVisible() const + { + if (!m_IsLive) + return false; + + const auto bounds = GetBounds(); + + return ImGui::IsRectVisible(bounds.Min, bounds.Max); + } + + virtual void Reset() { m_IsLive = false; } + + virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) = 0; + + virtual bool AcceptDrag() { return false; } + virtual void UpdateDrag(const ImVec2& offset) { } + virtual bool EndDrag() { return false; } + virtual ImVec2 DragStartLocation() { return GetBounds().Min; } + + virtual bool IsDraggable() { bool result = AcceptDrag(); EndDrag(); return result; } + virtual bool IsSelectable() { return false; } + + virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const + { + if (!m_IsLive) + return false; + + auto bounds = GetBounds(); + if (extraThickness > 0) + bounds.Expand(extraThickness); + + return bounds.Contains(point); + } + + virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const + { + if (!m_IsLive) + return false; + + const auto bounds = GetBounds(); + + return !ImRect_IsEmpty(bounds) && (allowIntersect ? bounds.Overlaps(rect) : rect.Contains(bounds)); + } + + virtual ImRect GetBounds() const = 0; + + virtual Node* AsNode() { return nullptr; } + virtual Pin* AsPin() { return nullptr; } + virtual Link* AsLink() { return nullptr; } +}; + +struct Pin final: Object +{ + using IdType = PinId; + + PinId m_ID; + PinKind m_Kind; + Node* m_Node; + ImRect m_Bounds; + ImRect m_Pivot; + Pin* m_PreviousPin; + ImU32 m_Color; + ImU32 m_BorderColor; + float m_BorderWidth; + float m_Rounding; + int m_Corners; + ImVec2 m_Dir; + float m_Strength; + float m_Radius; + float m_ArrowSize; + float m_ArrowWidth; + bool m_HasConnection; + bool m_HadConnection; + + Pin(EditorContext* editor, PinId id, PinKind kind) + : Object(editor) + , m_ID(id) + , m_Kind(kind) + , m_Node(nullptr) + , m_Bounds() + , m_PreviousPin(nullptr) + , m_Color(IM_COL32_WHITE) + , m_BorderColor(IM_COL32_BLACK) + , m_BorderWidth(0) + , m_Rounding(0) + , m_Corners(0) + , m_Dir(0, 0) + , m_Strength(0) + , m_Radius(0) + , m_ArrowSize(0) + , m_ArrowWidth(0) + , m_HasConnection(false) + , m_HadConnection(false) + { + } + + virtual ObjectId ID() override { return m_ID; } + + virtual void Reset() override final + { + m_HadConnection = m_HasConnection && m_IsLive; + m_HasConnection = false; + + Object::Reset(); + } + + virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final; + + ImVec2 GetClosestPoint(const ImVec2& p) const; + ImLine GetClosestLine(const Pin* pin) const; + + virtual ImRect GetBounds() const override final { return m_Bounds; } + + virtual Pin* AsPin() override final { return this; } +}; + +enum class NodeType +{ + Node, + Group +}; + +enum class NodeRegion : uint8_t +{ + None = 0x00, + Top = 0x01, + Bottom = 0x02, + Left = 0x04, + Right = 0x08, + Center = 0x10, + Header = 0x20, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, +}; + +inline NodeRegion operator |(NodeRegion lhs, NodeRegion rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } +inline NodeRegion operator &(NodeRegion lhs, NodeRegion rhs) { return static_cast(static_cast(lhs) & static_cast(rhs)); } + + +struct Node final: Object +{ + using IdType = NodeId; + + NodeId m_ID; + NodeType m_Type; + ImRect m_Bounds; + int m_Channel; + Pin* m_LastPin; + ImVec2 m_DragStart; + + ImU32 m_Color; + ImU32 m_BorderColor; + float m_BorderWidth; + float m_Rounding; + + ImU32 m_GroupColor; + ImU32 m_GroupBorderColor; + float m_GroupBorderWidth; + float m_GroupRounding; + ImRect m_GroupBounds; + + bool m_RestoreState; + bool m_CenterOnScreen; + + Node(EditorContext* editor, NodeId id) + : Object(editor) + , m_ID(id) + , m_Type(NodeType::Node) + , m_Bounds() + , m_Channel(0) + , m_LastPin(nullptr) + , m_DragStart() + , m_Color(IM_COL32_WHITE) + , m_BorderColor(IM_COL32_BLACK) + , m_BorderWidth(0) + , m_Rounding(0) + , m_GroupBounds() + , m_RestoreState(false) + , m_CenterOnScreen(false) + { + } + + virtual ObjectId ID() override { return m_ID; } + + bool AcceptDrag() override; + void UpdateDrag(const ImVec2& offset) override; + bool EndDrag() override; // return true, when changed + ImVec2 DragStartLocation() override { return m_DragStart; } + + virtual bool IsSelectable() override { return true; } + + virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final; + void DrawBorder(ImDrawList* drawList, ImU32 color, float thickness = 1.0f); + + void GetGroupedNodes(std::vector& result, bool append = false); + + void CenterOnScreenInNextFrame() { m_CenterOnScreen = true; } + + ImRect GetRegionBounds(NodeRegion region) const; + NodeRegion GetRegion(const ImVec2& point) const; + + virtual ImRect GetBounds() const override final { return m_Bounds; } + + virtual Node* AsNode() override final { return this; } +}; + +struct Link final: Object +{ + using IdType = LinkId; + + LinkId m_ID; + Pin* m_StartPin; + Pin* m_EndPin; + ImU32 m_Color; + float m_Thickness; + ImVec2 m_Start; + ImVec2 m_End; + + Link(EditorContext* editor, LinkId id) + : Object(editor) + , m_ID(id) + , m_StartPin(nullptr) + , m_EndPin(nullptr) + , m_Color(IM_COL32_WHITE) + , m_Thickness(1.0f) + { + } + + virtual ObjectId ID() override { return m_ID; } + + virtual bool IsSelectable() override { return true; } + + virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final; + void Draw(ImDrawList* drawList, ImU32 color, float extraThickness = 0.0f) const; + + void UpdateEndpoints(); + + ImCubicBezierPoints GetCurve() const; + + virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const override final; + virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const override final; + + virtual ImRect GetBounds() const override final; + + virtual Link* AsLink() override final { return this; } +}; + +struct NodeSettings +{ + NodeId m_ID; + ImVec2 m_Location; + ImVec2 m_Size; + ImVec2 m_GroupSize; + bool m_WasUsed; + + bool m_Saved; + bool m_IsDirty; + SaveReasonFlags m_DirtyReason; + + NodeSettings(NodeId id) + : m_ID(id) + , m_Location(0, 0) + , m_Size(0, 0) + , m_GroupSize(0, 0) + , m_WasUsed(false) + , m_Saved(false) + , m_IsDirty(false) + , m_DirtyReason(SaveReasonFlags::None) + { + } + + void ClearDirty(); + void MakeDirty(SaveReasonFlags reason); + + json::value Serialize(); + + static bool Parse(const std::string& string, NodeSettings& settings); + static bool Parse(const json::value& data, NodeSettings& result); +}; + +struct Settings +{ + bool m_IsDirty; + SaveReasonFlags m_DirtyReason; + + vector m_Nodes; + vector m_Selection; + ImVec2 m_ViewScroll; + float m_ViewZoom; + + Settings() + : m_IsDirty(false) + , m_DirtyReason(SaveReasonFlags::None) + , m_ViewScroll(0, 0) + , m_ViewZoom(1.0f) + { + } + + NodeSettings* AddNode(NodeId id); + NodeSettings* FindNode(NodeId id); + + void ClearDirty(Node* node = nullptr); + void MakeDirty(SaveReasonFlags reason, Node* node = nullptr); + + std::string Serialize(); + + static bool Parse(const std::string& string, Settings& settings); +}; + +struct Control +{ + Object* HotObject; + Object* ActiveObject; + Object* ClickedObject; + Object* DoubleClickedObject; + Node* HotNode; + Node* ActiveNode; + Node* ClickedNode; + Node* DoubleClickedNode; + Pin* HotPin; + Pin* ActivePin; + Pin* ClickedPin; + Pin* DoubleClickedPin; + Link* HotLink; + Link* ActiveLink; + Link* ClickedLink; + Link* DoubleClickedLink; + bool BackgroundHot; + bool BackgroundActive; + bool BackgroundClicked; + bool BackgroundDoubleClicked; + + Control(Object* hotObject, Object* activeObject, Object* clickedObject, Object* doubleClickedObject, + bool backgroundHot, bool backgroundActive, bool backgroundClicked, bool backgroundDoubleClicked) + : HotObject(hotObject) + , ActiveObject(activeObject) + , ClickedObject(clickedObject) + , DoubleClickedObject(doubleClickedObject) + , HotNode(nullptr) + , ActiveNode(nullptr) + , ClickedNode(nullptr) + , DoubleClickedNode(nullptr) + , HotPin(nullptr) + , ActivePin(nullptr) + , ClickedPin(nullptr) + , DoubleClickedPin(nullptr) + , HotLink(nullptr) + , ActiveLink(nullptr) + , ClickedLink(nullptr) + , DoubleClickedLink(nullptr) + , BackgroundHot(backgroundHot) + , BackgroundActive(backgroundActive) + , BackgroundClicked(backgroundClicked) + , BackgroundDoubleClicked(backgroundDoubleClicked) + { + if (hotObject) + { + HotNode = hotObject->AsNode(); + HotPin = hotObject->AsPin(); + HotLink = hotObject->AsLink(); + + if (HotPin) + HotNode = HotPin->m_Node; + } + + if (activeObject) + { + ActiveNode = activeObject->AsNode(); + ActivePin = activeObject->AsPin(); + ActiveLink = activeObject->AsLink(); + } + + if (clickedObject) + { + ClickedNode = clickedObject->AsNode(); + ClickedPin = clickedObject->AsPin(); + ClickedLink = clickedObject->AsLink(); + } + + if (doubleClickedObject) + { + DoubleClickedNode = doubleClickedObject->AsNode(); + DoubleClickedPin = doubleClickedObject->AsPin(); + DoubleClickedLink = doubleClickedObject->AsLink(); + } + } +}; + +struct NavigateAction; +struct SizeAction; +struct DragAction; +struct SelectAction; +struct CreateItemAction; +struct DeleteItemsAction; +struct ContextMenuAction; +struct ShortcutAction; + +struct AnimationController; +struct FlowAnimationController; + +struct Animation +{ + enum State + { + Playing, + Stopped + }; + + EditorContext* Editor; + State m_State; + float m_Time; + float m_Duration; + + Animation(EditorContext* editor); + virtual ~Animation(); + + void Play(float duration); + void Stop(); + void Finish(); + void Update(); + + bool IsPlaying() const { return m_State == Playing; } + + float GetProgress() const { return m_Time / m_Duration; } + +protected: + virtual void OnPlay() {} + virtual void OnFinish() {} + virtual void OnStop() {} + + virtual void OnUpdate(float progress) {} +}; + +struct NavigateAnimation final: Animation +{ + NavigateAction& Action; + ImRect m_Start; + ImRect m_Target; + + NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction); + + void NavigateTo(const ImRect& target, float duration); + +private: + void OnUpdate(float progress) override final; + void OnStop() override final; + void OnFinish() override final; +}; + +struct FlowAnimation final: Animation +{ + FlowAnimationController* Controller; + Link* m_Link; + float m_Speed; + float m_MarkerDistance; + float m_Offset; + + FlowAnimation(FlowAnimationController* controller); + + void Flow(Link* link, float markerDistance, float speed, float duration); + + void Draw(ImDrawList* drawList); + +private: + struct CurvePoint + { + float Distance; + ImVec2 Point; + }; + + ImVec2 m_LastStart; + ImVec2 m_LastEnd; + float m_PathLength; + vector m_Path; + + bool IsLinkValid() const; + bool IsPathValid() const; + void UpdatePath(); + void ClearPath(); + + ImVec2 SamplePath(float distance); + + void OnUpdate(float progress) override final; + void OnStop() override final; +}; + +struct AnimationController +{ + EditorContext* Editor; + + AnimationController(EditorContext* editor) + : Editor(editor) + { + } + + virtual ~AnimationController() + { + } + + virtual void Draw(ImDrawList* drawList) + { + } +}; + +struct FlowAnimationController final : AnimationController +{ + FlowAnimationController(EditorContext* editor); + virtual ~FlowAnimationController(); + + void Flow(Link* link); + + virtual void Draw(ImDrawList* drawList) override final; + + void Release(FlowAnimation* animation); + +private: + FlowAnimation* GetOrCreate(Link* link); + + vector m_Animations; + vector m_FreePool; +}; + +struct EditorAction +{ + enum AcceptResult { False, True, Possible }; + + EditorAction(EditorContext* editor) + : Editor(editor) + { + } + + virtual ~EditorAction() {} + + virtual const char* GetName() const = 0; + + virtual AcceptResult Accept(const Control& control) = 0; + virtual bool Process(const Control& control) = 0; + virtual void Reject() {} // celled when Accept return 'Possible' and was rejected + + virtual ImGuiMouseCursor GetCursor() { return ImGuiMouseCursor_Arrow; } + + virtual bool IsDragging() { return false; } + + virtual void ShowMetrics() {} + + virtual NavigateAction* AsNavigate() { return nullptr; } + virtual SizeAction* AsSize() { return nullptr; } + virtual DragAction* AsDrag() { return nullptr; } + virtual SelectAction* AsSelect() { return nullptr; } + virtual CreateItemAction* AsCreateItem() { return nullptr; } + virtual DeleteItemsAction* AsDeleteItems() { return nullptr; } + virtual ContextMenuAction* AsContextMenu() { return nullptr; } + virtual ShortcutAction* AsCutCopyPaste() { return nullptr; } + + EditorContext* Editor; +}; + +struct NavigateAction final: EditorAction +{ + enum class NavigationReason + { + Unknown, + MouseZoom, + Selection, + Object, + Content, + Edge + }; + + bool m_IsActive; + float m_Zoom; + ImVec2 m_Scroll; + ImVec2 m_ScrollStart; + ImVec2 m_ScrollDelta; + + NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas); + + virtual const char* GetName() const override final { return "Navigate"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual NavigateAction* AsNavigate() override final { return this; } + + void NavigateTo(const ImRect& bounds, bool zoomIn, float duration = -1.0f, NavigationReason reason = NavigationReason::Unknown); + void StopNavigation(); + void FinishNavigation(); + + bool MoveOverEdge(); + void StopMoveOverEdge(); + bool IsMovingOverEdge() const { return m_MovingOverEdge; } + ImVec2 GetMoveOffset() const { return m_MoveOffset; } + + void SetWindow(ImVec2 position, ImVec2 size); + + ImGuiEx::CanvasView GetView() const; + ImVec2 GetViewOrigin() const; + float GetViewScale() const; + + void SetViewRect(const ImRect& rect); + ImRect GetViewRect() const; + +private: + ImGuiEx::Canvas& m_Canvas; + ImVec2 m_WindowScreenPos; + ImVec2 m_WindowScreenSize; + + NavigateAnimation m_Animation; + NavigationReason m_Reason; + uint64_t m_LastSelectionId; + Object* m_LastObject; + bool m_MovingOverEdge; + ImVec2 m_MoveOffset; + + bool HandleZoom(const Control& control); + + void NavigateTo(const ImRect& target, float duration = -1.0f, NavigationReason reason = NavigationReason::Unknown); + + float MatchZoom(int steps, float fallbackZoom); + int MatchZoomIndex(int direction); + + static const float s_ZoomLevels[]; + static const int s_ZoomLevelCount; +}; + +struct SizeAction final: EditorAction +{ + bool m_IsActive; + bool m_Clean; + Node* m_SizedNode; + + SizeAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Size"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { return m_Cursor; } + + virtual void ShowMetrics() override final; + + virtual SizeAction* AsSize() override final { return this; } + + virtual bool IsDragging() override final { return m_IsActive; } + + const ImRect& GetStartGroupBounds() const { return m_StartGroupBounds; } + +private: + NodeRegion GetRegion(Node* node); + ImGuiMouseCursor ChooseCursor(NodeRegion region); + + ImRect m_StartBounds; + ImRect m_StartGroupBounds; + ImVec2 m_LastSize; + ImVec2 m_MinimumSize; + ImVec2 m_LastDragOffset; + ed::NodeRegion m_Pivot; + ImGuiMouseCursor m_Cursor; +}; + +struct DragAction final: EditorAction +{ + bool m_IsActive; + bool m_Clear; + Object* m_DraggedObject; + vector m_Objects; + + DragAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Drag"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { return ImGuiMouseCursor_ResizeAll; } + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual void ShowMetrics() override final; + + virtual DragAction* AsDrag() override final { return this; } +}; + +struct SelectAction final: EditorAction +{ + bool m_IsActive; + + bool m_SelectGroups; + bool m_SelectLinkMode; + bool m_CommitSelection; + ImVec2 m_StartPoint; + ImVec2 m_EndPoint; + vector m_CandidateObjects; + vector m_SelectedObjectsAtStart; + + Animation m_Animation; + + SelectAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Select"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual SelectAction* AsSelect() override final { return this; } + + void Draw(ImDrawList* drawList); +}; + +struct ContextMenuAction final: EditorAction +{ + enum Menu { None, Node, Pin, Link, Background }; + + Menu m_CandidateMenu; + Menu m_CurrentMenu; + ObjectId m_ContextId; + + ContextMenuAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Context Menu"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + virtual void Reject() override final; + + virtual void ShowMetrics() override final; + + virtual ContextMenuAction* AsContextMenu() override final { return this; } + + bool ShowNodeContextMenu(NodeId* nodeId); + bool ShowPinContextMenu(PinId* pinId); + bool ShowLinkContextMenu(LinkId* linkId); + bool ShowBackgroundContextMenu(); +}; + +struct ShortcutAction final: EditorAction +{ + enum Action { None, Cut, Copy, Paste, Duplicate, CreateNode }; + + bool m_IsActive; + bool m_InAction; + Action m_CurrentAction; + vector m_Context; + + ShortcutAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Shortcut"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + virtual void Reject() override final; + + virtual void ShowMetrics() override final; + + virtual ShortcutAction* AsCutCopyPaste() override final { return this; } + + bool Begin(); + void End(); + + bool AcceptCut(); + bool AcceptCopy(); + bool AcceptPaste(); + bool AcceptDuplicate(); + bool AcceptCreateNode(); +}; + +struct CreateItemAction final : EditorAction +{ + enum Stage + { + None, + Possible, + Create + }; + + enum Action + { + Unknown, + UserReject, + UserAccept + }; + + enum Type + { + NoItem, + Node, + Link + }; + + enum Result + { + True, + False, + Indeterminate + }; + + bool m_InActive; + Stage m_NextStage; + + Stage m_CurrentStage; + Type m_ItemType; + Action m_UserAction; + ImU32 m_LinkColor; + float m_LinkThickness; + Pin* m_LinkStart; + Pin* m_LinkEnd; + + bool m_IsActive; + Pin* m_DraggedPin; + + int m_LastChannel = -1; + + + CreateItemAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Create Item"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { return ImGuiMouseCursor_Arrow; } + + virtual void ShowMetrics() override final; + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual CreateItemAction* AsCreateItem() override final { return this; } + + void SetStyle(ImU32 color, float thickness); + + bool Begin(); + void End(); + + Result RejectItem(); + Result AcceptItem(); + + Result QueryLink(PinId* startId, PinId* endId); + Result QueryNode(PinId* pinId); + +private: + bool m_IsInGlobalSpace; + + void DragStart(Pin* startPin); + void DragEnd(); + void DropPin(Pin* endPin); + void DropNode(); + void DropNothing(); +}; + +struct DeleteItemsAction final: EditorAction +{ + bool m_IsActive; + bool m_InInteraction; + + DeleteItemsAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Delete Items"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual DeleteItemsAction* AsDeleteItems() override final { return this; } + + bool Add(Object* object); + + bool Begin(); + void End(); + + bool QueryLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr); + bool QueryNode(NodeId* nodeId); + + bool AcceptItem(); + void RejectItem(); + +private: + enum IteratorType { Unknown, Link, Node }; + enum UserAction { Undetermined, Accepted, Rejected }; + + bool QueryItem(ObjectId* itemId, IteratorType itemType); + void RemoveItem(); + + vector m_ManuallyDeletedObjects; + + IteratorType m_CurrentItemType; + UserAction m_UserAction; + vector m_CandidateObjects; + int m_CandidateItemIndex; +}; + +struct NodeBuilder +{ + EditorContext* const Editor; + + Node* m_CurrentNode; + Pin* m_CurrentPin; + + ImRect m_NodeRect; + + ImRect m_PivotRect; + ImVec2 m_PivotAlignment; + ImVec2 m_PivotSize; + ImVec2 m_PivotScale; + bool m_ResolvePinRect; + bool m_ResolvePivot; + + ImRect m_GroupBounds; + bool m_IsGroup; + + NodeBuilder(EditorContext* editor); + + void Begin(NodeId nodeId); + void End(); + + void BeginPin(PinId pinId, PinKind kind); + void EndPin(); + + void PinRect(const ImVec2& a, const ImVec2& b); + void PinPivotRect(const ImVec2& a, const ImVec2& b); + void PinPivotSize(const ImVec2& size); + void PinPivotScale(const ImVec2& scale); + void PinPivotAlignment(const ImVec2& alignment); + + void Group(const ImVec2& size); + + ImDrawList* GetUserBackgroundDrawList() const; + ImDrawList* GetUserBackgroundDrawList(Node* node) const; +}; + +struct HintBuilder +{ + EditorContext* const Editor; + bool m_IsActive; + Node* m_CurrentNode; + float m_LastFringe = 1.0f; + int m_LastChannel = 0; + + HintBuilder(EditorContext* editor); + + bool Begin(NodeId nodeId); + void End(); + + ImVec2 GetGroupMin(); + ImVec2 GetGroupMax(); + + ImDrawList* GetForegroundDrawList(); + ImDrawList* GetBackgroundDrawList(); +}; + +struct Style: ax::NodeEditor::Style +{ + void PushColor(StyleColor colorIndex, const ImVec4& color); + void PopColor(int count = 1); + + void PushVar(StyleVar varIndex, float value); + void PushVar(StyleVar varIndex, const ImVec2& value); + void PushVar(StyleVar varIndex, const ImVec4& value); + void PopVar(int count = 1); + + const char* GetColorName(StyleColor colorIndex) const; + +private: + struct ColorModifier + { + StyleColor Index; + ImVec4 Value; + }; + + struct VarModifier + { + StyleVar Index; + ImVec4 Value; + }; + + float* GetVarFloatAddr(StyleVar idx); + ImVec2* GetVarVec2Addr(StyleVar idx); + ImVec4* GetVarVec4Addr(StyleVar idx); + + vector m_ColorStack; + vector m_VarStack; +}; + +struct Config: ax::NodeEditor::Config +{ + Config(const ax::NodeEditor::Config* config); + + std::string Load(); + std::string LoadNode(NodeId nodeId); + + void BeginSave(); + bool Save(const std::string& data, SaveReasonFlags flags); + bool SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags); + void EndSave(); +}; + +struct EditorContext +{ + EditorContext(const ax::NodeEditor::Config* config = nullptr); + ~EditorContext(); + + Style& GetStyle() { return m_Style; } + + void Begin(const char* id, const ImVec2& size = ImVec2(0, 0)); + void End(); + + bool DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness); + + + NodeBuilder& GetNodeBuilder() { return m_NodeBuilder; } + HintBuilder& GetHintBuilder() { return m_HintBuilder; } + + EditorAction* GetCurrentAction() { return m_CurrentAction; } + + CreateItemAction& GetItemCreator() { return m_CreateItemAction; } + DeleteItemsAction& GetItemDeleter() { return m_DeleteItemsAction; } + ContextMenuAction& GetContextMenu() { return m_ContextMenuAction; } + ShortcutAction& GetShortcut() { return m_ShortcutAction; } + + const ImGuiEx::CanvasView& GetView() const { return m_Canvas.View(); } + const ImRect& GetViewRect() const { return m_Canvas.ViewRect(); } + const ImRect& GetRect() const { return m_Canvas.Rect(); } + + void SetNodePosition(NodeId nodeId, const ImVec2& screenPosition); + ImVec2 GetNodePosition(NodeId nodeId); + ImVec2 GetNodeSize(NodeId nodeId); + + void MarkNodeToRestoreState(Node* node); + void RestoreNodeState(Node* node); + + void ClearSelection(); + void SelectObject(Object* object); + void DeselectObject(Object* object); + void SetSelectedObject(Object* object); + void ToggleObjectSelection(Object* object); + bool IsSelected(Object* object); + const vector& GetSelectedObjects(); + bool IsAnyNodeSelected(); + bool IsAnyLinkSelected(); + bool HasSelectionChanged(); + uint64_t GetSelectionId() const { return m_SelectionId; } + + Node* FindNodeAt(const ImVec2& p); + // UsdLayerManager local patch: geometric pin hit-test, the pin analogue of + // FindNodeAt — used to resolve HotPin/ActivePin by pin bounds when ImGui's + // per-item hover/active attribution fails for a pin in this embedding + // (same failure family that breaks node click/hover here). Only live pins + // are considered; caller must confine the query to in-canvas positions. + Pin* FindPinAt(const ImVec2& p); + void FindNodesInRect(const ImRect& r, vector& result, bool append = false, bool includeIntersecting = true); + void FindLinksInRect(const ImRect& r, vector& result, bool append = false); + + void FindLinksForNode(NodeId nodeId, vector& result, bool add = false); + + bool PinHadAnyLinks(PinId pinId); + + ImVec2 ToCanvas(const ImVec2& point) const { return m_Canvas.ToLocal(point); } + ImVec2 ToScreen(const ImVec2& point) const { return m_Canvas.FromLocal(point); } + + void NotifyLinkDeleted(Link* link); + + void Suspend(); + void Resume(); + bool IsSuspended(); + + bool IsActive(); + + void MakeDirty(SaveReasonFlags reason); + void MakeDirty(SaveReasonFlags reason, Node* node); + + Pin* CreatePin(PinId id, PinKind kind); + Node* CreateNode(NodeId id); + Link* CreateLink(LinkId id); + + Node* FindNode(NodeId id); + Pin* FindPin(PinId id); + Link* FindLink(LinkId id); + Object* FindObject(ObjectId id); + + Node* GetNode(NodeId id); + Pin* GetPin(PinId id, PinKind kind); + Link* GetLink(LinkId id); + + Link* FindLinkAt(const ImVec2& p); + + template + ImRect GetBounds(const std::vector& objects) + { + ImRect bounds; + + for (auto object : objects) + if (object->m_IsLive) + bounds.Add(object->GetBounds()); + + return bounds; + } + + template + ImRect GetBounds(const std::vector>& objects) + { + ImRect bounds; + + for (auto object : objects) + if (object.m_Object->m_IsLive) + bounds.Add(object.m_Object->GetBounds()); + + return bounds; + } + + ImRect GetSelectionBounds() { return GetBounds(m_SelectedObjects); } + ImRect GetContentBounds() { return GetBounds(m_Nodes); } + + ImU32 GetColor(StyleColor colorIndex) const; + ImU32 GetColor(StyleColor colorIndex, float alpha) const; + + void NavigateTo(const ImRect& bounds, bool zoomIn = false, float duration = -1) { m_NavigateAction.NavigateTo(bounds, zoomIn, duration); } + + void RegisterAnimation(Animation* animation); + void UnregisterAnimation(Animation* animation); + + void Flow(Link* link); + + void SetUserContext(bool globalSpace = false); + + void EnableShortcuts(bool enable); + bool AreShortcutsEnabled(); + + NodeId GetDoubleClickedNode() const { return m_DoubleClickedNode; } + PinId GetDoubleClickedPin() const { return m_DoubleClickedPin; } + LinkId GetDoubleClickedLink() const { return m_DoubleClickedLink; } + bool IsBackgroundClicked() const { return m_BackgroundClicked; } + bool IsBackgroundDoubleClicked() const { return m_BackgroundDoubleClicked; } + // UsdLayerManager local patch: see m_PressedOnNode above. + // UsdLayerManager local patch: see m_PressedNode above. + Node* GetPressedNode() const { return m_PressedNode; } + Pin* GetPressedPin() const { return m_PressedPin; } + + float AlignPointToGrid(float p) const + { + if (!ImGui::GetIO().KeyAlt) + return p - ImFmod(p, 16.0f); + else + return p; + } + + ImVec2 AlignPointToGrid(const ImVec2& p) const + { + return ImVec2(AlignPointToGrid(p.x), AlignPointToGrid(p.y)); + } + +private: + void LoadSettings(); + void SaveSettings(); + + Control BuildControl(bool allowOffscreen); + + void ShowMetrics(const Control& control); + + void UpdateAnimations(); + + bool m_IsFirstFrame; + bool m_IsWindowActive; + + bool m_ShortcutsEnabled; + + Style m_Style; + + vector> m_Nodes; + vector> m_Pins; + vector> m_Links; + + vector m_SelectedObjects; + + vector m_LastSelectedObjects; + uint64_t m_SelectionId; + + Link* m_LastActiveLink; + + // UsdLayerManager local patch: the non-Group node the mouse-down that + // started the current gesture geometrically landed inside (null if it + // landed on empty canvas or on a Group); computed once per press in + // BuildControl() via FindNodeAt(), independent of ImGui's own per-frame + // hover/active resolution. Two uses: + // - SelectAction::Accept() refuses to start a rect-select when this is + // set, so a press that geometrically hit a node can never be + // misattributed to the background and turn into a marquee. + // - DragAction::Accept() falls back to this node when control.ActiveObject + // doesn't resolve on a given frame, since some node/embedding + // configurations (observed in this project's nested-child-window + // panel layout) have ImGui's hover/active attribution persistently + // fail for a specific node's InvisibleButton every frame of a + // gesture — not just a transient one-frame race — so waiting for + // dispatch to "self-correct" next frame never happens for those. + Node* m_PressedNode; + + // UsdLayerManager local patch: the pin the mouse-down that started the + // current gesture geometrically landed on (null otherwise). Takes + // precedence over m_PressedNode (a pin sits on top of its node body, and + // pressing a pin must start a link, not move the node), and is used as the + // ActivePin fallback so CreateItemAction can begin a connection drag even + // when ImGui never marked the pin active. Same lifecycle as m_PressedNode. + Pin* m_PressedPin; + + // UsdLayerManager local patch: true if the mouse-down that started the + // current gesture landed anywhere inside the canvas's visible view rect + // (on a node or on empty canvas), false if it landed outside — e.g. on + // the host ImGui window's title bar. Gates MoveOverEdge() auto-scroll + // (see Begin()): dragging the whole editor window by its title moves the + // cursor "outside" the canvas every frame, which otherwise looks exactly + // like a node-drag-past-the-edge and pans the graph. Requiring the press + // to have originated in the canvas distinguishes a genuine in-canvas drag + // from a window move. + bool m_PressedInCanvas; + + vector m_LiveAnimations; + vector m_LastLiveAnimations; + + ImGuiEx::Canvas m_Canvas; + bool m_IsCanvasVisible; + + NodeBuilder m_NodeBuilder; + HintBuilder m_HintBuilder; + + EditorAction* m_CurrentAction; + NavigateAction m_NavigateAction; + SizeAction m_SizeAction; + DragAction m_DragAction; + SelectAction m_SelectAction; + ContextMenuAction m_ContextMenuAction; + ShortcutAction m_ShortcutAction; + CreateItemAction m_CreateItemAction; + DeleteItemsAction m_DeleteItemsAction; + + vector m_AnimationControllers; + FlowAnimationController m_FlowAnimationController; + + NodeId m_DoubleClickedNode; + PinId m_DoubleClickedPin; + LinkId m_DoubleClickedLink; + bool m_BackgroundClicked; + bool m_BackgroundDoubleClicked; + + bool m_IsInitialized; + Settings m_Settings; + + Config m_Config; +}; + + +//------------------------------------------------------------------------------ +} // namespace Detail +} // namespace Editor +} // namespace ax + + +//------------------------------------------------------------------------------ +# include "imgui_node_editor_internal.inl" + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_NODE_EDITOR_INTERNAL_H__ diff --git a/src/ui/NodeEditor/Source/imgui_node_editor_internal.inl b/src/ui/NodeEditor/Source/imgui_node_editor_internal.inl new file mode 100644 index 0000000..0d16231 --- /dev/null +++ b/src/ui/NodeEditor/Source/imgui_node_editor_internal.inl @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_NODE_EDITOR_INTERNAL_INL__ +# define __IMGUI_NODE_EDITOR_INTERNAL_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_node_editor_internal.h" + + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { +namespace Detail { + + +//------------------------------------------------------------------------------ +//inline ImRect ToRect(const ax::rectf& rect) +//{ +// return ImRect( +// to_imvec(rect.top_left()), +// to_imvec(rect.bottom_right()) +// ); +//} +// +//inline ImRect ToRect(const ax::rect& rect) +//{ +// return ImRect( +// to_imvec(rect.top_left()), +// to_imvec(rect.bottom_right()) +// ); +//} + +inline ImRect ImGui_GetItemRect() +{ + return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); +} + + +//------------------------------------------------------------------------------ +} // namespace Detail +} // namespace Editor +} // namespace ax + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__