nodehub 1/5 | ai transform 2/3

This commit is contained in:
lovebird 2026-04-15 21:44:50 +02:00
parent 3a1ac7ff00
commit 386b6171af
187 changed files with 37881 additions and 58 deletions

View File

@ -202,6 +202,7 @@ if(WIN32)
"${_UI_NEXT_DIR}/App.cpp"
"${_UI_NEXT_DIR}/DropView.cpp"
"${_UI_NEXT_DIR}/FileQueue.cpp"
"${_UI_NEXT_DIR}/LogPanel.cpp"
"${_UI_NEXT_DIR}/SettingsPanel.cpp"
"${_UI_NEXT_DIR}/Mainfrm.cpp"
"${_UI_NEXT_DIR}/launch_ui_next.cpp"

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,328 @@
# Set up vcpkg toolchain
if(DEFINED CMAKE_TOOLCHAIN_FILE AND EXISTS "${CMAKE_TOOLCHAIN_FILE}")
include(${CMAKE_TOOLCHAIN_FILE})
endif()
#------------------------------------------------------------------------------
# ImGui Node Editor examples build configuration.
# - `get_filename_component(... ABSOLUTE CACHE)` resolves the project root to
# an absolute path and stores it in CMake's cache so re-configures reuse it.
# - `USE_FOLDERS` enables logical IDE folders for generated projects.
# - `BUILD_CONSOLE_VARIANTS` toggles additional Win32 console builds.
# - `USE_CONSOLE_AS_STARTUP` selects which target Visual Studio launches.
# - `EXAMPLES_NO_WARNING_FLAGS` holds warning overrides (set to `/W0` on MSVC).
#------------------------------------------------------------------------------
cmake_minimum_required(VERSION 3.5)
# Root project that pulls in the node editor and builds the sample apps.
project(imgui-node-editor)
# Enable testing capabilities with CTest
enable_testing()
# Fetch Google Test framework
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# Enforce the same compiler settings for gtest as for our project
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# Option to enable/disable protobuf support (default: ON)
option(ENABLE_PROTOBUF "Enable Protocol Buffers support" ON)
# Protocol Buffers support (optional, via vcpkg)
if (ENABLE_PROTOBUF)
# Try CONFIG mode first (modern protobuf CMake, works with vcpkg)
find_package(protobuf CONFIG QUIET)
if (protobuf_FOUND)
message(STATUS "Protocol Buffers: ENABLED (found via CONFIG mode)")
# Create an interface library to propagate protobuf settings
add_library(protobuf_interface INTERFACE)
target_link_libraries(protobuf_interface INTERFACE
protobuf::libprotobuf
protobuf::libprotoc
)
target_compile_definitions(protobuf_interface INTERFACE ENABLE_PROTOBUF=1)
else()
# Fallback to MODULE mode (legacy FindProtobuf)
find_package(Protobuf QUIET)
if (Protobuf_FOUND)
message(STATUS "Protocol Buffers: ENABLED (found via MODULE mode)")
# Create an interface library to propagate protobuf settings
add_library(protobuf_interface INTERFACE)
target_link_libraries(protobuf_interface INTERFACE ${Protobuf_LIBRARIES})
target_include_directories(protobuf_interface INTERFACE ${Protobuf_INCLUDE_DIRS})
target_compile_definitions(protobuf_interface INTERFACE ENABLE_PROTOBUF=1)
else()
message(WARNING "Protocol Buffers: ENABLED but not found. Install via 'vcpkg install protobuf[zlib] protobuf[zlib]:x64-windows' or set ENABLE_PROTOBUF=OFF")
message(WARNING " To use vcpkg, configure CMake with: -DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake")
endif()
endif()
else()
message(STATUS "Protocol Buffers: DISABLED")
endif()
# Define IMGUI_NODE_EDITOR_ROOT_DIR pointing to project root directory
get_filename_component(IMGUI_NODE_EDITOR_ROOT_DIR ${CMAKE_SOURCE_DIR}/.. ABSOLUTE CACHE)
# Define IMGUI_NODE_EDITOR_ROOT_DIR pointing to project root directory
get_filename_component(ROOT_DIR ${CMAKE_SOURCE_DIR}/.. ABSOLUTE CACHE)
# Ensure IDE generators expose logical folder groupings instead of a flat list.
# Enable solution folders in Visual Studio and Folders in Xcode
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Extend the module search path so `find_package(imgui_node_editor)` locates the
# custom `Findimgui_node_editor.cmake` bundled with this repository.
# Point CMake where to look for module files.
list(APPEND CMAKE_MODULE_PATH ${IMGUI_NODE_EDITOR_ROOT_DIR}/misc/cmake-modules)
# Central place to tweak warning configuration for all example targets.
if (MSVC)
set(EXAMPLES_NO_WARNING_FLAGS "/W0" CACHE STRING "Compiler warning overrides applied to all example targets" FORCE)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
set(EXAMPLES_NO_WARNING_FLAGS "-w" CACHE STRING "Compiler warning overrides applied to all example targets" FORCE)
else()
set(EXAMPLES_NO_WARNING_FLAGS "" CACHE STRING "Compiler warning overrides applied to all example targets" FORCE)
endif()
# Examples use C++17 features (structured bindings, custom unordered_map hash).
# Node editor use C++17 (required for structured bindings and unordered_map with custom hash)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
# Make it easy to toggle console builds without editing the file.
# Option to also build console variants on Windows
# option(BUILD_CONSOLE_VARIANTS "Also build console variants of applications on Windows" ON)
# Let developers choose which target Visual Studio launches by default.
# Option to set console variant as default VS startup project
# option(USE_CONSOLE_AS_STARTUP "Set console variant as Visual Studio startup project" ON)
# Macro that will configure an example application
macro(add_example_executable name)
# Start a nested project for each example so properties such as versioning
# remain isolated.
project(${name})
# Collect all sources provided by the caller.
set(_Example_Sources
${ARGN}
)
# Add entry_point.cpp to sources (not in application library due to _CONSOLE conditional compilation)
list(APPEND _Example_Sources ${APPLICATION_ENTRY_POINT_SOURCE})
#source_group("" FILES ${_Example_Sources})
# Group example sources under their tree
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${ARGN})
# Group entry_point.cpp separately since it's from application directory
source_group("base" FILES ${APPLICATION_ENTRY_POINT_SOURCE})
file(GLOB _Nodehub_CommonResources CONFIGURE_DEPENDS "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/nodehub/data/*")
file(GLOB _Example_CommonResources CONFIGURE_DEPENDS "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/data/*")
file(GLOB _Example_Resources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
#message(FATAL_ERROR "_Example_Resources = ${_Example_Resources}")
# Choose bundle/executable type depending on platform.
set(_Example_Type)
if (WIN32)
set(_Example_Type WIN32)
set(ApplicationIcon ${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/support/Icon.ico)
file(TO_NATIVE_PATH "${ApplicationIcon}" ApplicationIcon)
string(REPLACE "\\" "\\\\" ApplicationIcon "${ApplicationIcon}")
configure_file(
${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/support/Resource.rc.in
${CMAKE_CURRENT_BINARY_DIR}/Resource.rc
)
source_group(TREE "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base" FILES ${_Example_CommonResources})
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${_Example_Resources})
list(APPEND _Example_Resources
${CMAKE_CURRENT_BINARY_DIR}/Resource.rc
${_Example_CommonResources}
)
source_group("resources" FILES ${CMAKE_CURRENT_BINARY_DIR}/Resource.rc)
elseif (APPLE)
set(_Example_Type MACOSX_BUNDLE)
set_source_files_properties(${_Example_Resources} ${_Example_CommonResources} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources/data"
)
set(_Example_Icon "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/support/Icon.icns")
list(APPEND _Example_Resources ${_Example_Icon})
set_source_files_properties(${_Example_Icon} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
endif()
add_executable(${name} ${_Example_Type} ${_Example_Sources} ${_Example_Resources} ${_Example_CommonResources})
if (EXAMPLES_NO_WARNING_FLAGS)
target_compile_options(${name} PRIVATE ${EXAMPLES_NO_WARNING_FLAGS})
endif()
# Add /FS flag for MSVC to prevent PDB locking issues during parallel builds
if (WIN32 AND MSVC)
target_compile_options(${name} PRIVATE /FS)
endif()
find_package(imgui REQUIRED)
find_package(imgui_node_editor REQUIRED)
target_link_libraries(${name} PRIVATE imgui imgui_node_editor base)
# Link protobuf if available
if (ENABLE_PROTOBUF AND TARGET protobuf_interface)
target_link_libraries(${name} PRIVATE protobuf_interface)
endif()
set(_ExampleBinDir ${CMAKE_BINARY_DIR}/bin)
set_target_properties(${name} PROPERTIES
FOLDER "applications"
RUNTIME_OUTPUT_DIRECTORY "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${_ExampleBinDir}"
DEBUG_POSTFIX _d
RELWITHDEBINGO_POSTFIX _rd
MINSIZEREL_POSTFIX _r
VS_DEBUGGER_WORKING_DIRECTORY ${_ExampleBinDir}
MACOSX_BUNDLE_INFO_PLIST "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/support/Info.plist.in"
MACOSX_BUNDLE_BUNDLE_NAME "${PACKAGE_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.sandbox.collisions"
MACOSX_BUNDLE_LONG_VERSION_STRING "${PACKAGE_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}"
MACOSX_BUNDLE_ICON_FILE Icon.icns
)
# Ensure the runtime data directory exists before copying anything in.
add_custom_command(
TARGET ${name}
PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ARGS ${_ExampleBinDir}/data
)
set(_ResourceRoot ${CMAKE_CURRENT_SOURCE_DIR})
# Copy both shared and example-specific data into the runtime folder while
# preserving relative paths.
foreach(_Resource ROOT "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/data" ${_Example_CommonResources} ROOT "${CMAKE_CURRENT_SOURCE_DIR}/data" ${_Example_Resources})
if (_Resource STREQUAL ROOT)
set(_ResourceRoot FALSE)
continue()
elseif(NOT _ResourceRoot)
set(_ResourceRoot ${_Resource})
continue()
endif()
if ("${_Resource}" MATCHES "\.DS_Store$")
list(REMOVE_ITEM _Example_Resources ${_Resource})
list(REMOVE_ITEM _Example_CommonResources ${_Resource})
continue()
endif()
file(RELATIVE_PATH _RelResource ${_ResourceRoot} ${_Resource})
add_custom_command(
TARGET ${name}
PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ARGS ${_Resource} ${_ExampleBinDir}/data/${_RelResource}
)
endforeach()
# On Windows, optionally create a console variant
if (WIN32 AND BUILD_CONSOLE_VARIANTS)
set(_ConsoleTargetName ${name}-console)
# Create console executable (without WIN32 flag)
add_executable(${_ConsoleTargetName} ${_Example_Sources} ${_Example_Resources} ${_Example_CommonResources})
if (EXAMPLES_NO_WARNING_FLAGS)
target_compile_options(${_ConsoleTargetName} PRIVATE ${EXAMPLES_NO_WARNING_FLAGS})
endif()
# Define _CONSOLE to use main() instead of WinMain()
target_compile_definitions(${_ConsoleTargetName} PRIVATE _CONSOLE)
# Add /FS flag for MSVC to prevent PDB locking issues during parallel builds
if (MSVC)
target_compile_options(${_ConsoleTargetName} PRIVATE /FS)
endif()
# Link the same libraries
find_package(imgui REQUIRED)
find_package(imgui_node_editor REQUIRED)
target_link_libraries(${_ConsoleTargetName} PRIVATE imgui imgui_node_editor application)
# Link protobuf if available
if (ENABLE_PROTOBUF AND TARGET protobuf_interface)
target_link_libraries(${_ConsoleTargetName} PRIVATE protobuf_interface)
endif()
# Set the same properties as the GUI version
set_target_properties(${_ConsoleTargetName} PROPERTIES
FOLDER "examples"
RUNTIME_OUTPUT_DIRECTORY "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${_ExampleBinDir}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${_ExampleBinDir}"
DEBUG_POSTFIX _d
RELWITHDEBINGO_POSTFIX _rd
MINSIZEREL_POSTFIX _r
VS_DEBUGGER_WORKING_DIRECTORY ${_ExampleBinDir}
)
# Add custom commands for resource copying (same as GUI version)
add_custom_command(
TARGET ${_ConsoleTargetName}
PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ARGS ${_ExampleBinDir}/data
)
foreach(_Resource ROOT "${IMGUI_NODE_EDITOR_ROOT_DIR}/applications/base/data" ${_Example_CommonResources} ROOT "${CMAKE_CURRENT_SOURCE_DIR}/data" ${_Example_Resources})
if (_Resource STREQUAL ROOT)
set(_ResourceRoot FALSE)
continue()
elseif(NOT _ResourceRoot)
set(_ResourceRoot ${_Resource})
continue()
endif()
if ("${_Resource}" MATCHES "\.DS_Store$")
continue()
endif()
file(RELATIVE_PATH _RelResource ${_ResourceRoot} ${_Resource})
# Reuse the resource copying logic for the console binary.
add_custom_command(
TARGET ${_ConsoleTargetName}
PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ARGS ${_Resource} ${_ExampleBinDir}/data/${_RelResource}
)
endforeach()
endif()
endmacro()
# Build the shared application support code and individual examples.
add_subdirectory(base)
add_subdirectory(nodehub)
add_subdirectory(tests)
# Set the default Visual Studio startup project
if (WIN32 AND BUILD_CONSOLE_VARIANTS AND USE_CONSOLE_AS_STARTUP)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT nodehub-console)
message(STATUS "Visual Studio startup project: nodehub-console")
else()
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT nodehub)
message(STATUS "Visual Studio startup project: nodehub")
endif()

View File

@ -0,0 +1,119 @@
cmake_minimum_required(VERSION 3.5)
project(base)
set(_base_Sources
include/base.h
source/base.cpp
source/entry_point.cpp
source/imgui_extra_keys.h
source/config.h.in
source/setup.h
source/platform.h
source/platform_win32.cpp
source/platform_glfw.cpp
source/renderer.h
source/renderer_dx11.cpp
source/renderer_ogl3.cpp
)
# entry_point.cpp is not included in the library because it has
# conditional compilation for WinMain() vs main() based on _CONSOLE define.
# It will be added directly to each executable target instead.
set(base_ENTRY_POINT_SOURCE
${CMAKE_CURRENT_SOURCE_DIR}/source/entry_point.cpp
CACHE INTERNAL ""
)
add_library(base STATIC)
target_include_directories(base PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
find_package(imgui REQUIRED)
find_package(stb_image REQUIRED)
find_package(ScopeGuard REQUIRED)
target_link_libraries(base PUBLIC imgui)
target_link_libraries(base PRIVATE stb_image ScopeGuard)
if (WIN32)
list(APPEND _base_Sources
source/imgui_impl_dx11.cpp
source/imgui_impl_dx11.h
source/imgui_impl_win32.cpp
source/imgui_impl_win32.h
)
set(_DXSDK_Dir ${IMGUI_NODE_EDITOR_ROOT_DIR}/external/DXSDK)
set(_DXSDK_Arch x86)
if (${CMAKE_SIZEOF_VOID_P} EQUAL 8)
set(_DXSDK_Arch x64)
endif()
add_library(dxerr STATIC ${_DXSDK_Dir}/src/dxerr.cpp)
target_include_directories(dxerr PUBLIC "${_DXSDK_Dir}/include")
set_property(TARGET dxerr PROPERTY FOLDER "external")
add_library(d3dx11 UNKNOWN IMPORTED)
set_target_properties(d3dx11 PROPERTIES
IMPORTED_LOCATION "${_DXSDK_Dir}/lib/${_DXSDK_Arch}/d3dx11.lib"
IMPORTED_LOCATION_DEBUG "${_DXSDK_Dir}/lib/${_DXSDK_Arch}/d3dx11d.lib"
INTERFACE_INCLUDE_DIRECTORIES "${_DXSDK_Dir}/include"
INTERFACE_LINK_LIBRARIES "$<$<CONFIG:Debug>:dxerr>"
)
target_link_libraries(base PRIVATE d3d11.lib d3dcompiler.lib d3dx11)
else()
find_package(OpenGL REQUIRED)
find_package(glfw3 3 REQUIRED)
if (APPLE)
target_link_libraries(base PRIVATE
"-framework CoreFoundation"
"-framework Cocoa"
"-framework IOKit"
"-framework CoreVideo"
)
endif()
endif()
if (OpenGL_FOUND)
set(HAVE_OPENGL YES)
target_include_directories(base PRIVATE ${OPENGL_INCLUDE_DIR})
target_link_libraries(base PRIVATE ${OPENGL_gl_LIBRARY})
list(APPEND _base_Sources
source/imgui_impl_opengl3.cpp
source/imgui_impl_opengl3.h
source/imgui_impl_opengl3_loader.h
)
endif()
if (glfw3_FOUND)
set(HAVE_GLFW3 YES)
list(APPEND _base_Sources
source/imgui_impl_glfw.cpp
source/imgui_impl_glfw.h
)
target_link_libraries(base PRIVATE
glfw
)
endif()
configure_file(
source/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/source/config.h
)
target_compile_definitions(base PRIVATE
#BACKEND_CONFIG=IMGUI_GLFW
#RENDERER_CONFIG=IMGUI_OGL3
)
# Make config.h include directory PUBLIC since entry_point.cpp (which needs it) is now compiled in executables
target_include_directories(base PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/source)
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${_base_Sources})
target_sources(base PRIVATE ${_base_Sources})
set_property(TARGET base PROPERTY FOLDER "applications")

View File

@ -0,0 +1,33 @@
#ifndef APP_TYPES_H
#define APP_TYPES_H
# include <string>
# include <memory>
# include <map>
# include <vector>
#include <string>
struct ArgValue
{
enum class Type { Empty, Bool, Int, Double, String };
Type Type = Type::Empty;
bool Bool = false;
long long Int = 0;
double Double = 0.0;
std::string String;
ArgValue() = default;
ArgValue(bool value) : Type(Type::Bool), Bool(value) {}
ArgValue(int value) : Type(Type::Int), Int(value) {}
ArgValue(long long value) : Type(Type::Int), Int(value) {}
ArgValue(float value) : Type(Type::Double), Double(value) {}
ArgValue(double value) : Type(Type::Double), Double(value) {}
ArgValue(const char* value) : Type(Type::String), String(value) {}
ArgValue(std::string value) : Type(Type::String), String(std::move(value)) {}
};
using ArgsMap = std::map<std::string, ArgValue>;
#endif

View File

@ -0,0 +1,84 @@
# pragma once
# include <imgui.h>
# include <string>
# include <memory>
# include <map>
# include <vector>
# include "app-types.h"
struct WindowState
{
int x = -1;
int y = -1;
int width = 1440;
int height = 800;
int monitor = 0;
bool maximized = false;
};
struct Platform;
struct Renderer;
struct Application
{
Application(const char* name);
Application(const char* name, const ArgsMap& args);
~Application();
bool Create(int width = -1, int height = -1);
int Run();
void SetTitle(const char* title);
bool Close();
void Quit();
const std::string& GetName() const;
ImFont* DefaultFont() const;
ImFont* HeaderFont() const;
ImTextureID LoadTexture(const char* path);
ImTextureID CreateTexture(const void* data, int width, int height);
void DestroyTexture(ImTextureID texture);
int GetTextureWidth(ImTextureID texture);
int GetTextureHeight(ImTextureID texture);
bool TakeScreenshot(const char* filename = nullptr);
bool SaveWindowState();
bool LoadWindowState();
virtual void OnStart() {}
virtual void OnStop() {}
virtual void OnFrame(float deltaTime) {}
virtual ImGuiWindowFlags GetWindowFlags() const;
virtual bool CanClose() { return true; }
protected:
ArgsMap m_Args; // CLI arguments (accessible to derived classes)
private:
void RecreateFontAtlas();
void Frame();
std::string m_Name;
std::string m_IniFilename;
std::string m_WindowStateFilename;
std::unique_ptr<Platform> m_Platform;
std::unique_ptr<Renderer> m_Renderer;
ImGuiContext* m_Context = nullptr;
ImFont* m_DefaultFont = nullptr;
ImFont* m_HeaderFont = nullptr;
WindowState m_WindowState;
};
extern Application* g_Application;
int Main(const ArgsMap& args);

View File

@ -0,0 +1,458 @@
# include "base.h"
# include "setup.h"
# include "platform.h"
# include "renderer.h"
# include <vector>
# include <fstream>
# include <sstream>
#ifdef _WIN32
# include <windows.h>
# include <io.h>
# include <fcntl.h>
# include <iostream>
# include <cstdio>
#endif
namespace {
#ifdef _WIN32
static inline std::string WideToUtf8(const wchar_t* wstr) {
if (!wstr) return {};
// Ask for required size (includes NUL)
const int n = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
if (n <= 0) return {};
std::string out;
out.resize(n - 1); // we store without the trailing NUL
#if __cplusplus >= 201703L
// C++17: string::data() is char*
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, out.data(), n, nullptr, nullptr);
#else
// C++11/14: use &out[0] to get char*
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &out[0], n, nullptr, nullptr);
#endif
// The call above wrote a NUL at the end because we passed length n.
// Keep size at n-1 (already set via resize).
return out;
}
static std::string GetExecutableDir()
{
wchar_t exe_path_buffer[MAX_PATH] = { 0 };
if (GetModuleFileNameW(NULL, exe_path_buffer, MAX_PATH) == 0)
return "";
std::wstring exe_path(exe_path_buffer);
size_t last_slash = exe_path.find_last_of(L"\\/");
if (last_slash == std::wstring::npos)
return "";
return WideToUtf8(exe_path.substr(0, last_slash).c_str());
}
#else
static std::string GetExecutableDir()
{
// Implementation for other platforms (e.g., Linux, macOS) would go here.
return "";
}
#endif
static std::string ResolveResourcePath(const std::string& relativePath)
{
static const std::string exeDir = GetExecutableDir();
if (!exeDir.empty())
{
std::string exePath = exeDir + "/" + relativePath;
std::ifstream f(exePath);
if (f.good())
{
return exePath;
}
}
// Fallback to CWD
return relativePath;
}
} // namespace
extern "C" {
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_STATIC
#include "stb_image.h"
}
Application::Application(const char* name)
: Application(name, {}){}
Application::Application(const char* name, const ArgsMap& args)
: m_Name(name)
, m_Args(args)
, m_Platform(CreatePlatform(*this))
, m_Renderer(CreateRenderer())
{
g_Application = this;
// Convert map to argc/argv for platform compatibility
std::vector<std::string> argv_vec;
argv_vec.push_back(name); // program name
for (const auto& pair : args) {
if (!pair.first.empty()) {
argv_vec.push_back("--" + pair.first);
const auto& value = pair.second;
if (value.Type != ArgValue::Type::Empty)
{
if (value.Type == ArgValue::Type::String)
argv_vec.push_back(value.String);
else if (value.Type == ArgValue::Type::Bool)
argv_vec.push_back(value.Bool ? "true" : "false");
else if (value.Type == ArgValue::Type::Int)
argv_vec.push_back(std::to_string(value.Int));
else if (value.Type == ArgValue::Type::Double)
argv_vec.push_back(std::to_string(value.Double));
}
}
}
// Convert to char** for platform
std::vector<char*> argv_ptrs;
for (auto& str : argv_vec) {
argv_ptrs.push_back(const_cast<char*>(str.c_str()));
}
m_Platform->ApplicationStart(static_cast<int>(argv_ptrs.size()), argv_ptrs.data());
}
Application::~Application()
{
g_Application = nullptr;
// Save window state before cleanup
if (m_Platform)
{
if (!SaveWindowState())
{
}
}
m_Renderer->Destroy();
m_Platform->ApplicationStop();
if (m_Context)
{
ImGui::DestroyContext(m_Context);
m_Context= nullptr;
}
}
bool Application::Create(int width /*= -1*/, int height /*= -1*/)
{
m_Context = ImGui::CreateContext();
ImGui::SetCurrentContext(m_Context);
// Set filenames first
m_IniFilename = m_Name + ".ini";
m_WindowStateFilename = m_Name + "_window.json";
// Load saved window state
if (LoadWindowState())
{
// Use saved dimensions if not explicitly provided
if (width < 0)
width = m_WindowState.width;
if (height < 0)
height = m_WindowState.height;
}
else
{
}
if (!m_Platform->OpenMainWindow("NodeHub", width, height))
return false;
// Restore window position/monitor after creation
if (m_WindowState.x >= 0 && m_WindowState.y >= 0)
{
m_Platform->SetWindowState(m_WindowState);
}
if (!m_Renderer->Create(*m_Platform))
return false;
ImGuiIO& io = ImGui::GetIO();
//io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.IniFilename = m_IniFilename.c_str();
io.LogFilename = nullptr;
ImGui::StyleColorsDark();
RecreateFontAtlas();
m_Platform->AcknowledgeWindowScaleChanged();
m_Platform->AcknowledgeFramebufferScaleChanged();
OnStart();
Frame();
return true;
}
int Application::Run()
{
m_Platform->ShowMainWindow();
while (m_Platform->ProcessMainWindowEvents())
{
if (!m_Platform->IsMainWindowVisible())
continue;
Frame();
}
OnStop();
return 0;
}
void Application::RecreateFontAtlas()
{
ImGuiIO& io = ImGui::GetIO();
IM_DELETE(io.Fonts);
io.Fonts = IM_NEW(ImFontAtlas);
ImFontConfig config;
config.OversampleH = 4;
config.OversampleV = 4;
config.PixelSnapH = false;
m_DefaultFont = io.Fonts->AddFontFromFileTTF(ResolveResourcePath("data/Play-Regular.ttf").c_str(), 18.0f, &config);
m_HeaderFont = io.Fonts->AddFontFromFileTTF(ResolveResourcePath("data/Cuprum-Bold.ttf").c_str(), 20.0f, &config);
io.Fonts->Build();
}
void Application::Frame()
{
auto& io = ImGui::GetIO();
if (m_Platform->HasWindowScaleChanged())
m_Platform->AcknowledgeWindowScaleChanged();
if (m_Platform->HasFramebufferScaleChanged())
{
RecreateFontAtlas();
m_Platform->AcknowledgeFramebufferScaleChanged();
}
const float windowScale = m_Platform->GetWindowScale();
const float framebufferScale = m_Platform->GetFramebufferScale();
if (io.WantSetMousePos)
{
io.MousePos.x *= windowScale;
io.MousePos.y *= windowScale;
}
m_Platform->NewFrame();
// Don't touch "uninitialized" mouse position
if (io.MousePos.x > -FLT_MAX && io.MousePos.y > -FLT_MAX)
{
io.MousePos.x /= windowScale;
io.MousePos.y /= windowScale;
}
io.DisplaySize.x /= windowScale;
io.DisplaySize.y /= windowScale;
io.DisplayFramebufferScale.x = framebufferScale;
io.DisplayFramebufferScale.y = framebufferScale;
m_Renderer->NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(io.DisplaySize);
const auto windowBorderSize = ImGui::GetStyle().WindowBorderSize;
const auto windowRounding = ImGui::GetStyle().WindowRounding;
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::Begin("Content", nullptr, GetWindowFlags());
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, windowBorderSize);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, windowRounding);
OnFrame(io.DeltaTime);
ImGui::PopStyleVar(2);
ImGui::End();
ImGui::PopStyleVar(2);
// Rendering
m_Renderer->Clear(ImColor(32, 32, 32, 255));
ImGui::Render();
m_Renderer->RenderDrawData(ImGui::GetDrawData());
m_Platform->FinishFrame();
}
void Application::SetTitle(const char* title)
{
m_Platform->SetMainWindowTitle(title);
}
bool Application::Close()
{
return m_Platform->CloseMainWindow();
}
void Application::Quit()
{
m_Platform->Quit();
}
const std::string& Application::GetName() const
{
return m_Name;
}
ImFont* Application::DefaultFont() const
{
return m_DefaultFont;
}
ImFont* Application::HeaderFont() const
{
return m_HeaderFont;
}
ImTextureID Application::LoadTexture(const char* path)
{
int width = 0, height = 0, component = 0;
if (auto data = stbi_load(ResolveResourcePath(path).c_str(), &width, &height, &component, 4))
{
auto texture = CreateTexture(data, width, height);
stbi_image_free(data);
return texture;
}
else
return nullptr;
}
ImTextureID Application::CreateTexture(const void* data, int width, int height)
{
return m_Renderer->CreateTexture(data, width, height);
}
void Application::DestroyTexture(ImTextureID texture)
{
m_Renderer->DestroyTexture(texture);
}
int Application::GetTextureWidth(ImTextureID texture)
{
return m_Renderer->GetTextureWidth(texture);
}
int Application::GetTextureHeight(ImTextureID texture)
{
return m_Renderer->GetTextureHeight(texture);
}
bool Application::TakeScreenshot(const char* filename)
{
return m_Renderer->TakeScreenshot(filename);
}
bool Application::SaveWindowState()
{
if (!m_Platform)
return false;
auto state = m_Platform->GetWindowState();
std::ofstream file(m_WindowStateFilename);
if (!file)
return false;
file << "{\n";
file << " \"window\": {\n";
file << " \"x\": " << state.x << ",\n";
file << " \"y\": " << state.y << ",\n";
file << " \"width\": " << state.width << ",\n";
file << " \"height\": " << state.height << ",\n";
file << " \"monitor\": " << state.monitor << ",\n";
file << " \"maximized\": " << (state.maximized ? "true" : "false") << "\n";
file << " }\n";
file << "}\n";
return true;
}
bool Application::LoadWindowState()
{
std::ifstream file(m_WindowStateFilename);
if (!file)
return false;
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
// Simple JSON parsing for our specific structure
auto findValue = [&content](const std::string& key) -> std::string {
std::string searchKey = "\"" + key + "\":";
size_t pos = content.find(searchKey);
if (pos == std::string::npos)
return "";
pos += searchKey.length();
while (pos < content.length() && (content[pos] == ' ' || content[pos] == '\t'))
pos++;
size_t endPos = pos;
while (endPos < content.length() && content[endPos] != ',' && content[endPos] != '\n' && content[endPos] != '}')
endPos++;
return content.substr(pos, endPos - pos);
};
try {
std::string xStr = findValue("x");
std::string yStr = findValue("y");
std::string widthStr = findValue("width");
std::string heightStr = findValue("height");
std::string monitorStr = findValue("monitor");
std::string maximizedStr = findValue("maximized");
if (!xStr.empty()) m_WindowState.x = std::stoi(xStr);
if (!yStr.empty()) m_WindowState.y = std::stoi(yStr);
if (!widthStr.empty()) m_WindowState.width = std::stoi(widthStr);
if (!heightStr.empty()) m_WindowState.height = std::stoi(heightStr);
if (!monitorStr.empty()) m_WindowState.monitor = std::stoi(monitorStr);
if (!maximizedStr.empty()) m_WindowState.maximized = (maximizedStr.find("true") != std::string::npos);
return true;
}
catch (...) {
// If parsing fails, return false and use defaults
return false;
}
}
ImGuiWindowFlags Application::GetWindowFlags() const
{
return
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoBringToFrontOnFocus;
}

View File

@ -0,0 +1,4 @@
# pragma once
# define HAVE_GLFW3 0
# define HAVE_OPENGL 0

View File

@ -0,0 +1,4 @@
# pragma once
# cmakedefine01 HAVE_GLFW3
# cmakedefine01 HAVE_OPENGL

View File

@ -0,0 +1,324 @@
#define _CRT_SECURE_NO_WARNINGS
# include "base.h"
# include "platform.h"
# include <map>
# include <vector>
# include <iostream>
# include <cstdio>
# include <csignal>
#if defined(_WIN32)
# include <io.h>
# include <fcntl.h>
# include <windows.h>
#endif
#define CLI11_DISABLE_EXTRA_VALIDATORS 1
# include "../../external/CLI11.hpp"
namespace {
class CommandLineParser
{
public:
// Using unique_ptr as a C++14 alternative to optional.
// A null return indicates the program should exit.
std::unique_ptr<ArgsMap> Parse(std::vector<std::string> args)
{
CLI::App app{"NodeHub"};
app.allow_extras();
app.add_option("--file", "the source path")->capture_default_str();
// Graph file path (absolute or relative)
app.add_option("--graph", "Path to graph file (absolute or relative)")->capture_default_str();
// CLI mode options
app.add_flag("--headless", "Run without GUI (CLI mode)");
app.add_option("--command", "Command to execute in headless mode (validate, export, execute, info)")
->capture_default_str();
app.add_option("--output", "Output file path for export commands")->capture_default_str();
app.add_option("--format", "Output format (json, xml, yaml, dot)")->capture_default_str();
// Runtime execution options
app.add_flag("--run", "Execute the graph runtime (used with --headless and --graph)");
app.add_option("--log", "Path to log file (absolute or relative)")->capture_default_str();
app.add_option("--log-level", "Minimum logging level (trace, debug, info, warn, error, critical, off)")->capture_default_str();
// Manual pre-check for help flag because allow_extras() interferes with normal help handling
for (const auto& arg : args) {
if (arg == "--help" || arg == "-h") {
PrintHelp(app);
m_ExitCode = 0;
return nullptr;
}
}
try {
app.parse(args);
} catch (const CLI::ParseError& e) {
HandleParseError(app, e);
m_ExitCode = app.exit(e);
return nullptr;
}
auto args_map = std::make_unique<ArgsMap>();
for (const CLI::Option* option : app.get_options())
{
if(option->count() > 0 && !option->get_lnames().empty())
{
if (!option->results().empty())
{
(*args_map)[option->get_lnames()[0]] = ParseValue(option->results()[0]);
}
else
{
(*args_map)[option->get_lnames()[0]] = ArgValue(true);
}
}
}
ParseKeyValueArgs(app.remaining(), *args_map);
m_ExitCode = 0;
return args_map;
}
int GetExitCode() const { return m_ExitCode; }
protected:
virtual void PrintHelp(CLI::App& app)
{
printf("%s\n", app.help().c_str());
fflush(stdout);
}
virtual void HandleParseError(CLI::App& app, const CLI::ParseError& e)
{
// Default console implementation
fprintf(stderr, "%s\n", app.help().c_str());
fprintf(stderr, "%s: %s\n", app.get_name().c_str(), e.what());
fflush(stderr);
}
int m_ExitCode = 0;
private:
ArgValue ParseValue(const std::string& s)
{
if (s.empty())
return ArgValue();
if (s == "true" || s == "TRUE" || s == "True")
return ArgValue(true);
else if (s == "false" || s == "FALSE" || s == "False")
return ArgValue(false);
char* end = nullptr;
const char* start = s.c_str();
// Try parsing as an integer
long long int_val = strtoll(start, &end, 10);
if (end == start + s.length())
return ArgValue(int_val);
// Try parsing as a double
end = nullptr;
double double_val = strtod(start, &end);
if (end == start + s.length())
return ArgValue(double_val);
return ArgValue(s);
}
void ParseKeyValueArgs(const std::vector<std::string>& args, ArgsMap& args_map)
{
for (size_t i = 0; i < args.size(); ++i) {
std::string arg = args[i];
if (arg.length() > 2 && arg.substr(0, 2) == "--") {
std::string body = arg.substr(2);
if (body.empty())
continue;
auto equalPos = body.find('=');
if (equalPos != std::string::npos) {
std::string key = body.substr(0, equalPos);
std::string value = body.substr(equalPos + 1);
if (!key.empty())
args_map[key] = ParseValue(value);
continue;
}
std::string key = body;
if (i + 1 < args.size() && !args[i + 1].empty() && args[i + 1][0] != '-') {
args_map[key] = ParseValue(args[i + 1]);
++i; // Skip the value
} else {
args_map[key] = true;
}
}
}
}
};
}
Application* g_Application = nullptr;
namespace
{
void SignalHandler(int signal)
{
if (g_Application)
g_Application->Quit();
}
}
# if defined(_WIN32) && !defined(_CONSOLE)
static void redirect_std_to_console() {
// reopen stdout/stderr to the console
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
freopen_s(&fp, "CONOUT$", "w", stderr);
freopen_s(&fp, "CONIN$", "r", stdin);
// make iostreams sync with C stdio
std::ios::sync_with_stdio(true);
}
static bool ensure_console_attached() {
// Try to attach if launched from a terminal
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
redirect_std_to_console();
return true;
}
return false;
}
static void ensure_console_allocated() {
if (AllocConsole()) {
redirect_std_to_console();
}
}
# if PLATFORM(WINDOWS)
# define NOMINMAX
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <stdlib.h> // __argc, argv
# endif
inline std::string WideToUtf8(const wchar_t* wstr) {
if (!wstr) return {};
// Ask for required size (includes NUL)
const int n = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
if (n <= 0) return {};
std::string out;
out.resize(n - 1); // we store without the trailing NUL
#if __cplusplus >= 201703L
// C++17: string::data() is char*
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, out.data(), n, nullptr, nullptr);
#else
// C++11/14: use &out[0] to get char*
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &out[0], n, nullptr, nullptr);
#endif
// The call above wrote a NUL at the end because we passed length n.
// Keep size at n-1 (already set via resize).
return out;
}
// Build argc/argv as **mutable** C strings
struct ArgvUtf8 {
int argc = 0;
std::vector<std::unique_ptr<char[]>> storage; // owns the writable C strings
std::vector<char*> argv; // pointers to the writable strings
};
static std::vector<std::string> GetUtf8Argv() {
int wargc = 0;
LPWSTR* wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
std::vector<std::string> args;
if (wargv) {
for (int i = 0; i < wargc; ++i)
args.emplace_back(WideToUtf8(wargv[i]));
LocalFree(wargv);
}
return args;
}
static std::vector<std::string> GetUtf8Args()
{
int wargc = 0;
LPWSTR* wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
std::vector<std::string> args;
if (wargv) {
for (int i = 1; i < wargc; ++i) // skip executable path
args.emplace_back(WideToUtf8(wargv[i]));
LocalFree(wargv);
}
return args;
}
namespace {
class WindowsCommandLineParser : public CommandLineParser
{
protected:
void HandleParseError(CLI::App& app, const CLI::ParseError& e) override
{
// Print to attached console first
CommandLineParser::HandleParseError(app, e);
// Then show a message box for the GUI user
std::string msg = std::string("Argument parsing failed:\n\n") + e.what() +
"\n\nTry '--help' for usage.";
MessageBoxA(nullptr, msg.c_str(), "CLI Error", MB_ICONERROR | MB_OK);
}
};
}
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
signal(SIGINT, SignalHandler);
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
if (!ensure_console_attached()) ensure_console_allocated();
fflush(stdout);
WindowsCommandLineParser parser;
auto args = GetUtf8Args();
auto args_map = parser.Parse(args);
int exit_code;
if (args_map)
{
exit_code = Main(*args_map);
}
else
{
exit_code = parser.GetExitCode();
}
return exit_code;
}
# else
int main(int argc, char** argv)
{
signal(SIGINT, SignalHandler);
CommandLineParser parser;
std::vector<std::string> args;
for (int i = 1; i < argc; ++i)
args.push_back(argv[i]);
auto args_map = parser.Parse(args);
if (args_map)
return Main(*args_map);
else
return parser.GetExitCode();
}
# endif

View File

@ -0,0 +1,65 @@
# pragma once
# include <imgui.h>
# if !defined(IMGUI_VERSION_NUM) || (IMGUI_VERSION_NUM < 18822)
# include <type_traits>
// https://stackoverflow.com/a/8597498
# define DECLARE_HAS_NESTED(Name, Member) \
\
template<class T> \
struct has_nested_ ## Name \
{ \
typedef char yes; \
typedef yes(&no)[2]; \
\
template<class U> static yes test(decltype(U::Member)*); \
template<class U> static no test(...); \
\
static bool const value = sizeof(test<T>(0)) == sizeof(yes); \
};
# define DECLARE_KEY_TESTER(Key) \
DECLARE_HAS_NESTED(Key, Key) \
struct KeyTester_ ## Key \
{ \
template <typename T> \
static int Get(typename std::enable_if<has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
{ \
return T::Key; \
} \
\
template <typename T> \
static int Get(typename std::enable_if<!has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
{ \
return -1; \
} \
}
DECLARE_KEY_TESTER(ImGuiKey_F);
DECLARE_KEY_TESTER(ImGuiKey_D);
static inline int GetEnumValueForF()
{
return KeyTester_ImGuiKey_F::Get<ImGuiKey_>(nullptr);
}
static inline int GetEnumValueForD()
{
return KeyTester_ImGuiKey_D::Get<ImGuiKey_>(nullptr);
}
# else
static inline ImGuiKey GetEnumValueForF()
{
return ImGuiKey_F;
}
static inline ImGuiKey GetEnumValueForD()
{
return ImGuiKey_D;
}
# endif

View File

@ -0,0 +1,700 @@
// dear imgui: Renderer for DirectX11
// This needs to be used along with a Platform Binding (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID in imgui.cpp.
// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bits indices.
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp
// https://github.com/ocornut/imgui
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
// 2016-05-07: DirectX11: Disabling depth-write.
#include "imgui.h"
#include "imgui_impl_dx11.h"
// DirectX
struct IUnknown;
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#endif
#include <d3d11.h>
#include <d3dcompiler.h>
#ifdef _MSC_VER
#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
#endif
struct TEXTURE;
// DirectX data
static ID3D11Device* g_pd3dDevice = NULL;
static ID3D11DeviceContext* g_pd3dDeviceContext = NULL;
static IDXGIFactory* g_pFactory = NULL;
static ID3D11Buffer* g_pVB = NULL;
static ID3D11Buffer* g_pIB = NULL;
static ID3D10Blob* g_pVertexShaderBlob = NULL;
static ID3D11VertexShader* g_pVertexShader = NULL;
static ID3D11InputLayout* g_pInputLayout = NULL;
static ID3D11Buffer* g_pVertexConstantBuffer = NULL;
static ID3D10Blob* g_pPixelShaderBlob = NULL;
static ID3D11PixelShader* g_pPixelShader = NULL;
static ID3D11SamplerState* g_pFontSampler = NULL;
static ImTextureID g_pFontTextureID = NULL;
static ID3D11RasterizerState* g_pRasterizerState = NULL;
static ID3D11BlendState* g_pBlendState = NULL;
static ID3D11DepthStencilState* g_pDepthStencilState = NULL;
static int g_VertexBufferSize = 5000, g_IndexBufferSize = 10000;
static ImVector<TEXTURE*> g_Textures;
struct VERTEX_CONSTANT_BUFFER
{
float mvp[4][4];
};
struct TEXTURE
{
TEXTURE()
{
View = NULL;
Width = 0;
Height = 0;
}
ID3D11ShaderResourceView* View;
int Width;
int Height;
ImVector<unsigned char> Data;
};
// Forward Declarations
static bool ImGui_UploadTexture(TEXTURE* texture);
static void ImGui_ReleaseTexture(TEXTURE* texture);
static void ImGui_ImplDX11_SetupRenderState(ImDrawData* draw_data, ID3D11DeviceContext* ctx)
{
// Setup viewport
D3D11_VIEWPORT vp;
memset(&vp, 0, sizeof(D3D11_VIEWPORT));
vp.Width = draw_data->DisplaySize.x * draw_data->FramebufferScale.x;
vp.Height = draw_data->DisplaySize.y * draw_data->FramebufferScale.y;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = vp.TopLeftY = 0;
ctx->RSSetViewports(1, &vp);
// Setup shader and vertex buffers
unsigned int stride = sizeof(ImDrawVert);
unsigned int offset = 0;
ctx->IASetInputLayout(g_pInputLayout);
ctx->IASetVertexBuffers(0, 1, &g_pVB, &stride, &offset);
ctx->IASetIndexBuffer(g_pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ctx->VSSetShader(g_pVertexShader, NULL, 0);
ctx->VSSetConstantBuffers(0, 1, &g_pVertexConstantBuffer);
ctx->PSSetShader(g_pPixelShader, NULL, 0);
ctx->PSSetSamplers(0, 1, &g_pFontSampler);
// Setup blend state
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
ctx->OMSetBlendState(g_pBlendState, blend_factor, 0xffffffff);
ctx->OMSetDepthStencilState(g_pDepthStencilState, 0);
ctx->RSSetState(g_pRasterizerState);
}
// Render function
// (this used to be set in io.RenderDrawListsFn and called by ImGui::Render(), but you can now call this directly from your main loop)
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
{
// Avoid rendering when minimized
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
return;
ID3D11DeviceContext* ctx = g_pd3dDeviceContext;
// Create and grow vertex/index buffers if needed
if (!g_pVB || g_VertexBufferSize < draw_data->TotalVtxCount)
{
if (g_pVB) { g_pVB->Release(); g_pVB = NULL; }
g_VertexBufferSize = draw_data->TotalVtxCount + 5000;
D3D11_BUFFER_DESC desc;
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = g_VertexBufferSize * sizeof(ImDrawVert);
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
if (g_pd3dDevice->CreateBuffer(&desc, NULL, &g_pVB) < 0)
return;
}
if (!g_pIB || g_IndexBufferSize < draw_data->TotalIdxCount)
{
if (g_pIB) { g_pIB->Release(); g_pIB = NULL; }
g_IndexBufferSize = draw_data->TotalIdxCount + 10000;
D3D11_BUFFER_DESC desc;
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = g_IndexBufferSize * sizeof(ImDrawIdx);
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
if (g_pd3dDevice->CreateBuffer(&desc, NULL, &g_pIB) < 0)
return;
}
// Upload vertex/index data into a single contiguous GPU buffer
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
if (ctx->Map(g_pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
return;
if (ctx->Map(g_pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
return;
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
for (int n = 0; n < draw_data->CmdListsCount; n++)
{
const ImDrawList* cmd_list = draw_data->CmdLists[n];
memcpy(vtx_dst, cmd_list->VtxBuffer.Data, cmd_list->VtxBuffer.Size * sizeof(ImDrawVert));
memcpy(idx_dst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx));
vtx_dst += cmd_list->VtxBuffer.Size;
idx_dst += cmd_list->IdxBuffer.Size;
}
ctx->Unmap(g_pVB, 0);
ctx->Unmap(g_pIB, 0);
// Setup orthographic projection matrix into our constant buffer
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
{
D3D11_MAPPED_SUBRESOURCE mapped_resource;
if (ctx->Map(g_pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) != S_OK)
return;
VERTEX_CONSTANT_BUFFER* constant_buffer = (VERTEX_CONSTANT_BUFFER*)mapped_resource.pData;
float L = draw_data->DisplayPos.x;
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
float T = draw_data->DisplayPos.y;
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
float mvp[4][4] =
{
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.5f, 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
};
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
ctx->Unmap(g_pVertexConstantBuffer, 0);
}
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
struct BACKUP_DX11_STATE
{
UINT ScissorRectsCount, ViewportsCount;
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
ID3D11RasterizerState* RS;
ID3D11BlendState* BlendState;
FLOAT BlendFactor[4];
UINT SampleMask;
UINT StencilRef;
ID3D11DepthStencilState* DepthStencilState;
ID3D11ShaderResourceView* PSShaderResource;
ID3D11SamplerState* PSSampler;
ID3D11PixelShader* PS;
ID3D11VertexShader* VS;
UINT PSInstancesCount, VSInstancesCount;
ID3D11ClassInstance* PSInstances[256], *VSInstances[256]; // 256 is max according to PSSetShader documentation
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
DXGI_FORMAT IndexBufferFormat;
ID3D11InputLayout* InputLayout;
};
BACKUP_DX11_STATE old;
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
ctx->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
ctx->RSGetViewports(&old.ViewportsCount, old.Viewports);
ctx->RSGetState(&old.RS);
ctx->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
ctx->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
ctx->PSGetShaderResources(0, 1, &old.PSShaderResource);
ctx->PSGetSamplers(0, 1, &old.PSSampler);
old.PSInstancesCount = old.VSInstancesCount = 256;
ctx->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
ctx->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
ctx->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
ctx->IAGetPrimitiveTopology(&old.PrimitiveTopology);
ctx->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
ctx->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
ctx->IAGetInputLayout(&old.InputLayout);
// Setup desired DX state
ImGui_ImplDX11_SetupRenderState(draw_data, ctx);
// Render command lists
// (Because we merged all buffers into a single one, we maintain our own offset into them)
int global_idx_offset = 0;
int global_vtx_offset = 0;
// Will project scissor/clipping rectangles into framebuffer space
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
for (int n = 0; n < draw_data->CmdListsCount; n++)
{
const ImDrawList* cmd_list = draw_data->CmdLists[n];
for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
if (pcmd->UserCallback != NULL)
{
// User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
ImGui_ImplDX11_SetupRenderState(draw_data, ctx);
else
pcmd->UserCallback(cmd_list, pcmd);
}
else
{
ImVec4 clip_rect;
clip_rect.x = (pcmd->ClipRect.x - clip_off.x) * clip_scale.x;
clip_rect.y = (pcmd->ClipRect.y - clip_off.y) * clip_scale.y;
clip_rect.z = (pcmd->ClipRect.z - clip_off.x) * clip_scale.x;
clip_rect.w = (pcmd->ClipRect.w - clip_off.y) * clip_scale.y;
// Apply scissor/clipping rectangle
const D3D11_RECT r = { (LONG)clip_rect.x, (LONG)clip_rect.y, (LONG)clip_rect.z, (LONG)clip_rect.w };
ctx->RSSetScissorRects(1, &r);
// Bind texture, Draw
TEXTURE* texture = (TEXTURE*)pcmd->TextureId;
ID3D11ShaderResourceView* textureView = texture ? texture->View : nullptr;
ctx->PSSetShaderResources(0, 1, &textureView);
ctx->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
}
}
global_idx_offset += cmd_list->IdxBuffer.Size;
global_vtx_offset += cmd_list->VtxBuffer.Size;
}
// Restore modified DX state
ctx->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
ctx->RSSetViewports(old.ViewportsCount, old.Viewports);
ctx->RSSetState(old.RS); if (old.RS) old.RS->Release();
ctx->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
ctx->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
ctx->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
ctx->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
ctx->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
ctx->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
ctx->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
ctx->IASetPrimitiveTopology(old.PrimitiveTopology);
ctx->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
ctx->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
ctx->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
}
static void ImGui_ImplDX11_CreateFontsTexture()
{
// Build texture atlas
ImGuiIO& io = ImGui::GetIO();
unsigned char* pixels;
int width, height;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
g_pFontTextureID = ImGui_CreateTexture(pixels, width, height);
io.Fonts->TexID = g_pFontTextureID;
// Create texture sampler
{
D3D11_SAMPLER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
desc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
desc.MipLODBias = 0.f;
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
desc.MinLOD = 0.f;
desc.MaxLOD = 0.f;
g_pd3dDevice->CreateSamplerState(&desc, &g_pFontSampler);
}
}
bool ImGui_ImplDX11_CreateDeviceObjects()
{
if (!g_pd3dDevice)
return false;
if (g_pFontSampler)
ImGui_ImplDX11_InvalidateDeviceObjects();
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
// If you would like to use this DX11 sample code but remove this dependency you can:
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
// Create the vertex shader
{
static const char* vertexShader =
"cbuffer vertexBuffer : register(b0) \
{\
float4x4 ProjectionMatrix; \
};\
struct VS_INPUT\
{\
float2 pos : POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
PS_INPUT main(VS_INPUT input)\
{\
PS_INPUT output;\
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
output.col = input.col;\
output.uv = input.uv;\
return output;\
}";
ID3D10Blob* pErrorBlob = NULL;
HRESULT hr = D3DCompile(vertexShader, strlen(vertexShader), NULL, NULL, NULL, "main", "vs_4_0", 0, 0, &g_pVertexShaderBlob, &pErrorBlob);
if (FAILED(hr) || g_pVertexShaderBlob == NULL) // NB: Pass ID3D10Blob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
{
if (pErrorBlob)
{
OutputDebugStringA((const char*)pErrorBlob->GetBufferPointer());
pErrorBlob->Release();
}
return false;
}
if (g_pd3dDevice->CreateVertexShader((DWORD*)g_pVertexShaderBlob->GetBufferPointer(), g_pVertexShaderBlob->GetBufferSize(), NULL, &g_pVertexShader) != S_OK)
return false;
// Create the input layout
D3D11_INPUT_ELEMENT_DESC local_layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (size_t)(&((ImDrawVert*)0)->pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (size_t)(&((ImDrawVert*)0)->uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (size_t)(&((ImDrawVert*)0)->col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
if (g_pd3dDevice->CreateInputLayout(local_layout, 3, g_pVertexShaderBlob->GetBufferPointer(), g_pVertexShaderBlob->GetBufferSize(), &g_pInputLayout) != S_OK)
return false;
// Create the constant buffer
{
D3D11_BUFFER_DESC desc;
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER);
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
g_pd3dDevice->CreateBuffer(&desc, NULL, &g_pVertexConstantBuffer);
}
}
// Create the pixel shader
{
static const char* pixelShader =
"struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
sampler sampler0;\
Texture2D texture0;\
\
float4 main(PS_INPUT input) : SV_Target\
{\
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
out_col.rgb *= out_col.a; \
return out_col; \
}";
ID3D10Blob* pErrorBlob = NULL;
HRESULT hr = D3DCompile(pixelShader, strlen(pixelShader), NULL, NULL, NULL, "main", "ps_4_0", 0, 0, &g_pPixelShaderBlob, &pErrorBlob);
if (FAILED(hr) || g_pPixelShaderBlob == NULL) // NB: Pass ID3D10Blob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
{
if (pErrorBlob)
{
OutputDebugStringA((const char*)pErrorBlob->GetBufferPointer());
pErrorBlob->Release();
}
return false;
}
if (g_pd3dDevice->CreatePixelShader((DWORD*)g_pPixelShaderBlob->GetBufferPointer(), g_pPixelShaderBlob->GetBufferSize(), NULL, &g_pPixelShader) != S_OK)
return false;
}
// Create the blending setup
{
D3D11_BLEND_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.AlphaToCoverageEnable = false;
desc.RenderTarget[0].BlendEnable = true;
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
g_pd3dDevice->CreateBlendState(&desc, &g_pBlendState);
}
// Create the rasterizer state
{
D3D11_RASTERIZER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.FillMode = D3D11_FILL_SOLID;
desc.CullMode = D3D11_CULL_NONE;
desc.ScissorEnable = true;
desc.DepthClipEnable = true;
g_pd3dDevice->CreateRasterizerState(&desc, &g_pRasterizerState);
}
// Create depth-stencil State
{
D3D11_DEPTH_STENCIL_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.DepthEnable = false;
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
desc.StencilEnable = false;
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
desc.BackFace = desc.FrontFace;
g_pd3dDevice->CreateDepthStencilState(&desc, &g_pDepthStencilState);
}
ImGui_ImplDX11_CreateFontsTexture();
for (auto& texture : g_Textures)
ImGui_UploadTexture(texture);
return true;
}
void ImGui_ImplDX11_InvalidateDeviceObjects()
{
if (!g_pd3dDevice)
return;
for (auto& texture : g_Textures)
ImGui_ReleaseTexture(texture);
if (g_pFontTextureID)
{
ImGui_DestroyTexture(g_pFontTextureID);
g_pFontTextureID = NULL;
}
if (g_pFontSampler) { g_pFontSampler->Release(); g_pFontSampler = NULL; }
if (g_pIB) { g_pIB->Release(); g_pIB = NULL; }
if (g_pVB) { g_pVB->Release(); g_pVB = NULL; }
if (g_pBlendState) { g_pBlendState->Release(); g_pBlendState = NULL; }
if (g_pDepthStencilState) { g_pDepthStencilState->Release(); g_pDepthStencilState = NULL; }
if (g_pRasterizerState) { g_pRasterizerState->Release(); g_pRasterizerState = NULL; }
if (g_pPixelShader) { g_pPixelShader->Release(); g_pPixelShader = NULL; }
if (g_pPixelShaderBlob) { g_pPixelShaderBlob->Release(); g_pPixelShaderBlob = NULL; }
if (g_pVertexConstantBuffer) { g_pVertexConstantBuffer->Release(); g_pVertexConstantBuffer = NULL; }
if (g_pInputLayout) { g_pInputLayout->Release(); g_pInputLayout = NULL; }
if (g_pVertexShader) { g_pVertexShader->Release(); g_pVertexShader = NULL; }
if (g_pVertexShaderBlob) { g_pVertexShaderBlob->Release(); g_pVertexShaderBlob = NULL; }
}
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
{
// Setup back-end capabilities flags
ImGuiIO& io = ImGui::GetIO();
io.BackendRendererName = "imgui_impl_dx11";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
// Get factory from device
IDXGIDevice* pDXGIDevice = NULL;
IDXGIAdapter* pDXGIAdapter = NULL;
IDXGIFactory* pFactory = NULL;
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
{
g_pd3dDevice = device;
g_pd3dDeviceContext = device_context;
g_pFactory = pFactory;
}
if (pDXGIDevice) pDXGIDevice->Release();
if (pDXGIAdapter) pDXGIAdapter->Release();
g_pd3dDevice->AddRef();
g_pd3dDeviceContext->AddRef();
g_Textures.reserve(16);
return true;
}
void ImGui_ImplDX11_Shutdown()
{
ImGui_ImplDX11_InvalidateDeviceObjects();
if (g_pFactory) { g_pFactory->Release(); g_pFactory = NULL; }
if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = NULL; }
if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = NULL; }
}
void ImGui_ImplDX11_NewFrame()
{
if (!g_pFontSampler)
ImGui_ImplDX11_CreateDeviceObjects();
}
extern "C" {
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_STATIC
#include "stb_image.h"
}
ImTextureID ImGui_LoadTexture(const char* path)
{
int width = 0, height = 0, component = 0;
if (auto data = stbi_load(path, &width, &height, &component, 4))
{
auto texture = ImGui_CreateTexture(data, width, height);
stbi_image_free(data);
return texture;
}
else
return nullptr;
}
ImTextureID ImGui_CreateTexture(const void* data, int width, int height)
{
auto texture = IM_NEW(TEXTURE);
texture->Width = width;
texture->Height = height;
texture->Data.resize(width * height * 4);
memcpy(texture->Data.Data, data, texture->Data.Size);
if (!ImGui_UploadTexture(texture))
{
IM_DELETE(texture);
return nullptr;
}
g_Textures.push_back(texture);
return (ImTextureID)texture;
}
void ImGui_DestroyTexture(ImTextureID texture)
{
if (!texture)
return;
TEXTURE* texture_object = (TEXTURE*)(texture);
ImGui_ReleaseTexture(texture_object);
for (TEXTURE** it = g_Textures.begin(), **itEnd = g_Textures.end(); it != itEnd; ++it)
{
if (*it == texture_object)
{
g_Textures.erase(it);
break;
}
}
IM_DELETE(texture_object);
}
static bool ImGui_UploadTexture(TEXTURE* texture)
{
if (!g_pd3dDevice || !texture)
return false;
if (texture->View)
return true;
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = texture->Width;
desc.Height = texture->Height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA subResource = {};
subResource.pSysMem = texture->Data.Data;
subResource.SysMemPitch = desc.Width * 4;
subResource.SysMemSlicePitch = 0;
ID3D11Texture2D *pTexture = nullptr;
g_pd3dDevice->CreateTexture2D(&desc, &subResource, &pTexture);
if (!pTexture)
return false;
// Create texture view
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = desc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
g_pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc, &texture->View);
pTexture->Release();
return true;
}
static void ImGui_ReleaseTexture(TEXTURE* texture)
{
if (texture)
{
if (texture->View)
{
texture->View->Release();
texture->View = nullptr;
}
}
}
int ImGui_GetTextureWidth(ImTextureID texture)
{
if (TEXTURE* tex = (TEXTURE*)(texture))
return tex->Width;
else
return 0;
}
int ImGui_GetTextureHeight(ImTextureID texture)
{
if (TEXTURE* tex = (TEXTURE*)(texture))
return tex->Height;
else
return 0;
}

View File

@ -0,0 +1,29 @@
// dear imgui: Renderer for DirectX11
// This needs to be used along with a Platform Binding (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID in imgui.cpp.
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp.
// https://github.com/ocornut/imgui
#pragma once
struct ID3D11Device;
struct ID3D11DeviceContext;
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
// Use if you want to reset your rendering device without losing ImGui state.
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
IMGUI_IMPL_API ImTextureID ImGui_LoadTexture(const char* path);
IMGUI_IMPL_API ImTextureID ImGui_CreateTexture(const void* data, int width, int height);
IMGUI_IMPL_API void ImGui_DestroyTexture(ImTextureID texture);
IMGUI_IMPL_API int ImGui_GetTextureWidth(ImTextureID texture);
IMGUI_IMPL_API int ImGui_GetTextureHeight(ImTextureID texture);

View File

@ -0,0 +1,382 @@
// dear imgui: Platform Binding for GLFW
// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan..)
// (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.)
// (Requires: GLFW 3.1+)
// Implemented features:
// [X] Platform: Clipboard support.
// [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange' (note: the resizing cursors requires GLFW 3.4+).
// [X] Platform: Keyboard arrays indexed using GLFW_KEY_* codes, e.g. ImGui::IsKeyPressed(GLFW_KEY_SPACE).
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp.
// https://github.com/ocornut/imgui
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2020-01-17: Inputs: Disable error callback while assigning mouse cursors because some X11 setup don't have them and it generates errors.
// 2019-12-05: Inputs: Added support for new mouse cursors added in GLFW 3.4+ (resizing cursors, not allowed cursor).
// 2019-10-18: Misc: Previously installed user callbacks are now restored on shutdown.
// 2019-07-21: Inputs: Added mapping for ImGuiKey_KeyPadEnter.
// 2019-05-11: Inputs: Don't filter value from character callback before calling AddInputCharacter().
// 2019-03-12: Misc: Preserve DisplayFramebufferScale when main window is minimized.
// 2018-11-30: Misc: Setting up io.BackendPlatformName so it can be displayed in the About Window.
// 2018-11-07: Inputs: When installing our GLFW callbacks, we save user's previously installed ones - if any - and chain call them.
// 2018-08-01: Inputs: Workaround for Emscripten which doesn't seem to handle focus related calls.
// 2018-06-29: Inputs: Added support for the ImGuiMouseCursor_Hand cursor.
// 2018-06-08: Misc: Extracted imgui_impl_glfw.cpp/.h away from the old combined GLFW+OpenGL/Vulkan examples.
// 2018-03-20: Misc: Setup io.BackendFlags ImGuiBackendFlags_HasMouseCursors flag + honor ImGuiConfigFlags_NoMouseCursorChange flag.
// 2018-02-20: Inputs: Added support for mouse cursors (ImGui::GetMouseCursor() value, passed to glfwSetCursor()).
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
// 2018-02-06: Inputs: Added mapping for ImGuiKey_Space.
// 2018-01-25: Inputs: Added gamepad support if ImGuiConfigFlags_NavEnableGamepad is set.
// 2018-01-25: Inputs: Honoring the io.WantSetMousePos by repositioning the mouse (when using navigation and ImGuiConfigFlags_NavMoveMouse is set).
// 2018-01-20: Inputs: Added Horizontal Mouse Wheel support.
// 2018-01-18: Inputs: Added mapping for ImGuiKey_Insert.
// 2017-08-25: Inputs: MousePos set to -FLT_MAX,-FLT_MAX when mouse is unavailable/missing (instead of -1,-1).
// 2016-10-15: Misc: Added a void* user_data parameter to Clipboard function handlers.
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_extra_keys.h"
// GLFW
#include <GLFW/glfw3.h>
#ifdef _WIN32
#undef APIENTRY
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h> // for glfwGetWin32Window
#endif
#define GLFW_HAS_WINDOW_TOPMOST (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3200) // 3.2+ GLFW_FLOATING
#define GLFW_HAS_WINDOW_HOVERED (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ GLFW_HOVERED
#define GLFW_HAS_WINDOW_ALPHA (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ glfwSetWindowOpacity
#define GLFW_HAS_PER_MONITOR_DPI (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ glfwGetMonitorContentScale
#define GLFW_HAS_VULKAN (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3200) // 3.2+ glfwCreateWindowSurface
#ifdef GLFW_RESIZE_NESW_CURSOR // let's be nice to people who pulled GLFW between 2019-04-16 (3.4 define) and 2019-11-29 (cursors defines) // FIXME: Remove when GLFW 3.4 is released?
#define GLFW_HAS_NEW_CURSORS (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3400) // 3.4+ GLFW_RESIZE_ALL_CURSOR, GLFW_RESIZE_NESW_CURSOR, GLFW_RESIZE_NWSE_CURSOR, GLFW_NOT_ALLOWED_CURSOR
#else
#define GLFW_HAS_NEW_CURSORS (0)
#endif
// Data
enum GlfwClientApi
{
GlfwClientApi_Unknown,
GlfwClientApi_OpenGL,
GlfwClientApi_Vulkan
};
static GLFWwindow* g_Window = NULL; // Main window
static GlfwClientApi g_ClientApi = GlfwClientApi_Unknown;
static double g_Time = 0.0;
static bool g_MouseJustPressed[ImGuiMouseButton_COUNT] = {};
static GLFWcursor* g_MouseCursors[ImGuiMouseCursor_COUNT] = {};
static bool g_InstalledCallbacks = false;
// Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any.
static GLFWmousebuttonfun g_PrevUserCallbackMousebutton = NULL;
static GLFWscrollfun g_PrevUserCallbackScroll = NULL;
static GLFWkeyfun g_PrevUserCallbackKey = NULL;
static GLFWcharfun g_PrevUserCallbackChar = NULL;
static const char* ImGui_ImplGlfw_GetClipboardText(void* user_data)
{
return glfwGetClipboardString((GLFWwindow*)user_data);
}
static void ImGui_ImplGlfw_SetClipboardText(void* user_data, const char* text)
{
glfwSetClipboardString((GLFWwindow*)user_data, text);
}
void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods)
{
if (g_PrevUserCallbackMousebutton != NULL)
g_PrevUserCallbackMousebutton(window, button, action, mods);
if (action == GLFW_PRESS && button >= 0 && button < IM_ARRAYSIZE(g_MouseJustPressed))
g_MouseJustPressed[button] = true;
}
void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
if (g_PrevUserCallbackScroll != NULL)
g_PrevUserCallbackScroll(window, xoffset, yoffset);
ImGuiIO& io = ImGui::GetIO();
io.MouseWheelH += (float)xoffset;
io.MouseWheel += (float)yoffset;
}
void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (g_PrevUserCallbackKey != NULL)
g_PrevUserCallbackKey(window, key, scancode, action, mods);
ImGuiIO& io = ImGui::GetIO();
if (action == GLFW_PRESS)
io.KeysDown[key] = true;
if (action == GLFW_RELEASE)
io.KeysDown[key] = false;
// Modifiers are not reliable across systems
io.KeyCtrl = io.KeysDown[GLFW_KEY_LEFT_CONTROL] || io.KeysDown[GLFW_KEY_RIGHT_CONTROL];
io.KeyShift = io.KeysDown[GLFW_KEY_LEFT_SHIFT] || io.KeysDown[GLFW_KEY_RIGHT_SHIFT];
io.KeyAlt = io.KeysDown[GLFW_KEY_LEFT_ALT] || io.KeysDown[GLFW_KEY_RIGHT_ALT];
#ifdef _WIN32
io.KeySuper = false;
#else
io.KeySuper = io.KeysDown[GLFW_KEY_LEFT_SUPER] || io.KeysDown[GLFW_KEY_RIGHT_SUPER];
#endif
}
void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c)
{
if (g_PrevUserCallbackChar != NULL)
g_PrevUserCallbackChar(window, c);
ImGuiIO& io = ImGui::GetIO();
io.AddInputCharacter(c);
}
static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, GlfwClientApi client_api)
{
g_Window = window;
g_Time = 0.0;
// Setup back-end capabilities flags
ImGuiIO& io = ImGui::GetIO();
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
io.BackendPlatformName = "imgui_impl_glfw";
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
io.KeyMap[ImGuiKey_Tab] = GLFW_KEY_TAB;
io.KeyMap[ImGuiKey_LeftArrow] = GLFW_KEY_LEFT;
io.KeyMap[ImGuiKey_RightArrow] = GLFW_KEY_RIGHT;
io.KeyMap[ImGuiKey_UpArrow] = GLFW_KEY_UP;
io.KeyMap[ImGuiKey_DownArrow] = GLFW_KEY_DOWN;
io.KeyMap[ImGuiKey_PageUp] = GLFW_KEY_PAGE_UP;
io.KeyMap[ImGuiKey_PageDown] = GLFW_KEY_PAGE_DOWN;
io.KeyMap[ImGuiKey_Home] = GLFW_KEY_HOME;
io.KeyMap[ImGuiKey_End] = GLFW_KEY_END;
io.KeyMap[ImGuiKey_Insert] = GLFW_KEY_INSERT;
io.KeyMap[ImGuiKey_Delete] = GLFW_KEY_DELETE;
io.KeyMap[ImGuiKey_Backspace] = GLFW_KEY_BACKSPACE;
io.KeyMap[ImGuiKey_Space] = GLFW_KEY_SPACE;
io.KeyMap[ImGuiKey_Enter] = GLFW_KEY_ENTER;
io.KeyMap[ImGuiKey_Escape] = GLFW_KEY_ESCAPE;
io.KeyMap[ImGuiKey_KeyPadEnter] = GLFW_KEY_KP_ENTER;
io.KeyMap[ImGuiKey_A] = GLFW_KEY_A;
io.KeyMap[ImGuiKey_C] = GLFW_KEY_C;
io.KeyMap[ImGuiKey_V] = GLFW_KEY_V;
io.KeyMap[ImGuiKey_X] = GLFW_KEY_X;
io.KeyMap[ImGuiKey_Y] = GLFW_KEY_Y;
io.KeyMap[ImGuiKey_Z] = GLFW_KEY_Z;
int f_index = GetEnumValueForF();
int d_index = GetEnumValueForD();
if (f_index >= 0)
io.KeyMap[f_index] = GLFW_KEY_F;
if (d_index >= 0)
io.KeyMap[d_index] = GLFW_KEY_D;
io.SetClipboardTextFn = ImGui_ImplGlfw_SetClipboardText;
io.GetClipboardTextFn = ImGui_ImplGlfw_GetClipboardText;
io.ClipboardUserData = g_Window;
#if defined(_WIN32)
//io.ImeWindowHandle = (void*)glfwGetWin32Window(g_Window);
#endif
// Create mouse cursors
// (By design, on X11 cursors are user configurable and some cursors may be missing. When a cursor doesn't exist,
// GLFW will emit an error which will often be printed by the app, so we temporarily disable error reporting.
// Missing cursors will return NULL and our _UpdateMouseCursor() function will use the Arrow cursor instead.)
GLFWerrorfun prev_error_callback = glfwSetErrorCallback(NULL);
g_MouseCursors[ImGuiMouseCursor_Arrow] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
g_MouseCursors[ImGuiMouseCursor_TextInput] = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeNS] = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeEW] = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR);
g_MouseCursors[ImGuiMouseCursor_Hand] = glfwCreateStandardCursor(GLFW_HAND_CURSOR);
#if GLFW_HAS_NEW_CURSORS
g_MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_RESIZE_ALL_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_RESIZE_NESW_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR);
g_MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_NOT_ALLOWED_CURSOR);
#else
g_MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
g_MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
g_MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
#endif
glfwSetErrorCallback(prev_error_callback);
// Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any.
g_PrevUserCallbackMousebutton = NULL;
g_PrevUserCallbackScroll = NULL;
g_PrevUserCallbackKey = NULL;
g_PrevUserCallbackChar = NULL;
if (install_callbacks)
{
g_InstalledCallbacks = true;
g_PrevUserCallbackMousebutton = glfwSetMouseButtonCallback(window, ImGui_ImplGlfw_MouseButtonCallback);
g_PrevUserCallbackScroll = glfwSetScrollCallback(window, ImGui_ImplGlfw_ScrollCallback);
g_PrevUserCallbackKey = glfwSetKeyCallback(window, ImGui_ImplGlfw_KeyCallback);
g_PrevUserCallbackChar = glfwSetCharCallback(window, ImGui_ImplGlfw_CharCallback);
}
g_ClientApi = client_api;
return true;
}
bool ImGui_ImplGlfw_InitForNone(GLFWwindow* window, bool install_callbacks)
{
return ImGui_ImplGlfw_Init(window, install_callbacks, GlfwClientApi_Unknown);
}
bool ImGui_ImplGlfw_InitForOpenGL(GLFWwindow* window, bool install_callbacks)
{
return ImGui_ImplGlfw_Init(window, install_callbacks, GlfwClientApi_OpenGL);
}
bool ImGui_ImplGlfw_InitForVulkan(GLFWwindow* window, bool install_callbacks)
{
return ImGui_ImplGlfw_Init(window, install_callbacks, GlfwClientApi_Vulkan);
}
void ImGui_ImplGlfw_Shutdown()
{
if (g_InstalledCallbacks)
{
glfwSetMouseButtonCallback(g_Window, g_PrevUserCallbackMousebutton);
glfwSetScrollCallback(g_Window, g_PrevUserCallbackScroll);
glfwSetKeyCallback(g_Window, g_PrevUserCallbackKey);
glfwSetCharCallback(g_Window, g_PrevUserCallbackChar);
g_InstalledCallbacks = false;
}
for (ImGuiMouseCursor cursor_n = 0; cursor_n < ImGuiMouseCursor_COUNT; cursor_n++)
{
glfwDestroyCursor(g_MouseCursors[cursor_n]);
g_MouseCursors[cursor_n] = NULL;
}
g_ClientApi = GlfwClientApi_Unknown;
}
static void ImGui_ImplGlfw_UpdateMousePosAndButtons()
{
// Update buttons
ImGuiIO& io = ImGui::GetIO();
for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++)
{
// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
io.MouseDown[i] = g_MouseJustPressed[i] || glfwGetMouseButton(g_Window, i) != 0;
g_MouseJustPressed[i] = false;
}
// Update mouse position
const ImVec2 mouse_pos_backup = io.MousePos;
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
#ifdef __EMSCRIPTEN__
const bool focused = true; // Emscripten
#else
const bool focused = glfwGetWindowAttrib(g_Window, GLFW_FOCUSED) != 0;
#endif
if (focused)
{
if (io.WantSetMousePos)
{
glfwSetCursorPos(g_Window, (double)mouse_pos_backup.x, (double)mouse_pos_backup.y);
}
else
{
double mouse_x, mouse_y;
glfwGetCursorPos(g_Window, &mouse_x, &mouse_y);
io.MousePos = ImVec2((float)mouse_x, (float)mouse_y);
}
}
}
static void ImGui_ImplGlfw_UpdateMouseCursor()
{
ImGuiIO& io = ImGui::GetIO();
if ((io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) || glfwGetInputMode(g_Window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED)
return;
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
{
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
glfwSetInputMode(g_Window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
}
else
{
// Show OS mouse cursor
// FIXME-PLATFORM: Unfocused windows seems to fail changing the mouse cursor with GLFW 3.2, but 3.3 works here.
glfwSetCursor(g_Window, g_MouseCursors[imgui_cursor] ? g_MouseCursors[imgui_cursor] : g_MouseCursors[ImGuiMouseCursor_Arrow]);
glfwSetInputMode(g_Window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
}
static void ImGui_ImplGlfw_UpdateGamepads()
{
ImGuiIO& io = ImGui::GetIO();
memset(io.NavInputs, 0, sizeof(io.NavInputs));
if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0)
return;
// Update gamepad inputs
#define MAP_BUTTON(NAV_NO, BUTTON_NO) { if (buttons_count > BUTTON_NO && buttons[BUTTON_NO] == GLFW_PRESS) io.NavInputs[NAV_NO] = 1.0f; }
#define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float v = (axes_count > AXIS_NO) ? axes[AXIS_NO] : V0; v = (v - V0) / (V1 - V0); if (v > 1.0f) v = 1.0f; if (io.NavInputs[NAV_NO] < v) io.NavInputs[NAV_NO] = v; }
int axes_count = 0, buttons_count = 0;
const float* axes = glfwGetJoystickAxes(GLFW_JOYSTICK_1, &axes_count);
const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count);
MAP_BUTTON(ImGuiNavInput_Activate, 0); // Cross / A
MAP_BUTTON(ImGuiNavInput_Cancel, 1); // Circle / B
MAP_BUTTON(ImGuiNavInput_Menu, 2); // Square / X
MAP_BUTTON(ImGuiNavInput_Input, 3); // Triangle / Y
MAP_BUTTON(ImGuiNavInput_DpadLeft, 13); // D-Pad Left
MAP_BUTTON(ImGuiNavInput_DpadRight, 11); // D-Pad Right
MAP_BUTTON(ImGuiNavInput_DpadUp, 10); // D-Pad Up
MAP_BUTTON(ImGuiNavInput_DpadDown, 12); // D-Pad Down
MAP_BUTTON(ImGuiNavInput_FocusPrev, 4); // L1 / LB
MAP_BUTTON(ImGuiNavInput_FocusNext, 5); // R1 / RB
MAP_BUTTON(ImGuiNavInput_TweakSlow, 4); // L1 / LB
MAP_BUTTON(ImGuiNavInput_TweakFast, 5); // R1 / RB
MAP_ANALOG(ImGuiNavInput_LStickLeft, 0, -0.3f, -0.9f);
MAP_ANALOG(ImGuiNavInput_LStickRight,0, +0.3f, +0.9f);
MAP_ANALOG(ImGuiNavInput_LStickUp, 1, +0.3f, +0.9f);
MAP_ANALOG(ImGuiNavInput_LStickDown, 1, -0.3f, -0.9f);
#undef MAP_BUTTON
#undef MAP_ANALOG
if (axes_count > 0 && buttons_count > 0)
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
else
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
}
void ImGui_ImplGlfw_NewFrame()
{
ImGuiIO& io = ImGui::GetIO();
IM_ASSERT(io.Fonts->IsBuilt() && "Font atlas not built! It is generally built by the renderer back-end. Missing call to renderer _NewFrame() function? e.g. ImGui_ImplOpenGL3_NewFrame().");
// Setup display size (every frame to accommodate for window resizing)
int w, h;
int display_w, display_h;
glfwGetWindowSize(g_Window, &w, &h);
glfwGetFramebufferSize(g_Window, &display_w, &display_h);
io.DisplaySize = ImVec2((float)w, (float)h);
if (w > 0 && h > 0)
io.DisplayFramebufferScale = ImVec2((float)display_w / w, (float)display_h / h);
// Setup time step
double current_time = glfwGetTime();
io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f / 60.0f);
g_Time = current_time;
ImGui_ImplGlfw_UpdateMousePosAndButtons();
ImGui_ImplGlfw_UpdateMouseCursor();
// Update game controllers (if enabled and available)
ImGui_ImplGlfw_UpdateGamepads();
}

View File

@ -0,0 +1,36 @@
// dear imgui: Platform Binding for GLFW
// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan..)
// (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.)
// Implemented features:
// [X] Platform: Clipboard support.
// [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
// [x] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: 3 cursors types are missing from GLFW.
// [X] Platform: Keyboard arrays indexed using GLFW_KEY_* codes, e.g. ImGui::IsKeyPressed(GLFW_KEY_SPACE).
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp.
// https://github.com/ocornut/imgui
// About GLSL version:
// The 'glsl_version' initialization parameter defaults to "#version 150" if NULL.
// Only override if your GL version doesn't handle this GLSL version. Keep NULL if unsure!
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
struct GLFWwindow;
IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForNone(GLFWwindow* window, bool install_callbacks);
IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForOpenGL(GLFWwindow* window, bool install_callbacks);
IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForVulkan(GLFWwindow* window, bool install_callbacks);
IMGUI_IMPL_API void ImGui_ImplGlfw_Shutdown();
IMGUI_IMPL_API void ImGui_ImplGlfw_NewFrame();
// GLFW callbacks
// - When calling Init with 'install_callbacks=true': GLFW callbacks will be installed for you. They will call user's previously installed callbacks, if any.
// - When calling Init with 'install_callbacks=false': GLFW callbacks won't be installed. You will need to call those function yourself from your own GLFW callbacks.
IMGUI_IMPL_API void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods);
IMGUI_IMPL_API void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
IMGUI_IMPL_API void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
IMGUI_IMPL_API void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c);

View File

@ -0,0 +1,947 @@
// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline
// - Desktop GL: 2.x 3.x 4.x
// - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0)
// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..)
// Implemented features:
// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID!
// [x] Renderer: Large meshes support (64k+ vertices) with 16-bit indices (Desktop OpenGL only).
// About WebGL/ES:
// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES.
// - This is done automatically on iOS, Android and Emscripten targets.
// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2023-11-08: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" instead of "libGL.so.1", accomodating for NetBSD systems having only "libGL.so.3" available. (#6983)
// 2023-10-05: OpenGL: Rename symbols in our internal loader so that LTO compilation with another copy of gl3w is possible. (#6875, #6668, #4445)
// 2023-06-20: OpenGL: Fixed erroneous use glGetIntegerv(GL_CONTEXT_PROFILE_MASK) on contexts lower than 3.2. (#6539, #6333)
// 2023-05-09: OpenGL: Support for glBindSampler() backup/restore on ES3. (#6375)
// 2023-04-18: OpenGL: Restore front and back polygon mode separately when supported by context. (#6333)
// 2023-03-23: OpenGL: Properly restoring "no shader program bound" if it was the case prior to running the rendering function. (#6267, #6220, #6224)
// 2023-03-15: OpenGL: Fixed GL loader crash when GL_VERSION returns NULL. (#6154, #4445, #3530)
// 2023-03-06: OpenGL: Fixed restoration of a potentially deleted OpenGL program, by calling glIsProgram(). (#6220, #6224)
// 2022-11-09: OpenGL: Reverted use of glBufferSubData(), too many corruptions issues + old issues seemingly can't be reproed with Intel drivers nowadays (revert 2021-12-15 and 2022-05-23 changes).
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
// 2022-09-27: OpenGL: Added ability to '#define IMGUI_IMPL_OPENGL_DEBUG'.
// 2022-05-23: OpenGL: Reworking 2021-12-15 "Using buffer orphaning" so it only happens on Intel GPU, seems to cause problems otherwise. (#4468, #4825, #4832, #5127).
// 2022-05-13: OpenGL: Fixed state corruption on OpenGL ES 2.0 due to not preserving GL_ELEMENT_ARRAY_BUFFER_BINDING and vertex attribute states.
// 2021-12-15: OpenGL: Using buffer orphaning + glBufferSubData(), seems to fix leaks with multi-viewports with some Intel HD drivers.
// 2021-08-23: OpenGL: Fixed ES 3.0 shader ("#version 300 es") use normal precision floats to avoid wobbly rendering at HD resolutions.
// 2021-08-19: OpenGL: Embed and use our own minimal GL loader (imgui_impl_opengl3_loader.h), removing requirement and support for third-party loader.
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
// 2021-06-25: OpenGL: Use OES_vertex_array extension on Emscripten + backup/restore current state.
// 2021-06-21: OpenGL: Destroy individual vertex/fragment shader objects right after they are linked into the main shader.
// 2021-05-24: OpenGL: Access GL_CLIP_ORIGIN when "GL_ARB_clip_control" extension is detected, inside of just OpenGL 4.5 version.
// 2021-05-19: OpenGL: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
// 2021-04-06: OpenGL: Don't try to read GL_CLIP_ORIGIN unless we're OpenGL 4.5 or greater.
// 2021-02-18: OpenGL: Change blending equation to preserve alpha in output buffer.
// 2021-01-03: OpenGL: Backup, setup and restore GL_STENCIL_TEST state.
// 2020-10-23: OpenGL: Backup, setup and restore GL_PRIMITIVE_RESTART state.
// 2020-10-15: OpenGL: Use glGetString(GL_VERSION) instead of glGetIntegerv(GL_MAJOR_VERSION, ...) when the later returns zero (e.g. Desktop GL 2.x)
// 2020-09-17: OpenGL: Fix to avoid compiling/calling glBindSampler() on ES or pre 3.3 context which have the defines set by a loader.
// 2020-07-10: OpenGL: Added support for glad2 OpenGL loader.
// 2020-05-08: OpenGL: Made default GLSL version 150 (instead of 130) on OSX.
// 2020-04-21: OpenGL: Fixed handling of glClipControl(GL_UPPER_LEFT) by inverting projection matrix.
// 2020-04-12: OpenGL: Fixed context version check mistakenly testing for 4.0+ instead of 3.2+ to enable ImGuiBackendFlags_RendererHasVtxOffset.
// 2020-03-24: OpenGL: Added support for glbinding 2.x OpenGL loader.
// 2020-01-07: OpenGL: Added support for glbinding 3.x OpenGL loader.
// 2019-10-25: OpenGL: Using a combination of GL define and runtime GL version to decide whether to use glDrawElementsBaseVertex(). Fix building with pre-3.2 GL loaders.
// 2019-09-22: OpenGL: Detect default GL loader using __has_include compiler facility.
// 2019-09-16: OpenGL: Tweak initialization code to allow application calling ImGui_ImplOpenGL3_CreateFontsTexture() before the first NewFrame() call.
// 2019-05-29: OpenGL: Desktop GL only: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
// 2019-04-30: OpenGL: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
// 2019-03-29: OpenGL: Not calling glBindBuffer more than necessary in the render loop.
// 2019-03-15: OpenGL: Added a GL call + comments in ImGui_ImplOpenGL3_Init() to detect uninitialized GL function loaders early.
// 2019-03-03: OpenGL: Fix support for ES 2.0 (WebGL 1.0).
// 2019-02-20: OpenGL: Fix for OSX not supporting OpenGL 4.5, we don't try to read GL_CLIP_ORIGIN even if defined by the headers/loader.
// 2019-02-11: OpenGL: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display.
// 2019-02-01: OpenGL: Using GLSL 410 shaders for any version over 410 (e.g. 430, 450).
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
// 2018-11-13: OpenGL: Support for GL 4.5's glClipControl(GL_UPPER_LEFT) / GL_CLIP_ORIGIN.
// 2018-08-29: OpenGL: Added support for more OpenGL loaders: glew and glad, with comments indicative that any loader can be used.
// 2018-08-09: OpenGL: Default to OpenGL ES 3 on iOS and Android. GLSL version default to "#version 300 ES".
// 2018-07-30: OpenGL: Support for GLSL 300 ES and 410 core. Fixes for Emscripten compilation.
// 2018-07-10: OpenGL: Support for more GLSL versions (based on the GLSL version string). Added error output when shaders fail to compile/link.
// 2018-06-08: Misc: Extracted imgui_impl_opengl3.cpp/.h away from the old combined GLFW/SDL+OpenGL3 examples.
// 2018-06-08: OpenGL: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
// 2018-05-25: OpenGL: Removed unnecessary backup/restore of GL_ELEMENT_ARRAY_BUFFER_BINDING since this is part of the VAO state.
// 2018-05-14: OpenGL: Making the call to glBindSampler() optional so 3.2 context won't fail if the function is a nullptr pointer.
// 2018-03-06: OpenGL: Added const char* glsl_version parameter to ImGui_ImplOpenGL3_Init() so user can override the GLSL version e.g. "#version 150".
// 2018-02-23: OpenGL: Create the VAO in the render function so the setup can more easily be used with multiple shared GL context.
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplSdlGL3_RenderDrawData() in the .h file so you can call it yourself.
// 2018-01-07: OpenGL: Changed GLSL shader version from 330 to 150.
// 2017-09-01: OpenGL: Save and restore current bound sampler. Save and restore current polygon mode.
// 2017-05-01: OpenGL: Fixed save and restore of current blend func state.
// 2017-05-01: OpenGL: Fixed save and restore of current GL_ACTIVE_TEXTURE.
// 2016-09-05: OpenGL: Fixed save and restore of current scissor rectangle.
// 2016-07-29: OpenGL: Explicitly setting GL_UNPACK_ROW_LENGTH to reduce issues because SDL changes it. (#752)
//----------------------------------------
// OpenGL GLSL GLSL
// version version string
//----------------------------------------
// 2.0 110 "#version 110"
// 2.1 120 "#version 120"
// 3.0 130 "#version 130"
// 3.1 140 "#version 140"
// 3.2 150 "#version 150"
// 3.3 330 "#version 330 core"
// 4.0 400 "#version 400 core"
// 4.1 410 "#version 410 core"
// 4.2 420 "#version 410 core"
// 4.3 430 "#version 430 core"
// ES 2.0 100 "#version 100" = WebGL 1.0
// ES 3.0 300 "#version 300 es" = WebGL 2.0
//----------------------------------------
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#include <stdint.h> // intptr_t
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
// Clang/GCC warnings with -Weverything
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast
#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used
#pragma clang diagnostic ignored "-Wnonportable-system-include-path"
#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
#endif
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
#pragma GCC diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx'
#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
#endif
// GL includes
#if defined(IMGUI_IMPL_OPENGL_ES2)
#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV))
#include <OpenGLES/ES2/gl.h> // Use GL ES 2
#else
#include <GLES2/gl2.h> // Use GL ES 2
#endif
#if defined(__EMSCRIPTEN__)
#ifndef GL_GLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#endif
#include <GLES2/gl2ext.h>
#endif
#elif defined(IMGUI_IMPL_OPENGL_ES3)
#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV))
#include <OpenGLES/ES3/gl.h> // Use GL ES 3
#else
#include <GLES3/gl3.h> // Use GL ES 3
#endif
#elif !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM)
// Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers.
// Helper libraries are often used for this purpose! Here we are using our own minimal custom loader based on gl3w.
// In the rest of your app/engine, you can use another loader of your choice (gl3w, glew, glad, glbinding, glext, glLoadGen, etc.).
// If you happen to be developing a new feature for this backend (imgui_impl_opengl3.cpp):
// - You may need to regenerate imgui_impl_opengl3_loader.h to add new symbols. See https://github.com/dearimgui/gl3w_stripped
// - You can temporarily use an unstripped version. See https://github.com/dearimgui/gl3w_stripped/releases
// Changes to this backend using new APIs should be accompanied by a regenerated stripped loader version.
#define IMGL3W_IMPL
#include "imgui_impl_opengl3_loader.h"
#endif
// Vertex arrays are not supported on ES2/WebGL1 unless Emscripten which uses an extension
#ifndef IMGUI_IMPL_OPENGL_ES2
#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
#elif defined(__EMSCRIPTEN__)
#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
#define glBindVertexArray glBindVertexArrayOES
#define glGenVertexArrays glGenVertexArraysOES
#define glDeleteVertexArrays glDeleteVertexArraysOES
#define GL_VERTEX_ARRAY_BINDING GL_VERTEX_ARRAY_BINDING_OES
#endif
// Desktop GL 2.0+ has glPolygonMode() which GL ES and WebGL don't have.
#ifdef GL_POLYGON_MODE
#define IMGUI_IMPL_HAS_POLYGON_MODE
#endif
// Desktop GL 3.2+ has glDrawElementsBaseVertex() which GL ES and WebGL don't have.
#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_2)
#define IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET
#endif
// Desktop GL 3.3+ and GL ES 3.0+ have glBindSampler()
#if !defined(IMGUI_IMPL_OPENGL_ES2) && (defined(IMGUI_IMPL_OPENGL_ES3) || defined(GL_VERSION_3_3))
#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
#endif
// Desktop GL 3.1+ has GL_PRIMITIVE_RESTART state
#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_1)
#define IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART
#endif
// Desktop GL use extension detection
#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3)
#define IMGUI_IMPL_OPENGL_MAY_HAVE_EXTENSIONS
#endif
// [Debugging]
//#define IMGUI_IMPL_OPENGL_DEBUG
#ifdef IMGUI_IMPL_OPENGL_DEBUG
#include <stdio.h>
#define GL_CALL(_CALL) do { _CALL; GLenum gl_err = glGetError(); if (gl_err != 0) fprintf(stderr, "GL error 0x%x returned from '%s'.\n", gl_err, #_CALL); } while (0) // Call with error check
#else
#define GL_CALL(_CALL) _CALL // Call without error check
#endif
// OpenGL Data
struct ImGui_ImplOpenGL3_Data
{
GLuint GlVersion; // Extracted at runtime using GL_MAJOR_VERSION, GL_MINOR_VERSION queries (e.g. 320 for GL 3.2)
char GlslVersionString[32]; // Specified by user or detected based on compile time GL settings.
bool GlProfileIsES2;
bool GlProfileIsES3;
bool GlProfileIsCompat;
GLint GlProfileMask;
GLuint FontTexture;
GLuint ShaderHandle;
GLint AttribLocationTex; // Uniforms location
GLint AttribLocationProjMtx;
GLuint AttribLocationVtxPos; // Vertex attributes location
GLuint AttribLocationVtxUV;
GLuint AttribLocationVtxColor;
unsigned int VboHandle, ElementsHandle;
GLsizeiptr VertexBufferSize;
GLsizeiptr IndexBufferSize;
bool HasClipOrigin;
bool UseBufferSubData;
ImGui_ImplOpenGL3_Data() { memset((void*)this, 0, sizeof(*this)); }
};
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
static ImGui_ImplOpenGL3_Data* ImGui_ImplOpenGL3_GetBackendData()
{
return ImGui::GetCurrentContext() ? (ImGui_ImplOpenGL3_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
}
// OpenGL vertex attribute state (for ES 1.0 and ES 2.0 only)
#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
struct ImGui_ImplOpenGL3_VtxAttribState
{
GLint Enabled, Size, Type, Normalized, Stride;
GLvoid* Ptr;
void GetState(GLint index)
{
glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &Enabled);
glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &Size);
glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &Type);
glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &Normalized);
glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &Stride);
glGetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &Ptr);
}
void SetState(GLint index)
{
glVertexAttribPointer(index, Size, Type, (GLboolean)Normalized, Stride, Ptr);
if (Enabled) glEnableVertexAttribArray(index); else glDisableVertexAttribArray(index);
}
};
#endif
// Functions
bool ImGui_ImplOpenGL3_Init(const char* glsl_version)
{
ImGuiIO& io = ImGui::GetIO();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
// Initialize our loader
#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM)
if (imgl3wInit() != 0)
{
fprintf(stderr, "Failed to initialize OpenGL loader!\n");
return false;
}
#endif
// Setup backend capabilities flags
ImGui_ImplOpenGL3_Data* bd = IM_NEW(ImGui_ImplOpenGL3_Data)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "imgui_impl_opengl3";
// Query for GL version (e.g. 320 for GL 3.2)
#if defined(IMGUI_IMPL_OPENGL_ES2)
// GLES 2
bd->GlVersion = 200;
bd->GlProfileIsES2 = true;
#else
// Desktop or GLES 3
GLint major = 0;
GLint minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
if (major == 0 && minor == 0)
{
// Query GL_VERSION in desktop GL 2.x, the string will start with "<major>.<minor>"
const char* gl_version = (const char*)glGetString(GL_VERSION);
sscanf(gl_version, "%d.%d", &major, &minor);
}
bd->GlVersion = (GLuint)(major * 100 + minor * 10);
#if defined(GL_CONTEXT_PROFILE_MASK)
if (bd->GlVersion >= 320)
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &bd->GlProfileMask);
bd->GlProfileIsCompat = (bd->GlProfileMask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) != 0;
#endif
#if defined(IMGUI_IMPL_OPENGL_ES3)
bd->GlProfileIsES3 = true;
#endif
bd->UseBufferSubData = false;
/*
// Query vendor to enable glBufferSubData kludge
#ifdef _WIN32
if (const char* vendor = (const char*)glGetString(GL_VENDOR))
if (strncmp(vendor, "Intel", 5) == 0)
bd->UseBufferSubData = true;
#endif
*/
#endif
#ifdef IMGUI_IMPL_OPENGL_DEBUG
printf("GlVersion = %d\nGlProfileIsCompat = %d\nGlProfileMask = 0x%X\nGlProfileIsES2 = %d, GlProfileIsES3 = %d\nGL_VENDOR = '%s'\nGL_RENDERER = '%s'\n", bd->GlVersion, bd->GlProfileIsCompat, bd->GlProfileMask, bd->GlProfileIsES2, bd->GlProfileIsES3, (const char*)glGetString(GL_VENDOR), (const char*)glGetString(GL_RENDERER)); // [DEBUG]
#endif
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET
if (bd->GlVersion >= 320)
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
#endif
// Store GLSL version string so we can refer to it later in case we recreate shaders.
// Note: GLSL version is NOT the same as GL version. Leave this to nullptr if unsure.
if (glsl_version == nullptr)
{
#if defined(IMGUI_IMPL_OPENGL_ES2)
glsl_version = "#version 100";
#elif defined(IMGUI_IMPL_OPENGL_ES3)
glsl_version = "#version 300 es";
#elif defined(__APPLE__)
glsl_version = "#version 150";
#else
glsl_version = "#version 130";
#endif
}
IM_ASSERT((int)strlen(glsl_version) + 2 < IM_ARRAYSIZE(bd->GlslVersionString));
strcpy(bd->GlslVersionString, glsl_version);
strcat(bd->GlslVersionString, "\n");
// Make an arbitrary GL call (we don't actually need the result)
// IF YOU GET A CRASH HERE: it probably means the OpenGL function loader didn't do its job. Let us know!
GLint current_texture;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &current_texture);
// Detect extensions we support
bd->HasClipOrigin = (bd->GlVersion >= 450);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_EXTENSIONS
GLint num_extensions = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions);
for (GLint i = 0; i < num_extensions; i++)
{
const char* extension = (const char*)glGetStringi(GL_EXTENSIONS, i);
if (extension != nullptr && strcmp(extension, "GL_ARB_clip_control") == 0)
bd->HasClipOrigin = true;
}
#endif
return true;
}
void ImGui_ImplOpenGL3_Shutdown()
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplOpenGL3_DestroyDeviceObjects();
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset;
IM_DELETE(bd);
}
void ImGui_ImplOpenGL3_NewFrame()
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
IM_ASSERT(bd != nullptr && "Did you call ImGui_ImplOpenGL3_Init()?");
if (!bd->ShaderHandle)
ImGui_ImplOpenGL3_CreateDeviceObjects();
}
static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_width, int fb_height, GLuint vertex_array_object)
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glEnable(GL_SCISSOR_TEST);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART
if (bd->GlVersion >= 310)
glDisable(GL_PRIMITIVE_RESTART);
#endif
#ifdef IMGUI_IMPL_HAS_POLYGON_MODE
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif
// Support for GL 4.5 rarely used glClipControl(GL_UPPER_LEFT)
#if defined(GL_CLIP_ORIGIN)
bool clip_origin_lower_left = true;
if (bd->HasClipOrigin)
{
GLenum current_clip_origin = 0; glGetIntegerv(GL_CLIP_ORIGIN, (GLint*)&current_clip_origin);
if (current_clip_origin == GL_UPPER_LEFT)
clip_origin_lower_left = false;
}
#endif
// Setup viewport, orthographic projection matrix
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
GL_CALL(glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height));
float L = draw_data->DisplayPos.x;
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
float T = draw_data->DisplayPos.y;
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
#if defined(GL_CLIP_ORIGIN)
if (!clip_origin_lower_left) { float tmp = T; T = B; B = tmp; } // Swap top and bottom if origin is upper left
#endif
const float ortho_projection[4][4] =
{
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
{ 0.0f, 0.0f, -1.0f, 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), 0.0f, 1.0f },
};
glUseProgram(bd->ShaderHandle);
glUniform1i(bd->AttribLocationTex, 0);
glUniformMatrix4fv(bd->AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
if (bd->GlVersion >= 330 || bd->GlProfileIsES3)
glBindSampler(0, 0); // We use combined texture/sampler state. Applications using GL 3.3 and GL ES 3.0 may set that otherwise.
#endif
(void)vertex_array_object;
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
glBindVertexArray(vertex_array_object);
#endif
// Bind vertex/index buffers and setup attributes for ImDrawVert
GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, bd->VboHandle));
GL_CALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bd->ElementsHandle));
GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxPos));
GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxUV));
GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxColor));
GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos)));
GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv)));
GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col)));
}
// OpenGL3 Render function.
// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly.
// This is in order to be able to run within an OpenGL engine that doesn't do so.
void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data)
{
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x);
int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y);
if (fb_width <= 0 || fb_height <= 0)
return;
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
// Backup GL state
GLenum last_active_texture; glGetIntegerv(GL_ACTIVE_TEXTURE, (GLint*)&last_active_texture);
glActiveTexture(GL_TEXTURE0);
GLuint last_program; glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&last_program);
GLuint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, (GLint*)&last_texture);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
GLuint last_sampler; if (bd->GlVersion >= 330 || bd->GlProfileIsES3) { glGetIntegerv(GL_SAMPLER_BINDING, (GLint*)&last_sampler); } else { last_sampler = 0; }
#endif
GLuint last_array_buffer; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, (GLint*)&last_array_buffer);
#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
// This is part of VAO on OpenGL 3.0+ and OpenGL ES 3.0+.
GLint last_element_array_buffer; glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &last_element_array_buffer);
ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_pos; last_vtx_attrib_state_pos.GetState(bd->AttribLocationVtxPos);
ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_uv; last_vtx_attrib_state_uv.GetState(bd->AttribLocationVtxUV);
ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_color; last_vtx_attrib_state_color.GetState(bd->AttribLocationVtxColor);
#endif
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
GLuint last_vertex_array_object; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, (GLint*)&last_vertex_array_object);
#endif
#ifdef IMGUI_IMPL_HAS_POLYGON_MODE
GLint last_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, last_polygon_mode);
#endif
GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport);
GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box);
GLenum last_blend_src_rgb; glGetIntegerv(GL_BLEND_SRC_RGB, (GLint*)&last_blend_src_rgb);
GLenum last_blend_dst_rgb; glGetIntegerv(GL_BLEND_DST_RGB, (GLint*)&last_blend_dst_rgb);
GLenum last_blend_src_alpha; glGetIntegerv(GL_BLEND_SRC_ALPHA, (GLint*)&last_blend_src_alpha);
GLenum last_blend_dst_alpha; glGetIntegerv(GL_BLEND_DST_ALPHA, (GLint*)&last_blend_dst_alpha);
GLenum last_blend_equation_rgb; glGetIntegerv(GL_BLEND_EQUATION_RGB, (GLint*)&last_blend_equation_rgb);
GLenum last_blend_equation_alpha; glGetIntegerv(GL_BLEND_EQUATION_ALPHA, (GLint*)&last_blend_equation_alpha);
GLboolean last_enable_blend = glIsEnabled(GL_BLEND);
GLboolean last_enable_cull_face = glIsEnabled(GL_CULL_FACE);
GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST);
GLboolean last_enable_stencil_test = glIsEnabled(GL_STENCIL_TEST);
GLboolean last_enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART
GLboolean last_enable_primitive_restart = (bd->GlVersion >= 310) ? glIsEnabled(GL_PRIMITIVE_RESTART) : GL_FALSE;
#endif
// Setup desired GL state
// Recreate the VAO every time (this is to easily allow multiple GL contexts to be rendered to. VAO are not shared among GL contexts)
// The renderer would actually work without any VAO bound, but then our VertexAttrib calls would overwrite the default one currently bound.
GLuint vertex_array_object = 0;
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
GL_CALL(glGenVertexArrays(1, &vertex_array_object));
#endif
ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object);
// Will project scissor/clipping rectangles into framebuffer space
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
// Render command lists
for (int n = 0; n < draw_data->CmdListsCount; n++)
{
const ImDrawList* cmd_list = draw_data->CmdLists[n];
// Upload vertex/index buffers
// - OpenGL drivers are in a very sorry state nowadays....
// During 2021 we attempted to switch from glBufferData() to orphaning+glBufferSubData() following reports
// of leaks on Intel GPU when using multi-viewports on Windows.
// - After this we kept hearing of various display corruptions issues. We started disabling on non-Intel GPU, but issues still got reported on Intel.
// - We are now back to using exclusively glBufferData(). So bd->UseBufferSubData IS ALWAYS FALSE in this code.
// We are keeping the old code path for a while in case people finding new issues may want to test the bd->UseBufferSubData path.
// - See https://github.com/ocornut/imgui/issues/4468 and please report any corruption issues.
const GLsizeiptr vtx_buffer_size = (GLsizeiptr)cmd_list->VtxBuffer.Size * (int)sizeof(ImDrawVert);
const GLsizeiptr idx_buffer_size = (GLsizeiptr)cmd_list->IdxBuffer.Size * (int)sizeof(ImDrawIdx);
if (bd->UseBufferSubData)
{
if (bd->VertexBufferSize < vtx_buffer_size)
{
bd->VertexBufferSize = vtx_buffer_size;
GL_CALL(glBufferData(GL_ARRAY_BUFFER, bd->VertexBufferSize, nullptr, GL_STREAM_DRAW));
}
if (bd->IndexBufferSize < idx_buffer_size)
{
bd->IndexBufferSize = idx_buffer_size;
GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, bd->IndexBufferSize, nullptr, GL_STREAM_DRAW));
}
GL_CALL(glBufferSubData(GL_ARRAY_BUFFER, 0, vtx_buffer_size, (const GLvoid*)cmd_list->VtxBuffer.Data));
GL_CALL(glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, idx_buffer_size, (const GLvoid*)cmd_list->IdxBuffer.Data));
}
else
{
GL_CALL(glBufferData(GL_ARRAY_BUFFER, vtx_buffer_size, (const GLvoid*)cmd_list->VtxBuffer.Data, GL_STREAM_DRAW));
GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx_buffer_size, (const GLvoid*)cmd_list->IdxBuffer.Data, GL_STREAM_DRAW));
}
for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
if (pcmd->UserCallback != nullptr)
{
// User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object);
else
pcmd->UserCallback(cmd_list, pcmd);
}
else
{
// Project scissor/clipping rectangles into framebuffer space
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
continue;
// Apply scissor/clipping rectangle (Y is inverted in OpenGL)
GL_CALL(glScissor((int)clip_min.x, (int)((float)fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y)));
// Bind texture, Draw
GL_CALL(glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID()));
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET
if (bd->GlVersion >= 320)
GL_CALL(glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset));
else
#endif
GL_CALL(glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx))));
}
}
}
// Destroy the temporary VAO
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
GL_CALL(glDeleteVertexArrays(1, &vertex_array_object));
#endif
// Restore modified GL state
// This "glIsProgram()" check is required because if the program is "pending deletion" at the time of binding backup, it will have been deleted by now and will cause an OpenGL error. See #6220.
if (last_program == 0 || glIsProgram(last_program)) glUseProgram(last_program);
glBindTexture(GL_TEXTURE_2D, last_texture);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
if (bd->GlVersion >= 330 || bd->GlProfileIsES3)
glBindSampler(0, last_sampler);
#endif
glActiveTexture(last_active_texture);
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
glBindVertexArray(last_vertex_array_object);
#endif
glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer);
#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer);
last_vtx_attrib_state_pos.SetState(bd->AttribLocationVtxPos);
last_vtx_attrib_state_uv.SetState(bd->AttribLocationVtxUV);
last_vtx_attrib_state_color.SetState(bd->AttribLocationVtxColor);
#endif
glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha);
glBlendFuncSeparate(last_blend_src_rgb, last_blend_dst_rgb, last_blend_src_alpha, last_blend_dst_alpha);
if (last_enable_blend) glEnable(GL_BLEND); else glDisable(GL_BLEND);
if (last_enable_cull_face) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE);
if (last_enable_depth_test) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
if (last_enable_stencil_test) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST);
if (last_enable_scissor_test) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART
if (bd->GlVersion >= 310) { if (last_enable_primitive_restart) glEnable(GL_PRIMITIVE_RESTART); else glDisable(GL_PRIMITIVE_RESTART); }
#endif
#ifdef IMGUI_IMPL_HAS_POLYGON_MODE
// Desktop OpenGL 3.0 and OpenGL 3.1 had separate polygon draw modes for front-facing and back-facing faces of polygons
if (bd->GlVersion <= 310 || bd->GlProfileIsCompat)
{
glPolygonMode(GL_FRONT, (GLenum)last_polygon_mode[0]);
glPolygonMode(GL_BACK, (GLenum)last_polygon_mode[1]);
}
else
{
glPolygonMode(GL_FRONT_AND_BACK, (GLenum)last_polygon_mode[0]);
}
#endif // IMGUI_IMPL_HAS_POLYGON_MODE
glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]);
glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]);
(void)bd; // Not all compilation paths use this
}
bool ImGui_ImplOpenGL3_CreateFontsTexture()
{
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
// Build texture atlas
unsigned char* pixels;
int width, height;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory.
// Upload texture to graphics system
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
GLint last_texture;
GL_CALL(glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture));
GL_CALL(glGenTextures(1, &bd->FontTexture));
GL_CALL(glBindTexture(GL_TEXTURE_2D, bd->FontTexture));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
#ifdef GL_UNPACK_ROW_LENGTH // Not on WebGL/ES
GL_CALL(glPixelStorei(GL_UNPACK_ROW_LENGTH, 0));
#endif
GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
// Store our identifier
io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture);
// Restore state
GL_CALL(glBindTexture(GL_TEXTURE_2D, last_texture));
return true;
}
void ImGui_ImplOpenGL3_DestroyFontsTexture()
{
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
if (bd->FontTexture)
{
glDeleteTextures(1, &bd->FontTexture);
io.Fonts->SetTexID(0);
bd->FontTexture = 0;
}
}
// If you get an error please report on github. You may try different GL context version or GLSL version. See GL<>GLSL version table at the top of this file.
static bool CheckShader(GLuint handle, const char* desc)
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
GLint status = 0, log_length = 0;
glGetShaderiv(handle, GL_COMPILE_STATUS, &status);
glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &log_length);
if ((GLboolean)status == GL_FALSE)
fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to compile %s! With GLSL: %s\n", desc, bd->GlslVersionString);
if (log_length > 1)
{
ImVector<char> buf;
buf.resize((int)(log_length + 1));
glGetShaderInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin());
fprintf(stderr, "%s\n", buf.begin());
}
return (GLboolean)status == GL_TRUE;
}
// If you get an error please report on GitHub. You may try different GL context version or GLSL version.
static bool CheckProgram(GLuint handle, const char* desc)
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
GLint status = 0, log_length = 0;
glGetProgramiv(handle, GL_LINK_STATUS, &status);
glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &log_length);
if ((GLboolean)status == GL_FALSE)
fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to link %s! With GLSL %s\n", desc, bd->GlslVersionString);
if (log_length > 1)
{
ImVector<char> buf;
buf.resize((int)(log_length + 1));
glGetProgramInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin());
fprintf(stderr, "%s\n", buf.begin());
}
return (GLboolean)status == GL_TRUE;
}
bool ImGui_ImplOpenGL3_CreateDeviceObjects()
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
// Backup GL state
GLint last_texture, last_array_buffer;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture);
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer);
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
GLint last_vertex_array;
glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array);
#endif
// Parse GLSL version string
int glsl_version = 130;
sscanf(bd->GlslVersionString, "#version %d", &glsl_version);
const GLchar* vertex_shader_glsl_120 =
"uniform mat4 ProjMtx;\n"
"attribute vec2 Position;\n"
"attribute vec2 UV;\n"
"attribute vec4 Color;\n"
"varying vec2 Frag_UV;\n"
"varying vec4 Frag_Color;\n"
"void main()\n"
"{\n"
" Frag_UV = UV;\n"
" Frag_Color = Color;\n"
" gl_Position = ProjMtx * vec4(Position.xy,0,1);\n"
"}\n";
const GLchar* vertex_shader_glsl_130 =
"uniform mat4 ProjMtx;\n"
"in vec2 Position;\n"
"in vec2 UV;\n"
"in vec4 Color;\n"
"out vec2 Frag_UV;\n"
"out vec4 Frag_Color;\n"
"void main()\n"
"{\n"
" Frag_UV = UV;\n"
" Frag_Color = Color;\n"
" gl_Position = ProjMtx * vec4(Position.xy,0,1);\n"
"}\n";
const GLchar* vertex_shader_glsl_300_es =
"precision highp float;\n"
"layout (location = 0) in vec2 Position;\n"
"layout (location = 1) in vec2 UV;\n"
"layout (location = 2) in vec4 Color;\n"
"uniform mat4 ProjMtx;\n"
"out vec2 Frag_UV;\n"
"out vec4 Frag_Color;\n"
"void main()\n"
"{\n"
" Frag_UV = UV;\n"
" Frag_Color = Color;\n"
" gl_Position = ProjMtx * vec4(Position.xy,0,1);\n"
"}\n";
const GLchar* vertex_shader_glsl_410_core =
"layout (location = 0) in vec2 Position;\n"
"layout (location = 1) in vec2 UV;\n"
"layout (location = 2) in vec4 Color;\n"
"uniform mat4 ProjMtx;\n"
"out vec2 Frag_UV;\n"
"out vec4 Frag_Color;\n"
"void main()\n"
"{\n"
" Frag_UV = UV;\n"
" Frag_Color = Color;\n"
" gl_Position = ProjMtx * vec4(Position.xy,0,1);\n"
"}\n";
const GLchar* fragment_shader_glsl_120 =
"#ifdef GL_ES\n"
" precision mediump float;\n"
"#endif\n"
"uniform sampler2D Texture;\n"
"varying vec2 Frag_UV;\n"
"varying vec4 Frag_Color;\n"
"void main()\n"
"{\n"
" gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);\n"
"}\n";
const GLchar* fragment_shader_glsl_130 =
"uniform sampler2D Texture;\n"
"in vec2 Frag_UV;\n"
"in vec4 Frag_Color;\n"
"out vec4 Out_Color;\n"
"void main()\n"
"{\n"
" Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n"
"}\n";
const GLchar* fragment_shader_glsl_300_es =
"precision mediump float;\n"
"uniform sampler2D Texture;\n"
"in vec2 Frag_UV;\n"
"in vec4 Frag_Color;\n"
"layout (location = 0) out vec4 Out_Color;\n"
"void main()\n"
"{\n"
" Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n"
"}\n";
const GLchar* fragment_shader_glsl_410_core =
"in vec2 Frag_UV;\n"
"in vec4 Frag_Color;\n"
"uniform sampler2D Texture;\n"
"layout (location = 0) out vec4 Out_Color;\n"
"void main()\n"
"{\n"
" Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n"
"}\n";
// Select shaders matching our GLSL versions
const GLchar* vertex_shader = nullptr;
const GLchar* fragment_shader = nullptr;
if (glsl_version < 130)
{
vertex_shader = vertex_shader_glsl_120;
fragment_shader = fragment_shader_glsl_120;
}
else if (glsl_version >= 410)
{
vertex_shader = vertex_shader_glsl_410_core;
fragment_shader = fragment_shader_glsl_410_core;
}
else if (glsl_version == 300)
{
vertex_shader = vertex_shader_glsl_300_es;
fragment_shader = fragment_shader_glsl_300_es;
}
else
{
vertex_shader = vertex_shader_glsl_130;
fragment_shader = fragment_shader_glsl_130;
}
// Create shaders
const GLchar* vertex_shader_with_version[2] = { bd->GlslVersionString, vertex_shader };
GLuint vert_handle = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vert_handle, 2, vertex_shader_with_version, nullptr);
glCompileShader(vert_handle);
CheckShader(vert_handle, "vertex shader");
const GLchar* fragment_shader_with_version[2] = { bd->GlslVersionString, fragment_shader };
GLuint frag_handle = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(frag_handle, 2, fragment_shader_with_version, nullptr);
glCompileShader(frag_handle);
CheckShader(frag_handle, "fragment shader");
// Link
bd->ShaderHandle = glCreateProgram();
glAttachShader(bd->ShaderHandle, vert_handle);
glAttachShader(bd->ShaderHandle, frag_handle);
glLinkProgram(bd->ShaderHandle);
CheckProgram(bd->ShaderHandle, "shader program");
glDetachShader(bd->ShaderHandle, vert_handle);
glDetachShader(bd->ShaderHandle, frag_handle);
glDeleteShader(vert_handle);
glDeleteShader(frag_handle);
bd->AttribLocationTex = glGetUniformLocation(bd->ShaderHandle, "Texture");
bd->AttribLocationProjMtx = glGetUniformLocation(bd->ShaderHandle, "ProjMtx");
bd->AttribLocationVtxPos = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Position");
bd->AttribLocationVtxUV = (GLuint)glGetAttribLocation(bd->ShaderHandle, "UV");
bd->AttribLocationVtxColor = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Color");
// Create buffers
glGenBuffers(1, &bd->VboHandle);
glGenBuffers(1, &bd->ElementsHandle);
ImGui_ImplOpenGL3_CreateFontsTexture();
// Restore modified GL state
glBindTexture(GL_TEXTURE_2D, last_texture);
glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer);
#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY
glBindVertexArray(last_vertex_array);
#endif
return true;
}
void ImGui_ImplOpenGL3_DestroyDeviceObjects()
{
ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
if (bd->VboHandle) { glDeleteBuffers(1, &bd->VboHandle); bd->VboHandle = 0; }
if (bd->ElementsHandle) { glDeleteBuffers(1, &bd->ElementsHandle); bd->ElementsHandle = 0; }
if (bd->ShaderHandle) { glDeleteProgram(bd->ShaderHandle); bd->ShaderHandle = 0; }
ImGui_ImplOpenGL3_DestroyFontsTexture();
}
//-----------------------------------------------------------------------------
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
#endif // #ifndef IMGUI_DISABLE

View File

@ -0,0 +1,66 @@
// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline
// - Desktop GL: 2.x 3.x 4.x
// - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0)
// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..)
// Implemented features:
// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID!
// [x] Renderer: Large meshes support (64k+ vertices) with 16-bit indices (Desktop OpenGL only).
// About WebGL/ES:
// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES.
// - This is done automatically on iOS, Android and Emscripten targets.
// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
// About GLSL version:
// The 'glsl_version' initialization parameter should be nullptr (default) or a "#version XXX" string.
// On computer platform the GLSL version default to "#version 130". On OpenGL ES 3 platform it defaults to "#version 300 es"
// Only override if your GL version doesn't handle this GLSL version. See GLSL version table at the top of imgui_impl_opengl3.cpp.
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
// Backend API
IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = nullptr);
IMGUI_IMPL_API void ImGui_ImplOpenGL3_Shutdown();
IMGUI_IMPL_API void ImGui_ImplOpenGL3_NewFrame();
IMGUI_IMPL_API void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data);
// (Optional) Called by Init/NewFrame/Shutdown
IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateFontsTexture();
IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyFontsTexture();
IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateDeviceObjects();
IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects();
// Specific OpenGL ES versions
//#define IMGUI_IMPL_OPENGL_ES2 // Auto-detected on Emscripten
//#define IMGUI_IMPL_OPENGL_ES3 // Auto-detected on iOS/Android
// You can explicitly select GLES2 or GLES3 API by using one of the '#define IMGUI_IMPL_OPENGL_LOADER_XXX' in imconfig.h or compiler command-line.
#if !defined(IMGUI_IMPL_OPENGL_ES2) \
&& !defined(IMGUI_IMPL_OPENGL_ES3)
// Try to detect GLES on matching platforms
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) || (defined(__ANDROID__))
#define IMGUI_IMPL_OPENGL_ES3 // iOS, Android -> GL ES 3, "#version 300 es"
#elif defined(__EMSCRIPTEN__) || defined(__amigaos4__)
#define IMGUI_IMPL_OPENGL_ES2 // Emscripten -> GL ES 2, "#version 100"
#else
// Otherwise imgui_impl_opengl3_loader.h will be used.
#endif
#endif
#endif // #ifndef IMGUI_DISABLE

View File

@ -0,0 +1,810 @@
//-----------------------------------------------------------------------------
// About imgui_impl_opengl3_loader.h:
//
// We embed our own OpenGL loader to not require user to provide their own or to have to use ours,
// which proved to be endless problems for users.
// Our loader is custom-generated, based on gl3w but automatically filtered to only include
// enums/functions that we use in our imgui_impl_opengl3.cpp source file in order to be small.
//
// YOU SHOULD NOT NEED TO INCLUDE/USE THIS DIRECTLY. THIS IS USED BY imgui_impl_opengl3.cpp ONLY.
// THE REST OF YOUR APP SHOULD USE A DIFFERENT GL LOADER: ANY GL LOADER OF YOUR CHOICE.
//
// IF YOU GET BUILD ERRORS IN THIS FILE (commonly macro redefinitions or function redefinitions):
// IT LIKELY MEANS THAT YOU ARE BUILDING 'imgui_impl_opengl3.cpp' OR INCUDING 'imgui_impl_opengl3_loader.h'
// IN THE SAME COMPILATION UNIT AS ONE OF YOUR FILE WHICH IS USING A THIRD-PARTY OPENGL LOADER.
// (e.g. COULD HAPPEN IF YOU ARE DOING A UNITY/JUMBO BUILD, OR INCLUDING .CPP FILES FROM OTHERS)
// YOU SHOULD NOT BUILD BOTH IN THE SAME COMPILATION UNIT.
// BUT IF YOU REALLY WANT TO, you can '#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM' and imgui_impl_opengl3.cpp
// WILL NOT BE USING OUR LOADER, AND INSTEAD EXPECT ANOTHER/YOUR LOADER TO BE AVAILABLE IN THE COMPILATION UNIT.
//
// Regenerate with:
// python gl3w_gen.py --output ../imgui/backends/imgui_impl_opengl3_loader.h --ref ../imgui/backends/imgui_impl_opengl3.cpp ./extra_symbols.txt
//
// More info:
// https://github.com/dearimgui/gl3w_stripped
// https://github.com/ocornut/imgui/issues/4445
//-----------------------------------------------------------------------------
/*
* This file was generated with gl3w_gen.py, part of imgl3w
* (hosted at https://github.com/dearimgui/gl3w_stripped)
*
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* 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 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.
*/
#ifndef __gl3w_h_
#define __gl3w_h_
// Adapted from KHR/khrplatform.h to avoid including entire file.
#ifndef __khrplatform_h_
typedef float khronos_float_t;
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
#ifdef _WIN64
typedef signed long long int khronos_intptr_t;
typedef signed long long int khronos_ssize_t;
#else
typedef signed long int khronos_intptr_t;
typedef signed long int khronos_ssize_t;
#endif
#if defined(_MSC_VER) && !defined(__clang__)
typedef signed __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#elif (defined(__clang__) || defined(__GNUC__)) && (__cplusplus < 201100)
#include <stdint.h>
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#else
typedef signed long long khronos_int64_t;
typedef unsigned long long khronos_uint64_t;
#endif
#endif // __khrplatform_h_
#ifndef __gl_glcorearb_h_
#define __gl_glcorearb_h_ 1
#ifdef __cplusplus
extern "C" {
#endif
/*
** Copyright 2013-2020 The Khronos Group Inc.
** SPDX-License-Identifier: MIT
**
** This header is generated from the Khronos OpenGL / OpenGL ES XML
** API Registry. The current version of the Registry, generator scripts
** used to make the header, and the header can be found at
** https://github.com/KhronosGroup/OpenGL-Registry
*/
#if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
#endif
#ifndef APIENTRY
#define APIENTRY
#endif
#ifndef APIENTRYP
#define APIENTRYP APIENTRY *
#endif
#ifndef GLAPI
#define GLAPI extern
#endif
/* glcorearb.h is for use with OpenGL core profile implementations.
** It should should be placed in the same directory as gl.h and
** included as <GL/glcorearb.h>.
**
** glcorearb.h includes only APIs in the latest OpenGL core profile
** implementation together with APIs in newer ARB extensions which
** can be supported by the core profile. It does not, and never will
** include functionality removed from the core profile, such as
** fixed-function vertex and fragment processing.
**
** Do not #include both <GL/glcorearb.h> and either of <GL/gl.h> or
** <GL/glext.h> in the same source file.
*/
/* Generated C header for:
* API: gl
* Profile: core
* Versions considered: .*
* Versions emitted: .*
* Default extensions included: glcore
* Additional extensions included: _nomatch_^
* Extensions removed: _nomatch_^
*/
#ifndef GL_VERSION_1_0
typedef void GLvoid;
typedef unsigned int GLenum;
typedef khronos_float_t GLfloat;
typedef int GLint;
typedef int GLsizei;
typedef unsigned int GLbitfield;
typedef double GLdouble;
typedef unsigned int GLuint;
typedef unsigned char GLboolean;
typedef khronos_uint8_t GLubyte;
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_FALSE 0
#define GL_TRUE 1
#define GL_TRIANGLES 0x0004
#define GL_ONE 1
#define GL_SRC_ALPHA 0x0302
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
#define GL_FRONT 0x0404
#define GL_BACK 0x0405
#define GL_FRONT_AND_BACK 0x0408
#define GL_POLYGON_MODE 0x0B40
#define GL_CULL_FACE 0x0B44
#define GL_DEPTH_TEST 0x0B71
#define GL_STENCIL_TEST 0x0B90
#define GL_VIEWPORT 0x0BA2
#define GL_BLEND 0x0BE2
#define GL_SCISSOR_BOX 0x0C10
#define GL_SCISSOR_TEST 0x0C11
#define GL_UNPACK_ROW_LENGTH 0x0CF2
#define GL_PACK_ALIGNMENT 0x0D05
#define GL_TEXTURE_2D 0x0DE1
#define GL_UNSIGNED_BYTE 0x1401
#define GL_UNSIGNED_SHORT 0x1403
#define GL_UNSIGNED_INT 0x1405
#define GL_FLOAT 0x1406
#define GL_RGBA 0x1908
#define GL_FILL 0x1B02
#define GL_VENDOR 0x1F00
#define GL_RENDERER 0x1F01
#define GL_VERSION 0x1F02
#define GL_EXTENSIONS 0x1F03
#define GL_LINEAR 0x2601
#define GL_TEXTURE_MAG_FILTER 0x2800
#define GL_TEXTURE_MIN_FILTER 0x2801
typedef void (APIENTRYP PFNGLPOLYGONMODEPROC) (GLenum face, GLenum mode);
typedef void (APIENTRYP PFNGLSCISSORPROC) (GLint x, GLint y, GLsizei width, GLsizei height);
typedef void (APIENTRYP PFNGLTEXPARAMETERIPROC) (GLenum target, GLenum pname, GLint param);
typedef void (APIENTRYP PFNGLTEXIMAGE2DPROC) (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels);
typedef void (APIENTRYP PFNGLCLEARPROC) (GLbitfield mask);
typedef void (APIENTRYP PFNGLCLEARCOLORPROC) (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
typedef void (APIENTRYP PFNGLDISABLEPROC) (GLenum cap);
typedef void (APIENTRYP PFNGLENABLEPROC) (GLenum cap);
typedef void (APIENTRYP PFNGLFLUSHPROC) (void);
typedef void (APIENTRYP PFNGLPIXELSTOREIPROC) (GLenum pname, GLint param);
typedef void (APIENTRYP PFNGLREADPIXELSPROC) (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels);
typedef GLenum (APIENTRYP PFNGLGETERRORPROC) (void);
typedef void (APIENTRYP PFNGLGETINTEGERVPROC) (GLenum pname, GLint *data);
typedef const GLubyte *(APIENTRYP PFNGLGETSTRINGPROC) (GLenum name);
typedef GLboolean (APIENTRYP PFNGLISENABLEDPROC) (GLenum cap);
typedef void (APIENTRYP PFNGLVIEWPORTPROC) (GLint x, GLint y, GLsizei width, GLsizei height);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glPolygonMode (GLenum face, GLenum mode);
GLAPI void APIENTRY glScissor (GLint x, GLint y, GLsizei width, GLsizei height);
GLAPI void APIENTRY glTexParameteri (GLenum target, GLenum pname, GLint param);
GLAPI void APIENTRY glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels);
GLAPI void APIENTRY glClear (GLbitfield mask);
GLAPI void APIENTRY glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
GLAPI void APIENTRY glDisable (GLenum cap);
GLAPI void APIENTRY glEnable (GLenum cap);
GLAPI void APIENTRY glFlush (void);
GLAPI void APIENTRY glPixelStorei (GLenum pname, GLint param);
GLAPI void APIENTRY glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels);
GLAPI GLenum APIENTRY glGetError (void);
GLAPI void APIENTRY glGetIntegerv (GLenum pname, GLint *data);
GLAPI const GLubyte *APIENTRY glGetString (GLenum name);
GLAPI GLboolean APIENTRY glIsEnabled (GLenum cap);
GLAPI void APIENTRY glViewport (GLint x, GLint y, GLsizei width, GLsizei height);
#endif
#endif /* GL_VERSION_1_0 */
#ifndef GL_VERSION_1_1
typedef khronos_float_t GLclampf;
typedef double GLclampd;
#define GL_TEXTURE_BINDING_2D 0x8069
typedef void (APIENTRYP PFNGLDRAWELEMENTSPROC) (GLenum mode, GLsizei count, GLenum type, const void *indices);
typedef void (APIENTRYP PFNGLBINDTEXTUREPROC) (GLenum target, GLuint texture);
typedef void (APIENTRYP PFNGLDELETETEXTURESPROC) (GLsizei n, const GLuint *textures);
typedef void (APIENTRYP PFNGLGENTEXTURESPROC) (GLsizei n, GLuint *textures);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glDrawElements (GLenum mode, GLsizei count, GLenum type, const void *indices);
GLAPI void APIENTRY glBindTexture (GLenum target, GLuint texture);
GLAPI void APIENTRY glDeleteTextures (GLsizei n, const GLuint *textures);
GLAPI void APIENTRY glGenTextures (GLsizei n, GLuint *textures);
#endif
#endif /* GL_VERSION_1_1 */
#ifndef GL_VERSION_1_3
#define GL_TEXTURE0 0x84C0
#define GL_ACTIVE_TEXTURE 0x84E0
typedef void (APIENTRYP PFNGLACTIVETEXTUREPROC) (GLenum texture);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glActiveTexture (GLenum texture);
#endif
#endif /* GL_VERSION_1_3 */
#ifndef GL_VERSION_1_4
#define GL_BLEND_DST_RGB 0x80C8
#define GL_BLEND_SRC_RGB 0x80C9
#define GL_BLEND_DST_ALPHA 0x80CA
#define GL_BLEND_SRC_ALPHA 0x80CB
#define GL_FUNC_ADD 0x8006
typedef void (APIENTRYP PFNGLBLENDFUNCSEPARATEPROC) (GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
typedef void (APIENTRYP PFNGLBLENDEQUATIONPROC) (GLenum mode);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glBlendFuncSeparate (GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
GLAPI void APIENTRY glBlendEquation (GLenum mode);
#endif
#endif /* GL_VERSION_1_4 */
#ifndef GL_VERSION_1_5
typedef khronos_ssize_t GLsizeiptr;
typedef khronos_intptr_t GLintptr;
#define GL_ARRAY_BUFFER 0x8892
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
#define GL_ARRAY_BUFFER_BINDING 0x8894
#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895
#define GL_STREAM_DRAW 0x88E0
typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const void *data, GLenum usage);
typedef void (APIENTRYP PFNGLBUFFERSUBDATAPROC) (GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glBindBuffer (GLenum target, GLuint buffer);
GLAPI void APIENTRY glDeleteBuffers (GLsizei n, const GLuint *buffers);
GLAPI void APIENTRY glGenBuffers (GLsizei n, GLuint *buffers);
GLAPI void APIENTRY glBufferData (GLenum target, GLsizeiptr size, const void *data, GLenum usage);
GLAPI void APIENTRY glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
#endif
#endif /* GL_VERSION_1_5 */
#ifndef GL_VERSION_2_0
typedef char GLchar;
typedef khronos_int16_t GLshort;
typedef khronos_int8_t GLbyte;
typedef khronos_uint16_t GLushort;
#define GL_BLEND_EQUATION_RGB 0x8009
#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622
#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623
#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624
#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625
#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645
#define GL_BLEND_EQUATION_ALPHA 0x883D
#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_VERTEX_SHADER 0x8B31
#define GL_COMPILE_STATUS 0x8B81
#define GL_LINK_STATUS 0x8B82
#define GL_INFO_LOG_LENGTH 0x8B84
#define GL_CURRENT_PROGRAM 0x8B8D
#define GL_UPPER_LEFT 0x8CA2
typedef void (APIENTRYP PFNGLBLENDEQUATIONSEPARATEPROC) (GLenum modeRGB, GLenum modeAlpha);
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
typedef GLuint (APIENTRYP PFNGLCREATESHADERPROC) (GLenum type);
typedef void (APIENTRYP PFNGLDELETEPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLDELETESHADERPROC) (GLuint shader);
typedef void (APIENTRYP PFNGLDETACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRYP PFNGLDISABLEVERTEXATTRIBARRAYPROC) (GLuint index);
typedef void (APIENTRYP PFNGLENABLEVERTEXATTRIBARRAYPROC) (GLuint index);
typedef GLint (APIENTRYP PFNGLGETATTRIBLOCATIONPROC) (GLuint program, const GLchar *name);
typedef void (APIENTRYP PFNGLGETPROGRAMIVPROC) (GLuint program, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name);
typedef void (APIENTRYP PFNGLGETVERTEXATTRIBIVPROC) (GLuint index, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLGETVERTEXATTRIBPOINTERVPROC) (GLuint index, GLenum pname, void **pointer);
typedef GLboolean (APIENTRYP PFNGLISPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length);
typedef void (APIENTRYP PFNGLUSEPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLUNIFORM1IPROC) (GLint location, GLint v0);
typedef void (APIENTRYP PFNGLUNIFORMMATRIX4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
typedef void (APIENTRYP PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glBlendEquationSeparate (GLenum modeRGB, GLenum modeAlpha);
GLAPI void APIENTRY glAttachShader (GLuint program, GLuint shader);
GLAPI void APIENTRY glCompileShader (GLuint shader);
GLAPI GLuint APIENTRY glCreateProgram (void);
GLAPI GLuint APIENTRY glCreateShader (GLenum type);
GLAPI void APIENTRY glDeleteProgram (GLuint program);
GLAPI void APIENTRY glDeleteShader (GLuint shader);
GLAPI void APIENTRY glDetachShader (GLuint program, GLuint shader);
GLAPI void APIENTRY glDisableVertexAttribArray (GLuint index);
GLAPI void APIENTRY glEnableVertexAttribArray (GLuint index);
GLAPI GLint APIENTRY glGetAttribLocation (GLuint program, const GLchar *name);
GLAPI void APIENTRY glGetProgramiv (GLuint program, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetProgramInfoLog (GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
GLAPI void APIENTRY glGetShaderiv (GLuint shader, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetShaderInfoLog (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
GLAPI GLint APIENTRY glGetUniformLocation (GLuint program, const GLchar *name);
GLAPI void APIENTRY glGetVertexAttribiv (GLuint index, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetVertexAttribPointerv (GLuint index, GLenum pname, void **pointer);
GLAPI GLboolean APIENTRY glIsProgram (GLuint program);
GLAPI void APIENTRY glLinkProgram (GLuint program);
GLAPI void APIENTRY glShaderSource (GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length);
GLAPI void APIENTRY glUseProgram (GLuint program);
GLAPI void APIENTRY glUniform1i (GLint location, GLint v0);
GLAPI void APIENTRY glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
GLAPI void APIENTRY glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
#endif
#endif /* GL_VERSION_2_0 */
#ifndef GL_VERSION_3_0
typedef khronos_uint16_t GLhalf;
#define GL_MAJOR_VERSION 0x821B
#define GL_MINOR_VERSION 0x821C
#define GL_NUM_EXTENSIONS 0x821D
#define GL_FRAMEBUFFER_SRGB 0x8DB9
#define GL_VERTEX_ARRAY_BINDING 0x85B5
typedef void (APIENTRYP PFNGLGETBOOLEANI_VPROC) (GLenum target, GLuint index, GLboolean *data);
typedef void (APIENTRYP PFNGLGETINTEGERI_VPROC) (GLenum target, GLuint index, GLint *data);
typedef const GLubyte *(APIENTRYP PFNGLGETSTRINGIPROC) (GLenum name, GLuint index);
typedef void (APIENTRYP PFNGLBINDVERTEXARRAYPROC) (GLuint array);
typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays);
typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI const GLubyte *APIENTRY glGetStringi (GLenum name, GLuint index);
GLAPI void APIENTRY glBindVertexArray (GLuint array);
GLAPI void APIENTRY glDeleteVertexArrays (GLsizei n, const GLuint *arrays);
GLAPI void APIENTRY glGenVertexArrays (GLsizei n, GLuint *arrays);
#endif
#endif /* GL_VERSION_3_0 */
#ifndef GL_VERSION_3_1
#define GL_VERSION_3_1 1
#define GL_PRIMITIVE_RESTART 0x8F9D
#endif /* GL_VERSION_3_1 */
#ifndef GL_VERSION_3_2
#define GL_VERSION_3_2 1
typedef struct __GLsync *GLsync;
typedef khronos_uint64_t GLuint64;
typedef khronos_int64_t GLint64;
#define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002
#define GL_CONTEXT_PROFILE_MASK 0x9126
typedef void (APIENTRYP PFNGLDRAWELEMENTSBASEVERTEXPROC) (GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex);
typedef void (APIENTRYP PFNGLGETINTEGER64I_VPROC) (GLenum target, GLuint index, GLint64 *data);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glDrawElementsBaseVertex (GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex);
#endif
#endif /* GL_VERSION_3_2 */
#ifndef GL_VERSION_3_3
#define GL_VERSION_3_3 1
#define GL_SAMPLER_BINDING 0x8919
typedef void (APIENTRYP PFNGLBINDSAMPLERPROC) (GLuint unit, GLuint sampler);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glBindSampler (GLuint unit, GLuint sampler);
#endif
#endif /* GL_VERSION_3_3 */
#ifndef GL_VERSION_4_1
typedef void (APIENTRYP PFNGLGETFLOATI_VPROC) (GLenum target, GLuint index, GLfloat *data);
typedef void (APIENTRYP PFNGLGETDOUBLEI_VPROC) (GLenum target, GLuint index, GLdouble *data);
#endif /* GL_VERSION_4_1 */
#ifndef GL_VERSION_4_3
typedef void (APIENTRY *GLDEBUGPROC)(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam);
#endif /* GL_VERSION_4_3 */
#ifndef GL_VERSION_4_5
#define GL_CLIP_ORIGIN 0x935C
typedef void (APIENTRYP PFNGLGETTRANSFORMFEEDBACKI_VPROC) (GLuint xfb, GLenum pname, GLuint index, GLint *param);
typedef void (APIENTRYP PFNGLGETTRANSFORMFEEDBACKI64_VPROC) (GLuint xfb, GLenum pname, GLuint index, GLint64 *param);
#endif /* GL_VERSION_4_5 */
#ifndef GL_ARB_bindless_texture
typedef khronos_uint64_t GLuint64EXT;
#endif /* GL_ARB_bindless_texture */
#ifndef GL_ARB_cl_event
struct _cl_context;
struct _cl_event;
#endif /* GL_ARB_cl_event */
#ifndef GL_ARB_clip_control
#define GL_ARB_clip_control 1
#endif /* GL_ARB_clip_control */
#ifndef GL_ARB_debug_output
typedef void (APIENTRY *GLDEBUGPROCARB)(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam);
#endif /* GL_ARB_debug_output */
#ifndef GL_EXT_EGL_image_storage
typedef void *GLeglImageOES;
#endif /* GL_EXT_EGL_image_storage */
#ifndef GL_EXT_direct_state_access
typedef void (APIENTRYP PFNGLGETFLOATI_VEXTPROC) (GLenum pname, GLuint index, GLfloat *params);
typedef void (APIENTRYP PFNGLGETDOUBLEI_VEXTPROC) (GLenum pname, GLuint index, GLdouble *params);
typedef void (APIENTRYP PFNGLGETPOINTERI_VEXTPROC) (GLenum pname, GLuint index, void **params);
typedef void (APIENTRYP PFNGLGETVERTEXARRAYINTEGERI_VEXTPROC) (GLuint vaobj, GLuint index, GLenum pname, GLint *param);
typedef void (APIENTRYP PFNGLGETVERTEXARRAYPOINTERI_VEXTPROC) (GLuint vaobj, GLuint index, GLenum pname, void **param);
#endif /* GL_EXT_direct_state_access */
#ifndef GL_NV_draw_vulkan_image
typedef void (APIENTRY *GLVULKANPROCNV)(void);
#endif /* GL_NV_draw_vulkan_image */
#ifndef GL_NV_gpu_shader5
typedef khronos_int64_t GLint64EXT;
#endif /* GL_NV_gpu_shader5 */
#ifndef GL_NV_vertex_buffer_unified_memory
typedef void (APIENTRYP PFNGLGETINTEGERUI64I_VNVPROC) (GLenum value, GLuint index, GLuint64EXT *result);
#endif /* GL_NV_vertex_buffer_unified_memory */
#ifdef __cplusplus
}
#endif
#endif
#ifndef GL3W_API
#define GL3W_API
#endif
#ifndef __gl_h_
#define __gl_h_
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define GL3W_OK 0
#define GL3W_ERROR_INIT -1
#define GL3W_ERROR_LIBRARY_OPEN -2
#define GL3W_ERROR_OPENGL_VERSION -3
typedef void (*GL3WglProc)(void);
typedef GL3WglProc (*GL3WGetProcAddressProc)(const char *proc);
/* gl3w api */
GL3W_API int imgl3wInit(void);
GL3W_API int imgl3wInit2(GL3WGetProcAddressProc proc);
GL3W_API int imgl3wIsSupported(int major, int minor);
GL3W_API GL3WglProc imgl3wGetProcAddress(const char *proc);
/* gl3w internal state */
union ImGL3WProcs {
GL3WglProc ptr[59];
struct {
PFNGLACTIVETEXTUREPROC ActiveTexture;
PFNGLATTACHSHADERPROC AttachShader;
PFNGLBINDBUFFERPROC BindBuffer;
PFNGLBINDSAMPLERPROC BindSampler;
PFNGLBINDTEXTUREPROC BindTexture;
PFNGLBINDVERTEXARRAYPROC BindVertexArray;
PFNGLBLENDEQUATIONPROC BlendEquation;
PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate;
PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate;
PFNGLBUFFERDATAPROC BufferData;
PFNGLBUFFERSUBDATAPROC BufferSubData;
PFNGLCLEARPROC Clear;
PFNGLCLEARCOLORPROC ClearColor;
PFNGLCOMPILESHADERPROC CompileShader;
PFNGLCREATEPROGRAMPROC CreateProgram;
PFNGLCREATESHADERPROC CreateShader;
PFNGLDELETEBUFFERSPROC DeleteBuffers;
PFNGLDELETEPROGRAMPROC DeleteProgram;
PFNGLDELETESHADERPROC DeleteShader;
PFNGLDELETETEXTURESPROC DeleteTextures;
PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays;
PFNGLDETACHSHADERPROC DetachShader;
PFNGLDISABLEPROC Disable;
PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray;
PFNGLDRAWELEMENTSPROC DrawElements;
PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex;
PFNGLENABLEPROC Enable;
PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray;
PFNGLFLUSHPROC Flush;
PFNGLGENBUFFERSPROC GenBuffers;
PFNGLGENTEXTURESPROC GenTextures;
PFNGLGENVERTEXARRAYSPROC GenVertexArrays;
PFNGLGETATTRIBLOCATIONPROC GetAttribLocation;
PFNGLGETERRORPROC GetError;
PFNGLGETINTEGERVPROC GetIntegerv;
PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog;
PFNGLGETPROGRAMIVPROC GetProgramiv;
PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog;
PFNGLGETSHADERIVPROC GetShaderiv;
PFNGLGETSTRINGPROC GetString;
PFNGLGETSTRINGIPROC GetStringi;
PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation;
PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv;
PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv;
PFNGLISENABLEDPROC IsEnabled;
PFNGLISPROGRAMPROC IsProgram;
PFNGLLINKPROGRAMPROC LinkProgram;
PFNGLPIXELSTOREIPROC PixelStorei;
PFNGLPOLYGONMODEPROC PolygonMode;
PFNGLREADPIXELSPROC ReadPixels;
PFNGLSCISSORPROC Scissor;
PFNGLSHADERSOURCEPROC ShaderSource;
PFNGLTEXIMAGE2DPROC TexImage2D;
PFNGLTEXPARAMETERIPROC TexParameteri;
PFNGLUNIFORM1IPROC Uniform1i;
PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv;
PFNGLUSEPROGRAMPROC UseProgram;
PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer;
PFNGLVIEWPORTPROC Viewport;
} gl;
};
GL3W_API extern union ImGL3WProcs imgl3wProcs;
/* OpenGL functions */
#define glActiveTexture imgl3wProcs.gl.ActiveTexture
#define glAttachShader imgl3wProcs.gl.AttachShader
#define glBindBuffer imgl3wProcs.gl.BindBuffer
#define glBindSampler imgl3wProcs.gl.BindSampler
#define glBindTexture imgl3wProcs.gl.BindTexture
#define glBindVertexArray imgl3wProcs.gl.BindVertexArray
#define glBlendEquation imgl3wProcs.gl.BlendEquation
#define glBlendEquationSeparate imgl3wProcs.gl.BlendEquationSeparate
#define glBlendFuncSeparate imgl3wProcs.gl.BlendFuncSeparate
#define glBufferData imgl3wProcs.gl.BufferData
#define glBufferSubData imgl3wProcs.gl.BufferSubData
#define glClear imgl3wProcs.gl.Clear
#define glClearColor imgl3wProcs.gl.ClearColor
#define glCompileShader imgl3wProcs.gl.CompileShader
#define glCreateProgram imgl3wProcs.gl.CreateProgram
#define glCreateShader imgl3wProcs.gl.CreateShader
#define glDeleteBuffers imgl3wProcs.gl.DeleteBuffers
#define glDeleteProgram imgl3wProcs.gl.DeleteProgram
#define glDeleteShader imgl3wProcs.gl.DeleteShader
#define glDeleteTextures imgl3wProcs.gl.DeleteTextures
#define glDeleteVertexArrays imgl3wProcs.gl.DeleteVertexArrays
#define glDetachShader imgl3wProcs.gl.DetachShader
#define glDisable imgl3wProcs.gl.Disable
#define glDisableVertexAttribArray imgl3wProcs.gl.DisableVertexAttribArray
#define glDrawElements imgl3wProcs.gl.DrawElements
#define glDrawElementsBaseVertex imgl3wProcs.gl.DrawElementsBaseVertex
#define glEnable imgl3wProcs.gl.Enable
#define glEnableVertexAttribArray imgl3wProcs.gl.EnableVertexAttribArray
#define glFlush imgl3wProcs.gl.Flush
#define glGenBuffers imgl3wProcs.gl.GenBuffers
#define glGenTextures imgl3wProcs.gl.GenTextures
#define glGenVertexArrays imgl3wProcs.gl.GenVertexArrays
#define glGetAttribLocation imgl3wProcs.gl.GetAttribLocation
#define glGetError imgl3wProcs.gl.GetError
#define glGetIntegerv imgl3wProcs.gl.GetIntegerv
#define glGetProgramInfoLog imgl3wProcs.gl.GetProgramInfoLog
#define glGetProgramiv imgl3wProcs.gl.GetProgramiv
#define glGetShaderInfoLog imgl3wProcs.gl.GetShaderInfoLog
#define glGetShaderiv imgl3wProcs.gl.GetShaderiv
#define glGetString imgl3wProcs.gl.GetString
#define glGetStringi imgl3wProcs.gl.GetStringi
#define glGetUniformLocation imgl3wProcs.gl.GetUniformLocation
#define glGetVertexAttribPointerv imgl3wProcs.gl.GetVertexAttribPointerv
#define glGetVertexAttribiv imgl3wProcs.gl.GetVertexAttribiv
#define glIsEnabled imgl3wProcs.gl.IsEnabled
#define glIsProgram imgl3wProcs.gl.IsProgram
#define glLinkProgram imgl3wProcs.gl.LinkProgram
#define glPixelStorei imgl3wProcs.gl.PixelStorei
#define glPolygonMode imgl3wProcs.gl.PolygonMode
#define glReadPixels imgl3wProcs.gl.ReadPixels
#define glScissor imgl3wProcs.gl.Scissor
#define glShaderSource imgl3wProcs.gl.ShaderSource
#define glTexImage2D imgl3wProcs.gl.TexImage2D
#define glTexParameteri imgl3wProcs.gl.TexParameteri
#define glUniform1i imgl3wProcs.gl.Uniform1i
#define glUniformMatrix4fv imgl3wProcs.gl.UniformMatrix4fv
#define glUseProgram imgl3wProcs.gl.UseProgram
#define glVertexAttribPointer imgl3wProcs.gl.VertexAttribPointer
#define glViewport imgl3wProcs.gl.Viewport
#ifdef __cplusplus
}
#endif
#endif
#ifdef IMGL3W_IMPL
#ifdef __cplusplus
extern "C" {
#endif
#include <stdlib.h>
#define GL3W_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
static HMODULE libgl;
typedef PROC(__stdcall* GL3WglGetProcAddr)(LPCSTR);
static GL3WglGetProcAddr wgl_get_proc_address;
static int open_libgl(void)
{
libgl = LoadLibraryA("opengl32.dll");
if (!libgl)
return GL3W_ERROR_LIBRARY_OPEN;
wgl_get_proc_address = (GL3WglGetProcAddr)GetProcAddress(libgl, "wglGetProcAddress");
return GL3W_OK;
}
static void close_libgl(void) { FreeLibrary(libgl); }
static GL3WglProc get_proc(const char *proc)
{
GL3WglProc res;
res = (GL3WglProc)wgl_get_proc_address(proc);
if (!res)
res = (GL3WglProc)GetProcAddress(libgl, proc);
return res;
}
#elif defined(__APPLE__)
#include <dlfcn.h>
static void *libgl;
static int open_libgl(void)
{
libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", RTLD_LAZY | RTLD_LOCAL);
if (!libgl)
return GL3W_ERROR_LIBRARY_OPEN;
return GL3W_OK;
}
static void close_libgl(void) { dlclose(libgl); }
static GL3WglProc get_proc(const char *proc)
{
GL3WglProc res;
*(void **)(&res) = dlsym(libgl, proc);
return res;
}
#else
#include <dlfcn.h>
static void *libgl;
static GL3WglProc (*glx_get_proc_address)(const GLubyte *);
static int open_libgl(void)
{
// While most systems use libGL.so.1, NetBSD seems to use that libGL.so.3. See https://github.com/ocornut/imgui/issues/6983
libgl = dlopen("libGL.so", RTLD_LAZY | RTLD_LOCAL);
if (!libgl)
return GL3W_ERROR_LIBRARY_OPEN;
*(void **)(&glx_get_proc_address) = dlsym(libgl, "glXGetProcAddressARB");
return GL3W_OK;
}
static void close_libgl(void) { dlclose(libgl); }
static GL3WglProc get_proc(const char *proc)
{
GL3WglProc res;
res = glx_get_proc_address((const GLubyte *)proc);
if (!res)
*(void **)(&res) = dlsym(libgl, proc);
return res;
}
#endif
static struct { int major, minor; } version;
static int parse_version(void)
{
if (!glGetIntegerv)
return GL3W_ERROR_INIT;
glGetIntegerv(GL_MAJOR_VERSION, &version.major);
glGetIntegerv(GL_MINOR_VERSION, &version.minor);
if (version.major == 0 && version.minor == 0)
{
// Query GL_VERSION in desktop GL 2.x, the string will start with "<major>.<minor>"
if (const char* gl_version = (const char*)glGetString(GL_VERSION))
sscanf(gl_version, "%d.%d", &version.major, &version.minor);
}
if (version.major < 2)
return GL3W_ERROR_OPENGL_VERSION;
return GL3W_OK;
}
static void load_procs(GL3WGetProcAddressProc proc);
int imgl3wInit(void)
{
int res = open_libgl();
if (res)
return res;
atexit(close_libgl);
return imgl3wInit2(get_proc);
}
int imgl3wInit2(GL3WGetProcAddressProc proc)
{
load_procs(proc);
return parse_version();
}
int imgl3wIsSupported(int major, int minor)
{
if (major < 2)
return 0;
if (version.major == major)
return version.minor >= minor;
return version.major >= major;
}
GL3WglProc imgl3wGetProcAddress(const char *proc) { return get_proc(proc); }
static const char *proc_names[] = {
"glActiveTexture",
"glAttachShader",
"glBindBuffer",
"glBindSampler",
"glBindTexture",
"glBindVertexArray",
"glBlendEquation",
"glBlendEquationSeparate",
"glBlendFuncSeparate",
"glBufferData",
"glBufferSubData",
"glClear",
"glClearColor",
"glCompileShader",
"glCreateProgram",
"glCreateShader",
"glDeleteBuffers",
"glDeleteProgram",
"glDeleteShader",
"glDeleteTextures",
"glDeleteVertexArrays",
"glDetachShader",
"glDisable",
"glDisableVertexAttribArray",
"glDrawElements",
"glDrawElementsBaseVertex",
"glEnable",
"glEnableVertexAttribArray",
"glFlush",
"glGenBuffers",
"glGenTextures",
"glGenVertexArrays",
"glGetAttribLocation",
"glGetError",
"glGetIntegerv",
"glGetProgramInfoLog",
"glGetProgramiv",
"glGetShaderInfoLog",
"glGetShaderiv",
"glGetString",
"glGetStringi",
"glGetUniformLocation",
"glGetVertexAttribPointerv",
"glGetVertexAttribiv",
"glIsEnabled",
"glIsProgram",
"glLinkProgram",
"glPixelStorei",
"glPolygonMode",
"glReadPixels",
"glScissor",
"glShaderSource",
"glTexImage2D",
"glTexParameteri",
"glUniform1i",
"glUniformMatrix4fv",
"glUseProgram",
"glVertexAttribPointer",
"glViewport",
};
GL3W_API union ImGL3WProcs imgl3wProcs;
static void load_procs(GL3WGetProcAddressProc proc)
{
size_t i;
for (i = 0; i < GL3W_ARRAY_SIZE(proc_names); i++)
imgl3wProcs.ptr[i] = proc(proc_names[i]);
}
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,462 @@
// dear imgui: Platform Binding for Windows (standard windows API for 32 and 64 bits applications)
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
// Implemented features:
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
// [X] Platform: Keyboard arrays indexed using VK_* Virtual Key Codes, e.g. ImGui::IsKeyPressed(VK_SPACE).
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
#include "imgui.h"
#include "imgui_impl_win32.h"
#include "imgui_extra_keys.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <tchar.h>
// Using XInput library for gamepad (with recent Windows SDK this may leads to executables which won't run on Windows 7)
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
#include <XInput.h>
#else
#define IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT
#endif
#if defined(_MSC_VER) && !defined(IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT)
#pragma comment(lib, "xinput")
//#pragma comment(lib, "Xinput9_1_0")
#endif
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2020-03-03: Inputs: Calling AddInputCharacterUTF16() to support surrogate pairs leading to codepoint >= 0x10000 (for more complete CJK inputs)
// 2020-02-17: Added ImGui_ImplWin32_EnableDpiAwareness(), ImGui_ImplWin32_GetDpiScaleForHwnd(), ImGui_ImplWin32_GetDpiScaleForMonitor() helper functions.
// 2020-01-14: Inputs: Added support for #define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD/IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT.
// 2019-12-05: Inputs: Added support for ImGuiMouseCursor_NotAllowed mouse cursor.
// 2019-05-11: Inputs: Don't filter value from WM_CHAR before calling AddInputCharacter().
// 2019-01-17: Misc: Using GetForegroundWindow()+IsChild() instead of GetActiveWindow() to be compatible with windows created in a different thread or parent.
// 2019-01-17: Inputs: Added support for mouse buttons 4 and 5 via WM_XBUTTON* messages.
// 2019-01-15: Inputs: Added support for XInput gamepads (if ImGuiConfigFlags_NavEnableGamepad is set by user application).
// 2018-11-30: Misc: Setting up io.BackendPlatformName so it can be displayed in the About Window.
// 2018-06-29: Inputs: Added support for the ImGuiMouseCursor_Hand cursor.
// 2018-06-10: Inputs: Fixed handling of mouse wheel messages to support fine position messages (typically sent by track-pads).
// 2018-06-08: Misc: Extracted imgui_impl_win32.cpp/.h away from the old combined DX9/DX10/DX11/DX12 examples.
// 2018-03-20: Misc: Setup io.BackendFlags ImGuiBackendFlags_HasMouseCursors and ImGuiBackendFlags_HasSetMousePos flags + honor ImGuiConfigFlags_NoMouseCursorChange flag.
// 2018-02-20: Inputs: Added support for mouse cursors (ImGui::GetMouseCursor() value and WM_SETCURSOR message handling).
// 2018-02-06: Inputs: Added mapping for ImGuiKey_Space.
// 2018-02-06: Inputs: Honoring the io.WantSetMousePos by repositioning the mouse (when using navigation and ImGuiConfigFlags_NavMoveMouse is set).
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
// 2018-01-20: Inputs: Added Horizontal Mouse Wheel support.
// 2018-01-08: Inputs: Added mapping for ImGuiKey_Insert.
// 2018-01-05: Inputs: Added WM_LBUTTONDBLCLK double-click handlers for window classes with the CS_DBLCLKS flag.
// 2017-10-23: Inputs: Added WM_SYSKEYDOWN / WM_SYSKEYUP handlers so e.g. the VK_MENU key can be read.
// 2017-10-23: Inputs: Using Win32 ::SetCapture/::GetCapture() to retrieve mouse positions outside the client area when dragging.
// 2016-11-12: Inputs: Only call Win32 ::SetCursor(NULL) when io.MouseDrawCursor is set.
// Win32 Data
static HWND g_hWnd = NULL;
static INT64 g_Time = 0;
static INT64 g_TicksPerSecond = 0;
static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT;
static bool g_HasGamepad = false;
static bool g_WantUpdateHasGamepad = true;
// Functions
bool ImGui_ImplWin32_Init(void* hwnd)
{
if (!::QueryPerformanceFrequency((LARGE_INTEGER*)&g_TicksPerSecond))
return false;
if (!::QueryPerformanceCounter((LARGE_INTEGER*)&g_Time))
return false;
// Setup back-end capabilities flags
g_hWnd = (HWND)hwnd;
ImGuiIO& io = ImGui::GetIO();
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
io.BackendPlatformName = "imgui_impl_win32";
//io.ImeWindowHandle = hwnd;
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array that we will update during the application lifetime.
io.KeyMap[ImGuiKey_Tab] = VK_TAB;
io.KeyMap[ImGuiKey_LeftArrow] = VK_LEFT;
io.KeyMap[ImGuiKey_RightArrow] = VK_RIGHT;
io.KeyMap[ImGuiKey_UpArrow] = VK_UP;
io.KeyMap[ImGuiKey_DownArrow] = VK_DOWN;
io.KeyMap[ImGuiKey_PageUp] = VK_PRIOR;
io.KeyMap[ImGuiKey_PageDown] = VK_NEXT;
io.KeyMap[ImGuiKey_Home] = VK_HOME;
io.KeyMap[ImGuiKey_End] = VK_END;
io.KeyMap[ImGuiKey_Insert] = VK_INSERT;
io.KeyMap[ImGuiKey_Delete] = VK_DELETE;
io.KeyMap[ImGuiKey_Backspace] = VK_BACK;
io.KeyMap[ImGuiKey_Space] = VK_SPACE;
io.KeyMap[ImGuiKey_Enter] = VK_RETURN;
io.KeyMap[ImGuiKey_Escape] = VK_ESCAPE;
# if defined(IMGUI_VERSION_NUM) && (IMGUI_VERSION_NUM >= 18604)
io.KeyMap[ImGuiKey_KeypadEnter] = VK_RETURN;
# else
io.KeyMap[ImGuiKey_KeyPadEnter] = VK_RETURN;
# endif
io.KeyMap[ImGuiKey_A] = 'A';
io.KeyMap[ImGuiKey_C] = 'C';
io.KeyMap[ImGuiKey_V] = 'V';
io.KeyMap[ImGuiKey_X] = 'X';
io.KeyMap[ImGuiKey_Y] = 'Y';
io.KeyMap[ImGuiKey_Z] = 'Z';
int f_index = GetEnumValueForF();
int d_index = GetEnumValueForD();
if (f_index >= 0)
io.KeyMap[f_index] = 'F';
if (d_index >= 0)
io.KeyMap[d_index] = 'D';
return true;
}
void ImGui_ImplWin32_Shutdown()
{
g_hWnd = (HWND)0;
}
static bool ImGui_ImplWin32_UpdateMouseCursor()
{
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
return false;
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
{
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
::SetCursor(NULL);
}
else
{
// Show OS mouse cursor
LPTSTR win32_cursor = IDC_ARROW;
switch (imgui_cursor)
{
case ImGuiMouseCursor_Arrow: win32_cursor = IDC_ARROW; break;
case ImGuiMouseCursor_TextInput: win32_cursor = IDC_IBEAM; break;
case ImGuiMouseCursor_ResizeAll: win32_cursor = IDC_SIZEALL; break;
case ImGuiMouseCursor_ResizeEW: win32_cursor = IDC_SIZEWE; break;
case ImGuiMouseCursor_ResizeNS: win32_cursor = IDC_SIZENS; break;
case ImGuiMouseCursor_ResizeNESW: win32_cursor = IDC_SIZENESW; break;
case ImGuiMouseCursor_ResizeNWSE: win32_cursor = IDC_SIZENWSE; break;
case ImGuiMouseCursor_Hand: win32_cursor = IDC_HAND; break;
case ImGuiMouseCursor_NotAllowed: win32_cursor = IDC_NO; break;
}
::SetCursor(::LoadCursor(NULL, win32_cursor));
}
return true;
}
static void ImGui_ImplWin32_UpdateMousePos()
{
ImGuiIO& io = ImGui::GetIO();
// Set OS mouse position if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
if (io.WantSetMousePos)
{
POINT pos = { (int)(io.MousePos.x), (int)(io.MousePos.y) };
::ClientToScreen(g_hWnd, &pos);
::SetCursorPos(pos.x, pos.y);
}
// Set mouse position
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
POINT pos;
if (HWND active_window = ::GetForegroundWindow())
if (active_window == g_hWnd || ::IsChild(active_window, g_hWnd))
if (::GetCursorPos(&pos) && ::ScreenToClient(g_hWnd, &pos))
io.MousePos = ImVec2((float)pos.x, (float)pos.y);
}
// Gamepad navigation mapping
static void ImGui_ImplWin32_UpdateGamepads()
{
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
ImGuiIO& io = ImGui::GetIO();
memset(io.NavInputs, 0, sizeof(io.NavInputs));
if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0)
return;
// Calling XInputGetState() every frame on disconnected gamepads is unfortunately too slow.
// Instead we refresh gamepad availability by calling XInputGetCapabilities() _only_ after receiving WM_DEVICECHANGE.
if (g_WantUpdateHasGamepad)
{
XINPUT_CAPABILITIES caps;
g_HasGamepad = (XInputGetCapabilities(0, XINPUT_FLAG_GAMEPAD, &caps) == ERROR_SUCCESS);
g_WantUpdateHasGamepad = false;
}
XINPUT_STATE xinput_state;
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
if (g_HasGamepad && XInputGetState(0, &xinput_state) == ERROR_SUCCESS)
{
const XINPUT_GAMEPAD& gamepad = xinput_state.Gamepad;
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
#define MAP_BUTTON(NAV_NO, BUTTON_ENUM) { io.NavInputs[NAV_NO] = (gamepad.wButtons & BUTTON_ENUM) ? 1.0f : 0.0f; }
#define MAP_ANALOG(NAV_NO, VALUE, V0, V1) { float vn = (float)(VALUE - V0) / (float)(V1 - V0); if (vn > 1.0f) vn = 1.0f; if (vn > 0.0f && io.NavInputs[NAV_NO] < vn) io.NavInputs[NAV_NO] = vn; }
MAP_BUTTON(ImGuiNavInput_Activate, XINPUT_GAMEPAD_A); // Cross / A
MAP_BUTTON(ImGuiNavInput_Cancel, XINPUT_GAMEPAD_B); // Circle / B
MAP_BUTTON(ImGuiNavInput_Menu, XINPUT_GAMEPAD_X); // Square / X
MAP_BUTTON(ImGuiNavInput_Input, XINPUT_GAMEPAD_Y); // Triangle / Y
MAP_BUTTON(ImGuiNavInput_DpadLeft, XINPUT_GAMEPAD_DPAD_LEFT); // D-Pad Left
MAP_BUTTON(ImGuiNavInput_DpadRight, XINPUT_GAMEPAD_DPAD_RIGHT); // D-Pad Right
MAP_BUTTON(ImGuiNavInput_DpadUp, XINPUT_GAMEPAD_DPAD_UP); // D-Pad Up
MAP_BUTTON(ImGuiNavInput_DpadDown, XINPUT_GAMEPAD_DPAD_DOWN); // D-Pad Down
MAP_BUTTON(ImGuiNavInput_FocusPrev, XINPUT_GAMEPAD_LEFT_SHOULDER); // L1 / LB
MAP_BUTTON(ImGuiNavInput_FocusNext, XINPUT_GAMEPAD_RIGHT_SHOULDER); // R1 / RB
MAP_BUTTON(ImGuiNavInput_TweakSlow, XINPUT_GAMEPAD_LEFT_SHOULDER); // L1 / LB
MAP_BUTTON(ImGuiNavInput_TweakFast, XINPUT_GAMEPAD_RIGHT_SHOULDER); // R1 / RB
MAP_ANALOG(ImGuiNavInput_LStickLeft, gamepad.sThumbLX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
MAP_ANALOG(ImGuiNavInput_LStickRight, gamepad.sThumbLX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
MAP_ANALOG(ImGuiNavInput_LStickUp, gamepad.sThumbLY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
MAP_ANALOG(ImGuiNavInput_LStickDown, gamepad.sThumbLY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32767);
#undef MAP_BUTTON
#undef MAP_ANALOG
}
#endif // #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
}
void ImGui_ImplWin32_NewFrame()
{
ImGuiIO& io = ImGui::GetIO();
IM_ASSERT(io.Fonts->IsBuilt() && "Font atlas not built! It is generally built by the renderer back-end. Missing call to renderer _NewFrame() function? e.g. ImGui_ImplOpenGL3_NewFrame().");
// Setup display size (every frame to accommodate for window resizing)
RECT rect;
::GetClientRect(g_hWnd, &rect);
io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
// Setup time step
INT64 current_time;
::QueryPerformanceCounter((LARGE_INTEGER*)&current_time);
io.DeltaTime = (float)(current_time - g_Time) / g_TicksPerSecond;
g_Time = current_time;
// Read keyboard modifiers inputs
io.KeyCtrl = (::GetKeyState(VK_CONTROL) & 0x8000) != 0;
io.KeyShift = (::GetKeyState(VK_SHIFT) & 0x8000) != 0;
io.KeyAlt = (::GetKeyState(VK_MENU) & 0x8000) != 0;
io.KeySuper = false;
// io.KeysDown[], io.MousePos, io.MouseDown[], io.MouseWheel: filled by the WndProc handler below.
// Update OS mouse position
ImGui_ImplWin32_UpdateMousePos();
// Update OS mouse cursor with the cursor requested by imgui
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
if (g_LastMouseCursor != mouse_cursor)
{
g_LastMouseCursor = mouse_cursor;
ImGui_ImplWin32_UpdateMouseCursor();
}
// Update game controllers (if enabled and available)
ImGui_ImplWin32_UpdateGamepads();
}
// Allow compilation with old Windows SDK. MinGW doesn't have default _WIN32_WINNT/WINVER versions.
#ifndef WM_MOUSEHWHEEL
#define WM_MOUSEHWHEEL 0x020E
#endif
#ifndef DBT_DEVNODES_CHANGED
#define DBT_DEVNODES_CHANGED 0x0007
#endif
// Win32 message handler (process Win32 mouse/keyboard inputs, etc.)
// Call from your application's message handler.
// When implementing your own back-end, you can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if Dear ImGui wants to use your inputs.
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
// Generally you may always pass all inputs to Dear ImGui, and hide them from your application based on those two flags.
// PS: In this Win32 handler, we use the capture API (GetCapture/SetCapture/ReleaseCapture) to be able to read mouse coordinates when dragging mouse outside of our window bounds.
// PS: We treat DBLCLK messages as regular mouse down messages, so this code will work on windows classes that have the CS_DBLCLKS flag set. Our own example app code doesn't set this flag.
#if 0
// Copy this line into your .cpp file to forward declare the function.
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
#endif
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (ImGui::GetCurrentContext() == NULL)
return 0;
ImGuiIO& io = ImGui::GetIO();
switch (msg)
{
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK:
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK:
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK:
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK:
{
int button = 0;
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; }
if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; }
if (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONDBLCLK) { button = 2; }
if (msg == WM_XBUTTONDOWN || msg == WM_XBUTTONDBLCLK) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
if (!ImGui::IsAnyMouseDown() && ::GetCapture() == NULL)
::SetCapture(hwnd);
io.MouseDown[button] = true;
return 0;
}
case WM_LBUTTONUP:
case WM_RBUTTONUP:
case WM_MBUTTONUP:
case WM_XBUTTONUP:
{
int button = 0;
if (msg == WM_LBUTTONUP) { button = 0; }
if (msg == WM_RBUTTONUP) { button = 1; }
if (msg == WM_MBUTTONUP) { button = 2; }
if (msg == WM_XBUTTONUP) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
io.MouseDown[button] = false;
if (!ImGui::IsAnyMouseDown() && ::GetCapture() == hwnd)
::ReleaseCapture();
return 0;
}
case WM_MOUSEWHEEL:
io.MouseWheel += (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
return 0;
case WM_MOUSEHWHEEL:
io.MouseWheelH += (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (wParam < 256)
io.KeysDown[wParam] = 1;
return 0;
case WM_KEYUP:
case WM_SYSKEYUP:
if (wParam < 256)
io.KeysDown[wParam] = 0;
return 0;
case WM_CHAR:
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
if (wParam > 0 && wParam < 0x10000)
io.AddInputCharacterUTF16((unsigned short)wParam);
return 0;
case WM_SETCURSOR:
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor())
return 1;
return 0;
case WM_DEVICECHANGE:
if ((UINT)wParam == DBT_DEVNODES_CHANGED)
g_WantUpdateHasGamepad = true;
return 0;
case WM_KILLFOCUS:
for (int n = 0; n < IM_ARRAYSIZE(io.KeysDown); n++)
io.KeysDown[n] = false;
break;
}
return 0;
}
//--------------------------------------------------------------------------------------------------------
// DPI-related helpers (optional)
//--------------------------------------------------------------------------------------------------------
// - Use to enable DPI awareness without having to create an application manifest.
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
//---------------------------------------------------------------------------------------------------------
// This is the scheme successfully used by GLFW (from which we borrowed some of the code) and other apps aiming to be highly portable.
// ImGui_ImplWin32_EnableDpiAwareness() is just a helper called by main.cpp, we don't call it automatically.
// If you are trying to implement your own back-end for your own engine, you may ignore that noise.
//---------------------------------------------------------------------------------------------------------
// Implement some of the functions and types normally declared in recent Windows SDK.
#if !defined(_versionhelpers_H_INCLUDED_) && !defined(_INC_VERSIONHELPERS)
static BOOL IsWindowsVersionOrGreater(WORD major, WORD minor, WORD sp)
{
OSVERSIONINFOEXW osvi = { sizeof(osvi), major, minor, 0, 0, { 0 }, sp, 0, 0, 0, 0 };
DWORD mask = VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR;
ULONGLONG cond = ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL);
cond = ::VerSetConditionMask(cond, VER_MINORVERSION, VER_GREATER_EQUAL);
cond = ::VerSetConditionMask(cond, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
return ::VerifyVersionInfoW(&osvi, mask, cond);
}
#define IsWindows8Point1OrGreater() IsWindowsVersionOrGreater(HIBYTE(0x0602), LOBYTE(0x0602), 0) // _WIN32_WINNT_WINBLUE
#endif
#ifndef DPI_ENUMS_DECLARED
typedef enum { PROCESS_DPI_UNAWARE = 0, PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS;
typedef enum { MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE;
#endif
#ifndef _DPI_AWARENESS_CONTEXTS_
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT)-3
#endif
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT)-4
#endif
typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS); // Shcore.lib + dll, Windows 8.1+
typedef HRESULT(WINAPI* PFN_GetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); // Shcore.lib + dll, Windows 8.1+
typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT); // User32.lib + dll, Windows 10 v1607+ (Creators Update)
// Helper function to enable DPI awareness without setting up a manifest
void ImGui_ImplWin32_EnableDpiAwareness()
{
// if (IsWindows10OrGreater()) // This needs a manifest to succeed. Instead we try to grab the function pointer!
{
static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process
if (PFN_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContextFn = (PFN_SetThreadDpiAwarenessContext)::GetProcAddress(user32_dll, "SetThreadDpiAwarenessContext"))
{
SetThreadDpiAwarenessContextFn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
return;
}
}
if (IsWindows8Point1OrGreater())
{
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
if (PFN_SetProcessDpiAwareness SetProcessDpiAwarenessFn = (PFN_SetProcessDpiAwareness)::GetProcAddress(shcore_dll, "SetProcessDpiAwareness"))
{
SetProcessDpiAwarenessFn(PROCESS_PER_MONITOR_DPI_AWARE);
return;
}
}
#if _WIN32_WINNT >= 0x0600
::SetProcessDPIAware();
#endif
}
#if defined(_MSC_VER) && !defined(NOGDI)
#pragma comment(lib, "gdi32") // Link with gdi32.lib for GetDeviceCaps()
#endif
float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor)
{
UINT xdpi = 96, ydpi = 96;
static BOOL bIsWindows8Point1OrGreater = IsWindows8Point1OrGreater();
if (bIsWindows8Point1OrGreater)
{
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
if (PFN_GetDpiForMonitor GetDpiForMonitorFn = (PFN_GetDpiForMonitor)::GetProcAddress(shcore_dll, "GetDpiForMonitor"))
GetDpiForMonitorFn((HMONITOR)monitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi);
}
#ifndef NOGDI
else
{
const HDC dc = ::GetDC(NULL);
xdpi = ::GetDeviceCaps(dc, LOGPIXELSX);
ydpi = ::GetDeviceCaps(dc, LOGPIXELSY);
::ReleaseDC(NULL, dc);
}
#endif
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
return xdpi / 96.0f;
}
float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd)
{
HMONITOR monitor = ::MonitorFromWindow((HWND)hwnd, MONITOR_DEFAULTTONEAREST);
return ImGui_ImplWin32_GetDpiScaleForMonitor(monitor);
}
//---------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,37 @@
// dear imgui: Platform Binding for Windows (standard windows API for 32 and 64 bits applications)
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
// Implemented features:
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
// [X] Platform: Keyboard arrays indexed using VK_* Virtual Key Codes, e.g. ImGui::IsKeyPressed(VK_SPACE).
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd);
IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown();
IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame();
// Configuration
// - Disable gamepad support or linking with xinput.lib
//#define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
//#define IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT
// Win32 message handler your application need to call.
// - Intentionally commented out in a '#if 0' block to avoid dragging dependencies on <windows.h> from this helper.
// - You should COPY the line below into your .cpp code to forward declare the function and then you can call it.
#if 0
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
#endif
// DPI-related helpers (optional)
// - Use to enable DPI awareness without having to create an application manifest.
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
IMGUI_IMPL_API void ImGui_ImplWin32_EnableDpiAwareness();
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd); // HWND hwnd
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // HMONITOR monitor

View File

@ -0,0 +1,64 @@
# pragma once
# include "setup.h"
# include <memory>
struct Application;
struct Renderer;
struct WindowState;
struct Platform
{
virtual ~Platform() {};
virtual bool ApplicationStart(int argc, char** argv) = 0;
virtual void ApplicationStop() = 0;
virtual bool OpenMainWindow(const char* title, int width, int height) = 0;
virtual bool CloseMainWindow() = 0;
virtual void* GetMainWindowHandle() const = 0;
virtual void SetMainWindowTitle(const char* title) = 0;
virtual void ShowMainWindow() = 0;
virtual bool ProcessMainWindowEvents() = 0;
virtual bool IsMainWindowVisible() const = 0;
virtual void SetRenderer(Renderer* renderer) = 0;
virtual void NewFrame() = 0;
virtual void FinishFrame() = 0;
virtual void Quit() = 0;
virtual WindowState GetWindowState() const = 0;
virtual bool SetWindowState(const WindowState& state) = 0;
bool HasWindowScaleChanged() const { return m_WindowScaleChanged; }
void AcknowledgeWindowScaleChanged() { m_WindowScaleChanged = false; }
float GetWindowScale() const { return m_WindowScale; }
void SetWindowScale(float windowScale)
{
if (windowScale == m_WindowScale)
return;
m_WindowScale = windowScale;
m_WindowScaleChanged = true;
}
bool HasFramebufferScaleChanged() const { return m_FramebufferScaleChanged; }
void AcknowledgeFramebufferScaleChanged() { m_FramebufferScaleChanged = false; }
float GetFramebufferScale() const { return m_FramebufferScale; }
void SetFramebufferScale(float framebufferScale)
{
if (framebufferScale == m_FramebufferScale)
return;
m_FramebufferScale = framebufferScale;
m_FramebufferScaleChanged = true;
}
private:
bool m_WindowScaleChanged = false;
float m_WindowScale = 1.0f;
bool m_FramebufferScaleChanged = false;
float m_FramebufferScale = 1.0f;
};
std::unique_ptr<Platform> CreatePlatform(Application& application);

View File

@ -0,0 +1,321 @@
# include "platform.h"
# include "setup.h"
# if BACKEND(IMGUI_GLFW)
# include "application.h"
# include "renderer.h"
# include <GLFW/glfw3.h>
# if PLATFORM(WINDOWS)
# define GLFW_EXPOSE_NATIVE_WIN32
# include <GLFW/glfw3native.h>
# endif
# include <imgui.h>
# include "imgui_impl_glfw.h"
struct PlatformGLFW final
: Platform
{
static PlatformGLFW* s_Instance;
PlatformGLFW(Application& application);
bool ApplicationStart(int argc, char** argv) override;
void ApplicationStop() override;
bool OpenMainWindow(const char* title, int width, int height) override;
bool CloseMainWindow() override;
void* GetMainWindowHandle() const override;
void SetMainWindowTitle(const char* title) override;
void ShowMainWindow() override;
bool ProcessMainWindowEvents() override;
bool IsMainWindowVisible() const override;
void SetRenderer(Renderer* renderer) override;
void NewFrame() override;
void FinishFrame() override;
void Quit() override;
WindowState GetWindowState() const override;
bool SetWindowState(const WindowState& state) override;
void UpdatePixelDensity();
Application& m_Application;
GLFWwindow* m_Window = nullptr;
bool m_QuitRequested = false;
bool m_IsMinimized = false;
bool m_WasMinimized = false;
Renderer* m_Renderer = nullptr;
};
std::unique_ptr<Platform> CreatePlatform(Application& application)
{
return std::make_unique<PlatformGLFW>(application);
}
PlatformGLFW::PlatformGLFW(Application& application)
: m_Application(application)
{
}
bool PlatformGLFW::ApplicationStart(int argc, char** argv)
{
if (!glfwInit())
return false;
return true;
}
void PlatformGLFW::ApplicationStop()
{
glfwTerminate();
}
bool PlatformGLFW::OpenMainWindow(const char* title, int width, int height)
{
if (m_Window)
return false;
glfwWindowHint(GLFW_VISIBLE, 0);
using InitializerType = bool (*)(GLFWwindow* window, bool install_callbacks);
InitializerType initializer = nullptr;
# if RENDERER(IMGUI_OGL3)
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
# if PLATFORM(MACOS)
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
# ifdef GLFW_COCOA_RETINA_FRAMEBUFFER
glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GL_TRUE);
# endif
# ifdef GLFW_COCOA_GRAPHICS_SWITCHING
glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, GL_TRUE);
# endif
# endif
initializer = &ImGui_ImplGlfw_InitForOpenGL;
# else
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
initializer = &ImGui_ImplGlfw_InitForNone;
# endif
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GL_TRUE);
width = width < 0 ? 1440 : width;
height = height < 0 ? 800 : height;
m_Window = glfwCreateWindow(width, height, title, nullptr, nullptr);
if (!m_Window)
return false;
if (!initializer || !initializer(m_Window, true))
{
glfwDestroyWindow(m_Window);
m_Window = nullptr;
return false;
}
glfwSetWindowUserPointer(m_Window, this);
glfwSetWindowCloseCallback(m_Window, [](GLFWwindow* window)
{
auto self = reinterpret_cast<PlatformGLFW*>(glfwGetWindowUserPointer(window));
if (!self->m_QuitRequested)
self->CloseMainWindow();
});
glfwSetWindowIconifyCallback(m_Window, [](GLFWwindow* window, int iconified)
{
auto self = reinterpret_cast<PlatformGLFW*>(glfwGetWindowUserPointer(window));
if (iconified)
{
self->m_IsMinimized = true;
self->m_WasMinimized = true;
}
else
{
self->m_IsMinimized = false;
}
});
auto onFramebuferSizeChanged = [](GLFWwindow* window, int width, int height)
{
auto self = reinterpret_cast<PlatformGLFW*>(glfwGetWindowUserPointer(window));
if (self->m_Renderer)
{
self->m_Renderer->Resize(width, height);
self->UpdatePixelDensity();
}
};
glfwSetFramebufferSizeCallback(m_Window, onFramebuferSizeChanged);
auto onWindowContentScaleChanged = [](GLFWwindow* window, float xscale, float yscale)
{
auto self = reinterpret_cast<PlatformGLFW*>(glfwGetWindowUserPointer(window));
self->UpdatePixelDensity();
};
glfwSetWindowContentScaleCallback(m_Window, onWindowContentScaleChanged);
UpdatePixelDensity();
glfwMakeContextCurrent(m_Window);
glfwSwapInterval(1); // Enable vsync
return true;
}
bool PlatformGLFW::CloseMainWindow()
{
if (m_Window == nullptr)
return true;
auto canClose = m_Application.CanClose();
glfwSetWindowShouldClose(m_Window, canClose ? 1 : 0);
return canClose;
}
void* PlatformGLFW::GetMainWindowHandle() const
{
# if PLATFORM(WINDOWS)
return m_Window ? glfwGetWin32Window(m_Window) : nullptr;
# else
return nullptr;
# endif
}
void PlatformGLFW::SetMainWindowTitle(const char* title)
{
glfwSetWindowTitle(m_Window, title);
}
void PlatformGLFW::ShowMainWindow()
{
if (m_Window == nullptr)
return;
glfwShowWindow(m_Window);
}
bool PlatformGLFW::ProcessMainWindowEvents()
{
if (m_Window == nullptr)
return false;
if (m_IsMinimized)
glfwWaitEvents();
else
glfwPollEvents();
if (m_QuitRequested || glfwWindowShouldClose(m_Window))
{
ImGui_ImplGlfw_Shutdown();
glfwDestroyWindow(m_Window);
return false;
}
return true;
}
bool PlatformGLFW::IsMainWindowVisible() const
{
if (m_Window == nullptr)
return false;
if (m_IsMinimized)
return false;
return true;
}
void PlatformGLFW::SetRenderer(Renderer* renderer)
{
m_Renderer = renderer;
}
void PlatformGLFW::NewFrame()
{
ImGui_ImplGlfw_NewFrame();
if (m_WasMinimized)
{
ImGui::GetIO().DeltaTime = 0.1e-6f;
m_WasMinimized = false;
}
}
void PlatformGLFW::FinishFrame()
{
if (m_Renderer)
m_Renderer->Present();
glfwSwapBuffers(m_Window);
}
void PlatformGLFW::Quit()
{
m_QuitRequested = true;
glfwPostEmptyEvent();
}
WindowState PlatformGLFW::GetWindowState() const
{
WindowState state;
if (!m_Window)
return state;
glfwGetWindowPos(m_Window, &state.x, &state.y);
glfwGetWindowSize(m_Window, &state.width, &state.height);
// TODO: Implement monitor index detection for GLFW
state.monitor = 0;
// TODO: Implement maximized state detection for GLFW
state.maximized = false;
return state;
}
bool PlatformGLFW::SetWindowState(const WindowState& state)
{
if (!m_Window)
return false;
// TODO: Implement full GLFW window state restoration
glfwSetWindowPos(m_Window, state.x, state.y);
glfwSetWindowSize(m_Window, state.width, state.height);
return true;
}
void PlatformGLFW::UpdatePixelDensity()
{
float xscale, yscale;
glfwGetWindowContentScale(m_Window, &xscale, &yscale);
float scale = xscale > yscale ? xscale : yscale;
# if PLATFORM(WINDOWS)
float windowScale = scale;
float framebufferScale = scale;
# else
float windowScale = 1.0f;
float framebufferScale = scale;
# endif
SetWindowScale(windowScale); // this is how windows is scaled, not window content
SetFramebufferScale(framebufferScale);
}
# endif // BACKEND(IMGUI_GLFW)

View File

@ -0,0 +1,432 @@
# include "platform.h"
# include "setup.h"
# if BACKEND(IMGUI_WIN32)
# include "base.h"
# include "renderer.h"
# define NOMINMAX
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <tchar.h>
# include <string>
# include <imgui.h>
# include "imgui_impl_win32.h"
# if defined(_UNICODE)
std::wstring Utf8ToNative(const std::string& str)
{
int size = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), nullptr, 0);
std::wstring result(size, 0);
MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), (wchar_t*)result.data(), size);
return result;
}
# else
std::string Utf8ToNative(const std::string& str)
{
return str;
}
# endif
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
struct PlatformWin32 final
: Platform
{
static PlatformWin32* s_Instance;
PlatformWin32(Application& application);
bool ApplicationStart(int argc, char** argv) override;
void ApplicationStop() override;
bool OpenMainWindow(const char* title, int width, int height) override;
bool CloseMainWindow() override;
void* GetMainWindowHandle() const override;
void SetMainWindowTitle(const char* title) override;
void ShowMainWindow() override;
bool ProcessMainWindowEvents() override;
bool IsMainWindowVisible() const override;
void SetRenderer(Renderer* renderer) override;
void NewFrame() override;
void FinishFrame() override;
void Quit() override;
WindowState GetWindowState() const override;
bool SetWindowState(const WindowState& state) override;
void SetDpiScale(float dpiScale);
LRESULT WinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
Application& m_Application;
WNDCLASSEX m_WindowClass = {};
HWND m_MainWindowHandle = nullptr;
bool m_IsMinimized = false;
bool m_WasMinimized = false;
bool m_CanCloseResult = false;
Renderer* m_Renderer = nullptr;
};
std::unique_ptr<Platform> CreatePlatform(Application& application)
{
return std::make_unique<PlatformWin32>(application);
}
PlatformWin32* PlatformWin32::s_Instance = nullptr;
PlatformWin32::PlatformWin32(Application& application)
: m_Application(application)
{
}
bool PlatformWin32::ApplicationStart(int argc, char** argv)
{
if (s_Instance)
return false;
s_Instance = this;
auto winProc = [](HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -> LRESULT
{
return s_Instance->WinProc(hWnd, msg, wParam, lParam);
};
m_WindowClass =
{
sizeof(WNDCLASSEX),
CS_CLASSDC,
winProc,
0L,
0L,
GetModuleHandle(nullptr),
LoadIcon(GetModuleHandle(nullptr),
IDI_APPLICATION),
LoadCursor(nullptr, IDC_ARROW),
nullptr,
nullptr,
_T("imgui-node-editor-application"),
LoadIcon(GetModuleHandle(nullptr),
IDI_APPLICATION)
};
if (!RegisterClassEx(&m_WindowClass))
{
s_Instance = nullptr;
return false;
}
ImGui_ImplWin32_EnableDpiAwareness();
return true;
}
void PlatformWin32::ApplicationStop()
{
if (!s_Instance)
return;
UnregisterClass(m_WindowClass.lpszClassName, m_WindowClass.hInstance);
s_Instance = nullptr;
}
bool PlatformWin32::OpenMainWindow(const char* title, int width, int height)
{
if (m_MainWindowHandle)
return false;
m_MainWindowHandle = CreateWindow(
m_WindowClass.lpszClassName,
Utf8ToNative(title).c_str(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
width < 0 ? CW_USEDEFAULT : width,
height < 0 ? CW_USEDEFAULT : height,
nullptr, nullptr, m_WindowClass.hInstance, nullptr);
if (!m_MainWindowHandle)
return false;
if (!ImGui_ImplWin32_Init(m_MainWindowHandle))
{
DestroyWindow(m_MainWindowHandle);
m_MainWindowHandle = nullptr;
return false;
}
SetDpiScale(ImGui_ImplWin32_GetDpiScaleForHwnd(m_MainWindowHandle));
return true;
}
bool PlatformWin32::CloseMainWindow()
{
if (m_MainWindowHandle == nullptr)
return true;
SendMessage(m_MainWindowHandle, WM_CLOSE, 0, 0);
return m_CanCloseResult;
}
void* PlatformWin32::GetMainWindowHandle() const
{
return m_MainWindowHandle;
}
void PlatformWin32::SetMainWindowTitle(const char* title)
{
SetWindowText(m_MainWindowHandle, Utf8ToNative(title).c_str());
}
void PlatformWin32::ShowMainWindow()
{
if (m_MainWindowHandle == nullptr)
return;
//ShowWindow(m_MainWindowHandle, SW_SHOWMAXIMIZED);
ShowWindow(m_MainWindowHandle, SW_SHOW);
UpdateWindow(m_MainWindowHandle);
}
bool PlatformWin32::ProcessMainWindowEvents()
{
if (m_MainWindowHandle == nullptr)
return false;
auto fetchMessage = [this](MSG* msg) -> bool
{
if (!m_IsMinimized)
return PeekMessage(msg, nullptr, 0U, 0U, PM_REMOVE) != 0;
else
return GetMessage(msg, nullptr, 0U, 0U) != 0;
};
MSG msg = {};
while (fetchMessage(&msg))
{
if (msg.message == WM_KEYDOWN && (msg.wParam == VK_ESCAPE))
PostQuitMessage(0);
if (msg.message == WM_QUIT)
return false;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return true;
}
bool PlatformWin32::IsMainWindowVisible() const
{
if (m_MainWindowHandle == nullptr)
return false;
if (m_IsMinimized)
return false;
return true;
}
void PlatformWin32::SetRenderer(Renderer* renderer)
{
m_Renderer = renderer;
}
void PlatformWin32::NewFrame()
{
ImGui_ImplWin32_NewFrame();
if (m_WasMinimized)
{
ImGui::GetIO().DeltaTime = 0.1e-6f;
m_WasMinimized = false;
}
}
void PlatformWin32::FinishFrame()
{
if (m_Renderer)
m_Renderer->Present();
}
void PlatformWin32::Quit()
{
PostQuitMessage(0);
}
WindowState PlatformWin32::GetWindowState() const
{
WindowState state;
if (!m_MainWindowHandle)
return state;
WINDOWPLACEMENT placement = { sizeof(WINDOWPLACEMENT) };
if (GetWindowPlacement(m_MainWindowHandle, &placement))
{
state.x = placement.rcNormalPosition.left;
state.y = placement.rcNormalPosition.top;
state.width = placement.rcNormalPosition.right - placement.rcNormalPosition.left;
state.height = placement.rcNormalPosition.bottom - placement.rcNormalPosition.top;
state.maximized = (placement.showCmd == SW_SHOWMAXIMIZED);
}
// Get monitor index (0-based)
HMONITOR hMonitor = MonitorFromWindow(m_MainWindowHandle, MONITOR_DEFAULTTONEAREST);
state.monitor = 0; // Default to primary
// Enumerate monitors to find index
struct MonitorEnumData {
HMONITOR target;
int index;
int currentIndex;
};
MonitorEnumData enumData = { hMonitor, 0, 0 };
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMon, HDC, LPRECT, LPARAM data) -> BOOL {
auto* enumData = reinterpret_cast<MonitorEnumData*>(data);
if (hMon == enumData->target) {
enumData->index = enumData->currentIndex;
return FALSE; // Stop enumeration
}
enumData->currentIndex++;
return TRUE;
}, reinterpret_cast<LPARAM>(&enumData));
state.monitor = enumData.index;
return state;
}
bool PlatformWin32::SetWindowState(const WindowState& state)
{
if (!m_MainWindowHandle)
return false;
// Validate and clamp position/size to be within monitor bounds
HMONITOR hMonitor = nullptr;
// Try to get the specified monitor
int currentMonitor = 0;
struct MonitorEnumData {
int targetIndex;
int currentIndex;
HMONITOR result;
};
MonitorEnumData enumData = { state.monitor, 0, nullptr };
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMon, HDC, LPRECT, LPARAM data) -> BOOL {
auto* enumData = reinterpret_cast<MonitorEnumData*>(data);
if (enumData->currentIndex == enumData->targetIndex) {
enumData->result = hMon;
return FALSE; // Stop enumeration
}
enumData->currentIndex++;
return TRUE;
}, reinterpret_cast<LPARAM>(&enumData));
hMonitor = enumData.result;
// Fall back to primary monitor if specified monitor doesn't exist
if (!hMonitor)
{
const POINT pt = { 0, 0 };
hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
}
// Get monitor info to validate position
MONITORINFO monitorInfo = { sizeof(MONITORINFO) };
GetMonitorInfo(hMonitor, &monitorInfo);
// Build window placement structure
WINDOWPLACEMENT placement = { sizeof(WINDOWPLACEMENT) };
placement.flags = 0;
placement.showCmd = state.maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL;
// Set position (validate it's at least partially on screen)
int x = state.x;
int y = state.y;
int w = state.width;
int h = state.height;
// Ensure window is at least partially visible
const int MIN_VISIBLE = 50; // At least 50 pixels must be visible
if (x + w < monitorInfo.rcWork.left + MIN_VISIBLE)
x = monitorInfo.rcWork.left;
if (y + h < monitorInfo.rcWork.top + MIN_VISIBLE)
y = monitorInfo.rcWork.top;
if (x > monitorInfo.rcWork.right - MIN_VISIBLE)
x = monitorInfo.rcWork.right - w;
if (y > monitorInfo.rcWork.bottom - MIN_VISIBLE)
y = monitorInfo.rcWork.bottom - h;
placement.rcNormalPosition.left = x;
placement.rcNormalPosition.top = y;
placement.rcNormalPosition.right = x + w;
placement.rcNormalPosition.bottom = y + h;
return SetWindowPlacement(m_MainWindowHandle, &placement) != 0;
}
void PlatformWin32::SetDpiScale(float dpiScale)
{
SetWindowScale(dpiScale);
SetFramebufferScale(dpiScale);
}
LRESULT PlatformWin32::WinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
return 1;
switch (msg)
{
case WM_CLOSE:
m_CanCloseResult = m_Application.CanClose();
if (m_CanCloseResult)
{
ImGui_ImplWin32_Shutdown();
DestroyWindow(hWnd);
}
return 0;
case WM_SIZE:
if (wParam == SIZE_MINIMIZED)
{
m_IsMinimized = true;
m_WasMinimized = true;
}
else if (wParam == SIZE_RESTORED && m_IsMinimized)
{
m_IsMinimized = false;
}
if (m_Renderer != nullptr && wParam != SIZE_MINIMIZED)
m_Renderer->Resize(static_cast<int>(LOWORD(lParam)), static_cast<int>(HIWORD(lParam)));
return 0;
case WM_SYSCOMMAND:
if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu
return 0;
break;
case WM_DPICHANGED:
SetDpiScale(ImGui_ImplWin32_GetDpiScaleForHwnd(hWnd));
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
# endif // BACKEND(IMGUI_WIN32)

View File

@ -0,0 +1,34 @@
# pragma once
# include "setup.h"
# include <memory>
struct Platform;
struct ImDrawData;
struct ImVec4;
using ImTextureID= void*;
struct Renderer
{
virtual ~Renderer() {};
virtual bool Create(Platform& platform) = 0;
virtual void Destroy() = 0;
virtual void NewFrame() = 0;
virtual void RenderDrawData(ImDrawData* drawData) = 0;
virtual void Clear(const ImVec4& color) = 0;
virtual void Present() = 0;
virtual void Resize(int width, int height) = 0;
virtual ImTextureID CreateTexture(const void* data, int width, int height) = 0;
virtual void DestroyTexture(ImTextureID texture) = 0;
virtual int GetTextureWidth(ImTextureID texture) = 0;
virtual int GetTextureHeight(ImTextureID texture) = 0;
virtual bool TakeScreenshot(const char* filename) = 0;
};
std::unique_ptr<Renderer> CreateRenderer();

View File

@ -0,0 +1,316 @@
# include "renderer.h"
# include "setup.h"
# if RENDERER(IMGUI_DX11)
# include "platform.h"
# if PLATFORM(WINDOWS)
# define NOMINMAX
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# endif
# include <imgui.h>
# include "imgui_impl_dx11.h"
# include <d3d11.h>
# include <vector>
# include <string>
# include <ctime>
# include <cstring>
# define STB_IMAGE_WRITE_IMPLEMENTATION
# include "../../external/stb_image_latest/stb_image_write.h"
struct RendererDX11 final
: Renderer
{
bool Create(Platform& platform) override;
void Destroy() override;
void NewFrame() override;
void RenderDrawData(ImDrawData* drawData) override;
void Clear(const ImVec4& color) override;
void Present() override;
void Resize(int width, int height) override;
ImTextureID CreateTexture(const void* data, int width, int height) override;
void DestroyTexture(ImTextureID texture) override;
int GetTextureWidth(ImTextureID texture) override;
int GetTextureHeight(ImTextureID texture) override;
bool TakeScreenshot(const char* filename) override;
HRESULT CreateDeviceD3D(HWND hWnd);
void CleanupDeviceD3D();
void CreateRenderTarget();
void CleanupRenderTarget();
Platform* m_Platform = nullptr;
ID3D11Device* m_device = nullptr;
ID3D11DeviceContext* m_deviceContext = nullptr;
IDXGISwapChain* m_swapChain = nullptr;
ID3D11RenderTargetView* m_mainRenderTargetView = nullptr;
};
std::unique_ptr<Renderer> CreateRenderer()
{
return std::make_unique<RendererDX11>();
}
bool RendererDX11::Create(Platform& platform)
{
m_Platform = &platform;
auto hr = CreateDeviceD3D(reinterpret_cast<HWND>(platform.GetMainWindowHandle()));
if (FAILED(hr))
return false;
if (!ImGui_ImplDX11_Init(m_device, m_deviceContext))
{
CleanupDeviceD3D();
return false;
}
m_Platform->SetRenderer(this);
return true;
}
void RendererDX11::Destroy()
{
if (!m_Platform)
return;
m_Platform->SetRenderer(nullptr);
ImGui_ImplDX11_Shutdown();
CleanupDeviceD3D();
}
void RendererDX11::NewFrame()
{
ImGui_ImplDX11_NewFrame();
}
void RendererDX11::RenderDrawData(ImDrawData* drawData)
{
ImGui_ImplDX11_RenderDrawData(drawData);
}
void RendererDX11::Clear(const ImVec4& color)
{
m_deviceContext->ClearRenderTargetView(m_mainRenderTargetView, (float*)&color.x);
}
void RendererDX11::Present()
{
m_swapChain->Present(1, 0);
}
void RendererDX11::Resize(int width, int height)
{
ImGui_ImplDX11_InvalidateDeviceObjects();
CleanupRenderTarget();
m_swapChain->ResizeBuffers(0, (UINT)width, (UINT)height, DXGI_FORMAT_UNKNOWN, 0);
CreateRenderTarget();
}
HRESULT RendererDX11::CreateDeviceD3D(HWND hWnd)
{
// Setup swap chain
DXGI_SWAP_CHAIN_DESC sd;
{
ZeroMemory(&sd, sizeof(sd));
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
}
UINT createDeviceFlags = 0;
//createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
D3D_FEATURE_LEVEL featureLevel;
const D3D_FEATURE_LEVEL featureLevelArray[1] = { D3D_FEATURE_LEVEL_11_0, };
if (D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 1, D3D11_SDK_VERSION, &sd, &m_swapChain, &m_device, &featureLevel, &m_deviceContext) != S_OK)
return E_FAIL;
CreateRenderTarget();
return S_OK;
}
void RendererDX11::CleanupDeviceD3D()
{
CleanupRenderTarget();
if (m_swapChain) { m_swapChain->Release(); m_swapChain = nullptr; }
if (m_deviceContext) { m_deviceContext->Release(); m_deviceContext = nullptr; }
if (m_device) { m_device->Release(); m_device = nullptr; }
}
void RendererDX11::CreateRenderTarget()
{
DXGI_SWAP_CHAIN_DESC sd;
m_swapChain->GetDesc(&sd);
// Create the render target
ID3D11Texture2D* pBackBuffer;
D3D11_RENDER_TARGET_VIEW_DESC render_target_view_desc;
ZeroMemory(&render_target_view_desc, sizeof(render_target_view_desc));
render_target_view_desc.Format = sd.BufferDesc.Format;
render_target_view_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
m_device->CreateRenderTargetView(pBackBuffer, &render_target_view_desc, &m_mainRenderTargetView);
m_deviceContext->OMSetRenderTargets(1, &m_mainRenderTargetView, nullptr);
pBackBuffer->Release();
}
void RendererDX11::CleanupRenderTarget()
{
if (m_mainRenderTargetView) { m_mainRenderTargetView->Release(); m_mainRenderTargetView = nullptr; }
}
ImTextureID RendererDX11::CreateTexture(const void* data, int width, int height)
{
return ImGui_CreateTexture(data, width, height);
}
void RendererDX11::DestroyTexture(ImTextureID texture)
{
return ImGui_DestroyTexture(texture);
}
int RendererDX11::GetTextureWidth(ImTextureID texture)
{
return ImGui_GetTextureWidth(texture);
}
int RendererDX11::GetTextureHeight(ImTextureID texture)
{
return ImGui_GetTextureHeight(texture);
}
static void GpuFinish(ID3D11Device* device, ID3D11DeviceContext* ctx) {
ID3D11Query* query = nullptr;
D3D11_QUERY_DESC qd = { D3D11_QUERY_EVENT, 0 };
device->CreateQuery(&qd, &query);
ctx->End(query);
while (ctx->GetData(query, nullptr, 0, 0) == S_FALSE) { /* spin */ }
query->Release();
}
bool RendererDX11::TakeScreenshot(const char* filename)
{
// Generate filename if not provided
std::string fname;
if (!filename)
{
time_t now = time(nullptr);
char buffer[64];
strftime(buffer, sizeof(buffer), "screenshot_%Y%m%d_%H%M%S.png", localtime(&now));
fname = buffer;
}
else
fname = filename;
// Flush any pending GPU work before capturing
m_deviceContext->Flush();
// Get back buffer
ID3D11Texture2D* backBuffer = nullptr;
HRESULT hr = m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
if (FAILED(hr) || !backBuffer)
return false;
// Get description
D3D11_TEXTURE2D_DESC desc;
backBuffer->GetDesc(&desc);
// Create staging texture for CPU readback
D3D11_TEXTURE2D_DESC stagingDesc = desc;
stagingDesc.BindFlags = 0;
stagingDesc.MiscFlags = 0;
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
stagingDesc.Usage = D3D11_USAGE_STAGING;
ID3D11Texture2D* stagingTexture = nullptr;
hr = m_device->CreateTexture2D(&stagingDesc, nullptr, &stagingTexture);
if (FAILED(hr) || !stagingTexture)
{
backBuffer->Release();
return false;
}
// Copy back buffer to staging
m_deviceContext->CopyResource(stagingTexture, backBuffer);
m_deviceContext->Flush();
// Wait for GPU to finish
GpuFinish(m_device, m_deviceContext);
backBuffer->Release();
// Map and read
D3D11_MAPPED_SUBRESOURCE mapped = {};
hr = m_deviceContext->Map(stagingTexture, 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr))
{
stagingTexture->Release();
return false;
}
// Check format
const bool isBGRA = (desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM ||
desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB);
const bool isRGBA = (desc.Format == DXGI_FORMAT_R8G8B8A8_UNORM ||
desc.Format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB);
if (!isBGRA && !isRGBA)
{
m_deviceContext->Unmap(stagingTexture, 0);
stagingTexture->Release();
return false;
}
const int w = (int)desc.Width;
const int h = (int)desc.Height;
std::vector<unsigned char> rgba(w * h * 4);
// Copy row by row and convert BGRA to RGBA if needed
// DirectX11 render targets are typically already in the correct orientation (top-to-bottom)
for (int y = 0; y < h; ++y)
{
const unsigned char* srcRow = (const unsigned char*)mapped.pData + y * mapped.RowPitch;
unsigned char* dstRow = rgba.data() + y * w * 4;
for (int x = 0; x < w; ++x)
{
const unsigned char* p = srcRow + x * 4;
// DXGI_FORMAT_R8G8B8A8_UNORM is actually BGRA in memory!
// Convert BGRA to RGBA
dstRow[x * 4 + 0] = p[2]; // R <- B
dstRow[x * 4 + 1] = p[1]; // G <- G
dstRow[x * 4 + 2] = p[0]; // B <- R
dstRow[x * 4 + 3] = p[3]; // A <- A
}
}
m_deviceContext->Unmap(stagingTexture, 0);
stagingTexture->Release();
// Write PNG
int ok = stbi_write_png(fname.c_str(), w, h, 4, rgba.data(), w * 4);
return ok != 0;
}
# endif // RENDERER(IMGUI_DX11)

View File

@ -0,0 +1,257 @@
# include "renderer.h"
# if RENDERER(IMGUI_OGL3)
# include "platform.h"
# include <algorithm>
# include <cstdint> // std::intptr_t
# include <vector>
# include <string>
# include <ctime>
# include <cstring>
# define STB_IMAGE_WRITE_IMPLEMENTATION
# include "../../external/stb_image_latest/stb_image_write.h"
# if PLATFORM(WINDOWS)
# define NOMINMAX
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# endif
# include "imgui_impl_opengl3.h"
# if defined(IMGUI_IMPL_OPENGL_LOADER_GL3W)
# include <GL/gl3w.h> // Initialize with gl3wInit()
# elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW)
# include <GL/glew.h> // Initialize with glewInit()
# elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD)
# include <glad/glad.h> // Initialize with gladLoadGL()
# elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD2)
# include <glad/gl.h> // Initialize with gladLoadGL(...) or gladLoaderLoadGL()
# elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2)
# define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors.
# include <glbinding/Binding.h> // Initialize with glbinding::Binding::initialize()
# include <glbinding/gl/gl.h>
using namespace gl;
# elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3)
# define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors.
# include <glbinding/glbinding.h>// Initialize with glbinding::initialize()
# include <glbinding/gl/gl.h>
using namespace gl;
# elif defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM)
# include IMGUI_IMPL_OPENGL_LOADER_CUSTOM
# else
# include "imgui_impl_opengl3_loader.h"
# endif
struct ImTexture
{
GLuint TextureID = 0;
int Width = 0;
int Height = 0;
};
struct RendererOpenGL3 final
: Renderer
{
bool Create(Platform& platform) override;
void Destroy() override;
void NewFrame() override;
void RenderDrawData(ImDrawData* drawData) override;
void Clear(const ImVec4& color) override;
void Present() override;
void Resize(int width, int height) override;
ImVector<ImTexture>::iterator FindTexture(ImTextureID texture);
ImTextureID CreateTexture(const void* data, int width, int height) override;
void DestroyTexture(ImTextureID texture) override;
int GetTextureWidth(ImTextureID texture) override;
int GetTextureHeight(ImTextureID texture) override;
bool TakeScreenshot(const char* filename) override;
Platform* m_Platform = nullptr;
ImVector<ImTexture> m_Textures;
};
std::unique_ptr<Renderer> CreateRenderer()
{
return std::make_unique<RendererOpenGL3>();
}
bool RendererOpenGL3::Create(Platform& platform)
{
m_Platform = &platform;
// Technically we should initialize OpenGL context here,
// but for now we relay on one created by GLFW3
#if defined(IMGUI_IMPL_OPENGL_LOADER_GL3W)
bool err = gl3wInit() != 0;
#elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW)
bool err = glewInit() != GLEW_OK;
#elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD)
bool err = gladLoadGL() == 0;
#elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD2)
bool err = gladLoadGL(glfwGetProcAddress) == 0; // glad2 recommend using the windowing library loader instead of the (optionally) bundled one.
#elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2)
bool err = false;
glbinding::Binding::initialize();
#elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3)
bool err = false;
glbinding::initialize([](const char* name) { return (glbinding::ProcAddress)glfwGetProcAddress(name); });
#else
bool err = false; // If you use IMGUI_IMPL_OPENGL_LOADER_CUSTOM, your loader is likely to requires some form of initialization.
#endif
if (err)
return false;
# if PLATFORM(MACOS)
const char* glslVersion = "#version 150";
# else
const char* glslVersion = "#version 130";
# endif
if (!ImGui_ImplOpenGL3_Init(glslVersion))
return false;
m_Platform->SetRenderer(this);
return true;
}
void RendererOpenGL3::Destroy()
{
if (!m_Platform)
return;
m_Platform->SetRenderer(nullptr);
ImGui_ImplOpenGL3_Shutdown();
}
void RendererOpenGL3::NewFrame()
{
ImGui_ImplOpenGL3_NewFrame();
}
void RendererOpenGL3::RenderDrawData(ImDrawData* drawData)
{
ImGui_ImplOpenGL3_RenderDrawData(drawData);
}
void RendererOpenGL3::Clear(const ImVec4& color)
{
glClearColor(color.x, color.y, color.z, color.w);
glClear(GL_COLOR_BUFFER_BIT);
}
void RendererOpenGL3::Present()
{
}
void RendererOpenGL3::Resize(int width, int height)
{
glViewport(0, 0, width, height);
}
ImTextureID RendererOpenGL3::CreateTexture(const void* data, int width, int height)
{
m_Textures.resize(m_Textures.size() + 1);
ImTexture& texture = m_Textures.back();
// Upload texture to graphics system
GLint last_texture = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture);
glGenTextures(1, &texture.TextureID);
glBindTexture(GL_TEXTURE_2D, texture.TextureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, last_texture);
texture.Width = width;
texture.Height = height;
return reinterpret_cast<ImTextureID>(static_cast<std::intptr_t>(texture.TextureID));
}
ImVector<ImTexture>::iterator RendererOpenGL3::FindTexture(ImTextureID texture)
{
auto textureID = static_cast<GLuint>(reinterpret_cast<std::intptr_t>(texture));
return std::find_if(m_Textures.begin(), m_Textures.end(), [textureID](ImTexture& texture)
{
return texture.TextureID == textureID;
});
}
void RendererOpenGL3::DestroyTexture(ImTextureID texture)
{
auto textureIt = FindTexture(texture);
if (textureIt == m_Textures.end())
return;
glDeleteTextures(1, &textureIt->TextureID);
m_Textures.erase(textureIt);
}
int RendererOpenGL3::GetTextureWidth(ImTextureID texture)
{
auto textureIt = FindTexture(texture);
if (textureIt != m_Textures.end())
return textureIt->Width;
return 0;
}
int RendererOpenGL3::GetTextureHeight(ImTextureID texture)
{
auto textureIt = FindTexture(texture);
if (textureIt != m_Textures.end())
return textureIt->Height;
return 0;
}
bool RendererOpenGL3::TakeScreenshot(const char* filename)
{
// Generate filename if not provided
std::string fname;
if (!filename)
{
time_t now = time(nullptr);
char buffer[64];
strftime(buffer, sizeof(buffer), "screenshot_%Y%m%d_%H%M%S.png", localtime(&now));
fname = buffer;
}
else
fname = filename;
// Get viewport size
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
int width = viewport[2];
int height = viewport[3];
// Allocate pixel buffer (RGBA)
std::vector<unsigned char> pixels(width * height * 4);
// Read pixels from framebuffer (OpenGL)
// Ensure pixel alignment is 1 byte
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
// Flip vertically (OpenGL is bottom-up)
std::vector<unsigned char> flipped(width * height * 4);
for (int y = 0; y < height; y++)
{
memcpy(&flipped[y * width * 4],
&pixels[(height - 1 - y) * width * 4],
width * 4);
}
// Save using stb_image_write
return stbi_write_png(fname.c_str(), width, height, 4, flipped.data(), width * 4);
}
# endif // RENDERER(IMGUI_OGL3)

View File

@ -0,0 +1,98 @@
# pragma once
# include "config.h"
# define DETAIL_PRIV_EXPAND(x) x
# define EXPAND(x) DETAIL_PRIV_EXPAND(x)
# define DETAIL_PRIV_CONCAT(x, y) x ## y
# define CONCAT(x, y) DETAIL_PRIV_CONCAT(x, y)
// Define PLATFORM(x) which evaluate to 0 or 1 when
// 'x' is: WINDOWS, MACOS or LINUX
# if defined(_WIN32)
# define PLATFORM_PRIV_WINDOWS() 1
# elif defined(__APPLE__)
# define PLATFORM_PRIV_MACOS() 1
# elif defined(__linux__)
# define PLATFORM_PRIV_LINUX() 1
# else
# error Unsupported platform
# endif
# ifndef PLATFORM_PRIV_WINDOWS
# define PLATFORM_PRIV_WINDOWS() 0
# endif
# ifndef PLATFORM_PRIV_MACOS
# define PLATFORM_PRIV_MACOS() 0
# endif
# ifndef PLATFORM_PRIV_LINUX
# define PLATFORM_PRIV_LINUX() 0
# endif
# define PLATFORM(x) (PLATFORM_PRIV_##x())
// Define BACKEND(x) which evaluate to 0 or 1 when
// 'x' is: IMGUI_WIN32 or IMGUI_GLFW
//
// Use BACKEND_CONFIG to override desired backend
//
# if PLATFORM(WINDOWS)
# define BACKEND_HAVE_IMGUI_WIN32() 1
# endif
# if HAVE_GLFW3
# define BACKEND_HAVE_IMGUI_GLFW() 1
# endif
# ifndef BACKEND_HAVE_IMGUI_WIN32
# define BACKEND_HAVE_IMGUI_WIN32() 0
# endif
# ifndef BACKEND_HAVE_IMGUI_GLFW
# define BACKEND_HAVE_IMGUI_GLFW() 0
# endif
# define BACKEND_PRIV_IMGUI_WIN32() 1
# define BACKEND_PRIV_IMGUI_GLFW() 2
# if !defined(BACKEND_CONFIG)
# if PLATFORM(WINDOWS)
# define BACKEND_CONFIG IMGUI_WIN32
# else
# define BACKEND_CONFIG IMGUI_GLFW
# endif
# endif
# define BACKEND(x) ((BACKEND_PRIV_##x()) == CONCAT(BACKEND_PRIV_, EXPAND(BACKEND_CONFIG))() && (BACKEND_HAVE_##x()))
// Define RENDERER(x) which evaluate to 0 or 1 when
// 'x' is: IMGUI_DX11 or IMGUI_OGL3
//
// Use RENDERER_CONFIG to override desired renderer
//
# if PLATFORM(WINDOWS)
# define RENDERER_HAVE_IMGUI_DX11() 1
# endif
# if HAVE_OPENGL
# define RENDERER_HAVE_IMGUI_OGL3() 1
# endif
# ifndef RENDERER_HAVE_IMGUI_DX11
# define RENDERER_HAVE_IMGUI_DX11() 0
# endif
# ifndef RENDERER_HAVE_IMGUI_OGL3
# define RENDERER_HAVE_IMGUI_OGL3() 0
# endif
# define RENDERER_PRIV_IMGUI_DX11() 1
# define RENDERER_PRIV_IMGUI_OGL3() 2
# if !defined(RENDERER_CONFIG)
# if PLATFORM(WINDOWS)
# define RENDERER_CONFIG IMGUI_DX11
# else
# define RENDERER_CONFIG IMGUI_OGL3
# endif
# endif
# define RENDERER(x) ((RENDERER_PRIV_##x()) == CONCAT(RENDERER_PRIV_, EXPAND(RENDERER_CONFIG))() && (RENDERER_HAVE_##x()))

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,3 @@
#define IDI_APPLICATION 32512
IDI_APPLICATION ICON "${ApplicationIcon}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,93 @@
Copyright 2010 The Cuprum Project Authors (lemonad@jovanny.ru), with Reserved Font Name "Cuprum".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright (c) 2011, Jonas Hecksher, Playtypes, e-types AS (lasse@e-types.com), with Reserved Font Name 'Play', 'Playtype', 'Playtype Sans'.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

View File

@ -0,0 +1,289 @@
v0.9.4 (WIP):
NEW: Editor: Add smooth zoom (#266)
BUGFIX: Canvas: Remember index of first command buffer to not miss updating any used (#260)
BUGFIX: Editor: Don't duplicated ImVec2/ImVec3 == != operators defined since ImGui r19002 (#268)
BUGFIX: Examples: Use imgui_impl_opengl3_loader.h instead of gl3w (#264)
v0.9.3 (2023-10-14):
CHANGE: Canvas: Use ImDrawCallback_ImCanvas macro as draw callback sentinel (#256), thanks @nspitko
BUGFIX: Canvas: Ensure SentinelDrawCallback cleanup (#255)
BUGFIX: Editor: Don't call Reasume/Suspend on invisible canvas (#255)
v0.9.2 (2023-09-01):
NEW: Editor: Add offset of hover/select to style (thanks @MultiPain)
NEW: Editor: Add IMGUI_NODE_EDITOR_API to support building editor as a shared library (#189)
NEW: Canvas: Add IMGUIEX_CANVAS_API to support building canvas as a shared library (#189)
CHANGE: Editor: Support ImGui r18836 after SetItemUsingMouseWheel removal (#218), thanks @ocornut
CHANGE: Editor: Define IMGUI_DEFINE_MATH_OPERATORS before <imgui.h> (#209), thanks @ocornut
CHANGE: Examples: Define IMGUI_DEFINE_MATH_OPERATORS before <imgui.h> (#209), thanks @ocornut
CHANGE: Canvas: Don't use deprecated SetItemAllowOverlap (#250)
CHANGE: Examples: Don't use deprecated SetItemAllowOverlap (#250)
CHANGE: Editor: Define IMGUI_DEFINE_MATH_OPERATORS before <imgui.h> (#209), thanks @ocornut
CHANGE: Editor: Unary operator- for ImVec2 is defined by ImGui since r18955 (#248)
BUGFIX: Editor: Correctly initialize 'width' for view resize code (thx @gnif)
BUGFIX: Examples: Handle node deletion before links (#182)
Deleting node queue connected links for deletion.
BUGFIX: Examples: Simplify and fix drawing of node header line (#180)
BUGFIX: Editor: Cleanup tabs.
BUGFIX: Editor: Use ImGuiKey directly with ImGui r18822 (#183)
BUGFIX: Examples: Use ImGuiKey directly with ImGui r18822 (#183)
BUGFIX: Examples: Use ImGuiKey_KeypadEnter with ImGui r18604 (#183)
BUGFIX: Examples: Add missing <cstdint> include for std::intptr_t (#199)
BUGFIX: Examples: Don't use empty string as identifier
BUGFIX: Editor: Clean long to int implicit cast warning in crude_json
BUGFIX: Canvas: Ensure canvas draw commands are separated from other ImGui draw commands (#205, #250)
BUGFIX: Editor: Don't call Canvas.End() when Canvas.Begin() failed (#186), thanks @pthom, @TheZoc
v0.9.1 (2022-08-27):
CHANGE: Remove unwanted extra frame height from node bottom
CHANGE: Allow to specify if links of deleted node should also be automatically deleted
Now it is possible to delete only node without automatically serving links,
application can choose to do this operation by itself and for example
short circuit flow links ot do any other special operation.
CHANGE: Canvas: Allow to overlap canvas widget
CHANGE: Natvis: Move crude_json natvis to separate file
CHANGE: Natvis: Show readable NodeId/PinId/LinkId
CHANGE: Make Navigate action to honor duration
CHANGE: Travis: Use Ubuntu Bionic (18.04) for CI, to get newer version of GLFW3
CHANGE: Editor: Make action button internally configurable
CHANGE: Make Node Editor forward compatible with ImGui 1.80+ (#112)
We're keeping backward compatibility with pre 1.8x versions.
CHANGE: Update internal copy ImGui to 1.84 (WIP) (3512f2c2c283ec86) (#107)
Internal copy has two PR's merged:
https://github.com/thedmd/imgui/tree/feature/layout - used in blueprints example only
https://github.com/thedmd/imgui/tree/feature/extra-keys - optional: used by Node Editor if present
CHANGE: Use github actions instead of Travis and AppVeyor (#113)
CHANGE: Delete operation on node/link will remove internal object (#173)
CHANGE: Natvis: Add crude_json::value visualization
NEW: All source components are now versioned
NEW: Make view state independent of window resolution.
NEW: Editor can now break links connected specified node or pin
New API:
int BreakLinks(NodeId nodeId);
int BreakLinks(PinId pinId);
NEW: Editor can now tell if node or pin has any links attached
New API:
bool HasAnyLinks(NodeId nodeId);
bool HasAnyLinks(PinId pinId);
NEW: Editor can be queried if particular node or link is selected
New API:
bool IsNodeSelected(NodeId nodeId);
bool IsLinkSelected(LinkId linkId);
NEW: Editor now can return pins of the link
New API:
bool GetLinkPins(LinkId linkId, PinId* startPinId, PinId* endPinId);
`startPinId` and `endPinId` may be null if caller is not interested
in particular id.
NEW: Editor now return ids of hovered node/pin/link
New API:
NodeId GetHoveredNode();
PinId GetHoveredPin();
LinkId GetHoveredLink();
NEW: Add SetGroupSize() to explicitly set group size
New API:
void SetGroupSize(NodeId nodeId, const ImVec2& size);
NEW: crude_json: Add save() and load()
When CRUDE_JSON_IO == 1 value will have load() and save()
function implemented using stdio.h FILE.
NEW: crude_json: Add erase() and get_ptr()
NEW: Application overhaul
- Convert from function based to inheritable class
- Add ability to close app and change title from code
- Add ability to control main window flags (ex. show menubar)
- Save ImGui state to ini file
- Render using pre-multiplied alpha textures
- Add extra fonts to examples.
NEW: Reintegrate Widgets example from @crolando (#77)
NEW: User can now override button indices for various actions (#88)
New API in Config:
int DragButtonIndex; // Mouse button index drag action will react to (0-left, 1-right, 2-middle)
int SelectButtonIndex; // Mouse button index select action will react to (0-left, 1-right, 2-middle)
int NavigateButtonIndex; // Mouse button index navigate action will react to (0-left, 1-right, 2-middle)
int ContextMenuButtonIndex; // Mouse button index context menu action will react to (0-left, 1-right, 2-middle)
NEW: Flow direction can now be picked per flow (#104)
New API:
enum class FlowDirection
{
Forward,
Backward
};
void Flow(LinkId linkId, FlowDirection direction = FlowDirection::Forward);
NEW: Editor can now return number of submitted nodes (#81)
New API:
int GetNodeCount(); // Returns number of submitted nodes since Begin() call
NEW: Editor can now return nodes in order they are drawn (#81)
New API:
int GetOrderedNodeIds(NodeId* nodes, int size); // Fills an array with node id's in order they're drawn; up to 'size` elements are set. Returns actual size of filled id's.
NEW: Editor now allow to set Z position for nodes (#109)
Nodes with higher Z position are drawn on top of ones with lower.
New API:
void SetNodeZPosition(NodeId nodeId, float z); // Sets node z position, nodes with higher value are drawn over nodes with lower value
float GetNodeZPosition(NodeId nodeId); // Returns node z position, defaults is 0.0f
NEW: Editor: SaveReasonFlags now inform about node creation/deletion
NEW: Editor: Expose button index background was clicked with
New API:
ImGuiMouseButton GetBackgroundClickButtonIndex(); // -1 if none
ImGuiMouseButton GetBackgroundDoubleClickButtonIndex(); // -1 if none
NEW: Editor: Expose configuration editor was created with
New API:
const Config& GetConfig(EditorContext* ctx = nullptr);
NEW: Editor: Add highlighting of Links connected to selected Node (#175)
New API:
StyleColor_HighlightLinkBorder
StyleVar_HighlightConnectedLinks
NEW: Editor: Add ability to snap link origin to pin direction (#167)
New API:
StyleVar_SnapLinkToPinDir
NEW: Editor: Add way to override default zoom levels (#174)
New API:
ImVector<float> Config::CustomZoomLevels;
NEW: Editor: Add canvas size mode (#170)
Config can now decide how editor should resize view when changing size.
New API:
enum class CanvasSizeMode;
Config::CanvasSizeMode;
BUGFIX: Avoid crash while destroying editor.
BUGFIX: Save draw list used by editor between Begin() and End()
There is a chance ImGui::GetWindowDrawList() will return different draw list
while nodes are being composed. To avoid troubles of manipulating incorrect
draw list one obtained in Begin() is remembered and used.
BUGFIX: Avoid division by zero in ImCubicBezierBoundingRect
BUGFIX: Don't stuck in delete action if user does not handle it
BUGFIX: Enable use channel splitter inside Begin/End for node and pin. #28
BUGFIX: Don't manipulate channels when editor is suspended #28
BUGFIX: Fix ObjectId serialization
BUGFIX: GroupNode resize instead of move on low zoom #87
BUGFIX: Make Canvas work with Viewports (#91, #90)
BUGFIX: Explicitly choose embedded GL3W as OpenGL extension loader (#96)
BUGFIX: Application: Don't apply HiDPI logic for (-FLT_MAX,-FLT_MAX) mouse position
BUGFIX: Editor: Clamp over-the-edge drag distance to prevent scrolling to infinity on focus lost
BUGFIX: Editor: Consume scroll event (#73) (require ImGui 17909 or later)
BUGFIX: Editor: Respect window focus while handling actions (#99)
BUGFIX: Examples: Correct case of `data` directory (#97)
BUGFIX: Canvas: Save/Restore CursorMaxPos only in Begin/End (#101)
BUGFIX: Editor: Don't implicitly capture keyboard (#83)
BUGFIX: Application: Reset key down state after loosing keyboard focus
BUGFIX: Editor: Make off-screen dragging work again
BUGFIX: ImGui: Disable obsolete functions (#103)
BUGFIX: Editor: Allow nodes with zero size (#134)
BUGFIX: Canvas: Update call ImGui::IsClippedEx() on ImGui > 18415 (#138)
BUGFIX: Canvas: Disable pink debug outline around widget (#150)
BUGFIX: Editor: Remove node settings when it is explicitly deleted (#153)
BUGFIX: Editor: Improve link dragging with fast movement (#156)
BUGFIX: Editor: Make selection rect start at click point
BUGFIX: Editor: Make selection rect sharp
BUGFIX: Editor: Don't populate unused channels with empty draw command, fixes memory leak (#168, #165)
BUGFIX: Application: Correctly set DX11 View for NULL textures (#162)
BUGFIX: Application: Recreate DX11 resources lazily (related to #162)
BUGFIX: Editor: Don't steal input from active user widget (#172)
BUGFIX: Editor: Delete item from internal list only when action accepts (#178)
BUGFIX: Editor: Cycle canvas to correctly restore view on first frame (#159)
BUGFIX: Editor: Don't relay on ImGui CursorMaxPos to apply padding (https://github.com/ocornut/imgui/issues/5548)

View File

@ -0,0 +1,136 @@
# Documentation Index
## Quick Start
- **[Quick Start Guide](../CONSOLE_VARIANT_QUICKSTART.md)** - Get up and running fast
- **[Debugging Guide](../DEBUGGING.md)** - How to debug GUI and console variants
## Console Variants
- **[Console Variants Guide](console-variants.md)** - Full guide to console variants
- **[CMake Options](cmake-options.md)** - All configuration options
- **[Console Variant Setup](CONSOLE_VARIANT_SETUP.md)** - Implementation details
## CLI/Headless Mode
- **[CLI Architecture](cli.md)** ⭐ - Architectural options for true CLI mode
- **[CLI Implementation Example](cli-implementation-example.md)** - Concrete implementation guide
## Feature Guides
- **[Groups & Containers](groups-container-api.md)** - Container system API
- **[Groups Architecture](groups-container-architecture.md)** - Container system architecture
- **[Waypoints](waypoints.md)** - Waypoint system
- **[Shortcuts](shortcuts.md)** - Keyboard shortcuts
- **[Screenshots](screen.md)** - Taking screenshots
- **[MCP Screenshots](taking-screenshots-mcp.md)** - Using MCP for screenshots
- **[Plugins](plugins-js.md)** - Plugin system
## Technical Documentation
- **[Classes Documentation](../docs-classes.md)** - Class reference
- **[Complete Solution Summary](COMPLETE_SOLUTION_SUMMARY.md)** - Full solution overview
- **[Implementation Summary](IMPLEMENTATION_SUMMARY.md)** - Implementation details
- **[TODO](TODO.md)** - Future enhancements
## Test Reports
- **[Window State Test](WINDOW_STATE_TEST_REPORT.md)** - Window state persistence tests
- **[Zoom to Content Fix](ZOOM_TO_CONTENT_FIX.md)** - Zoom functionality fixes
## Current State (Nov 2025)
### Console Variants ✅
- **GUI Variant**: Standard Windows application with console attachment
- **Console Variant**: Always-visible console for debugging
- **Visual Studio**: Console variant is default startup project
- **Build Scripts**: Support for both variants
### CLI Mode 🚧
- **Status**: Architecture documented, implementation ready
- **Options**: 5 architectural approaches analyzed
- **Recommendation**: Start with Option 1 (Headless Flag) for quick wins
- **Next Step**: Implement headless mode following [cli-implementation-example.md](cli-implementation-example.md)
## Architecture Summary
```
Current Architecture:
blueprints-example.exe (GUI with console attach)
blueprints-example-console.exe (GUI + always-visible console)
Proposed CLI Architecture:
--headless flag → RunCLI() (No UI, true CLI mode)
(default) → App.Run() (Full UI as before)
```
## Quick Reference
### Build Commands
```bash
# GUI variant
./scripts/build.sh Debug
# Console variant
./scripts/build.sh Debug console
# Both
cmake --build build --config Debug
```
### Run Commands
```bash
# GUI
./build/bin/blueprints-example_d.exe --file graph.json
# Console
./build/bin/blueprints-example-console_d.exe --file graph.json
# CLI (once implemented)
./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command validate
```
### Visual Studio
```bash
# Open solution (console variant is default startup)
start build\imgui-node-editor.sln
# Press F5 to debug console variant
```
### CMake Options
```bash
# Default: Both variants, console as startup
cmake -S examples -B build
# GUI only
cmake -S examples -B build -DBUILD_CONSOLE_VARIANTS=OFF
# Both, GUI as startup
cmake -S examples -B build -DUSE_CONSOLE_AS_STARTUP=OFF
```
## Getting Started with CLI Mode
1. Read **[CLI Architecture](cli.md)** to understand options
2. Follow **[CLI Implementation Example](cli-implementation-example.md)** for step-by-step guide
3. Start with Option 1 (Headless Flag) - minimal changes, maximum benefit
4. Test with: `--headless --file graph.json --command validate`
5. Refactor to Option 3 or 4 later if needed
## Key Decisions Made
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Console Variants | Build both by default | Better development experience |
| VS Startup Project | Console variant | Immediate console output for debugging |
| Entry Point | Shared `entry_point.cpp` | Reuse CLI parsing, single codebase |
| CLI Mode | Headless flag (recommended) | Quick to implement, validates use cases |
## Contributing
When adding new features:
- GUI features → Add to `app.cpp` / `app-*.cpp`
- CLI commands → Add to `blueprints-cli.cpp` (once created)
- Core logic → Extract to separate files for reuse
- Document in appropriate guide above

View File

@ -0,0 +1,14 @@
In random order:
* Documentation: Make one
Done:
* ~~ImGui: Factor out changes to ImGui to use vanilla version.~~
* ~~Editor: Fix variable naming (mainly add `m_` prefix)~~
* ~~Editor: Split NodeEditorImpl.cpp to multiple files, file has grown too big.~~
* ~~Editor: Factor out use of `picojson.h`~~
* ~~Editor: Move use of `<optional>` to optional code extensions~~
#57 - join `ax::NodeEditor::EditorContext` with `struct EditorContext` and remove `reinterpret_cast<>`

View File

@ -0,0 +1,571 @@
# CLI Architecture Options
## Current State
Currently, both GUI and console variants create the full UI:
- Console variant = GUI variant + visible console window
- All ImGui/rendering initialization still happens
- Window is created even if you just want to process files
**Problem:** No true headless/CLI mode for automation, testing, or server environments.
## Goal
Enable true CLI operation:
- Process graphs without creating windows
- Validate, convert, export without UI overhead
- Reuse existing CLI argument parsing from `entry_point.cpp`
- Keep GUI and CLI code maintainable
## Architecture Options
### Option 1: Headless Mode Flag ⭐ (Recommended for Quick Implementation)
**Concept:** Add a `--headless` flag that skips all UI initialization.
**Pros:**
- Minimal code changes
- Reuses all existing infrastructure
- Single codebase for both modes
- Easy to maintain
**Cons:**
- Application class still assumes UI might exist
- Some refactoring needed to make UI optional
- Not the cleanest separation
**Implementation:**
```cpp
// blueprints-example.cpp
int Main(const ArgsMap& args)
{
// Check for headless mode
bool headless = false;
auto it = args.find("headless");
if (it != args.end() && it->second.Type == ArgValue::Type::Bool && it->second.Bool) {
headless = true;
}
if (headless) {
// CLI mode - no UI
return RunCLI(args);
} else {
// GUI mode - existing code
App example("Blueprints", args);
if (example.Create())
return example.Run();
return 0;
}
}
int RunCLI(const ArgsMap& args)
{
// Load graph
std::string filename = GetStringArg(args, "file", "");
if (filename.empty()) {
fprintf(stderr, "Error: --file required in headless mode\n");
return 1;
}
// Process without UI
GraphState graph;
if (!graph.Load(filename)) {
fprintf(stderr, "Error: Failed to load %s\n", filename.c_str());
return 1;
}
// Perform operations
std::string command = GetStringArg(args, "command", "validate");
if (command == "validate") {
return ValidateGraph(graph);
} else if (command == "export") {
std::string output = GetStringArg(args, "output", "");
return ExportGraph(graph, output);
}
// ... more commands
return 0;
}
```
**Entry point update:**
```cpp
// entry_point.cpp - add to CLI parser
app.add_option("--headless", "Run without GUI")->capture_default_str();
app.add_option("--command", "Command to execute (validate, export, etc.)")->capture_default_str();
app.add_option("--output", "Output file path")->capture_default_str();
```
**Usage:**
```bash
# Validate graph
blueprints-example-console.exe --headless --file graph.json --command validate
# Export to different format
blueprints-example-console.exe --headless --file graph.json --command export --output graph.xml
# GUI mode (existing)
blueprints-example.exe --file graph.json
```
---
### Option 2: Separate CLI Executable
**Concept:** Create `blueprints-cli` as a completely separate executable.
**Pros:**
- Clean separation
- No UI dependencies in CLI build
- Smaller executable for CLI
- Clear intent (different binary = different purpose)
**Cons:**
- Code duplication risk
- Two build targets to maintain
- Shared code must be extracted to library
**Implementation:**
```cmake
# CMakeLists.txt
add_library(blueprints-core STATIC
core/graph_state.cpp
blocks/block.cpp
# ... all non-UI code
)
# GUI application
add_example_executable(blueprints-example
blueprints-example.cpp
app.cpp
# ... UI files
)
target_link_libraries(blueprints-example PRIVATE blueprints-core)
# CLI application
add_executable(blueprints-cli
cli/blueprints-cli.cpp
cli/commands.cpp
)
target_link_libraries(blueprints-cli PRIVATE blueprints-core)
# Note: No imgui, no application library
```
```cpp
// cli/blueprints-cli.cpp
int main(int argc, char** argv)
{
CLI::App app{"Blueprints CLI"};
std::string file;
std::string command;
std::string output;
app.add_option("-f,--file", file, "Input graph file")->required();
app.add_option("-c,--command", command, "Command")->required();
app.add_option("-o,--output", output, "Output file");
CLI11_PARSE(app, argc, argv);
// Execute command
return ExecuteCommand(command, file, output);
}
```
**Usage:**
```bash
blueprints-cli -f graph.json -c validate
blueprints-cli -f graph.json -c export -o graph.xml
blueprints-example --file graph.json # GUI
```
---
### Option 3: Application Base Class Architecture ⭐⭐ (Recommended for Long-term)
**Concept:** Separate core logic from presentation.
**Pros:**
- Clean architecture
- Core testable without UI
- Natural separation of concerns
- Easy to add new frontends (Web, TUI, etc.)
**Cons:**
- Requires significant refactoring
- More files to maintain
- Need to identify what's "core" vs "UI"
**Implementation:**
```cpp
// core/blueprints_engine.h
class BlueprintsEngine {
public:
BlueprintsEngine();
virtual ~BlueprintsEngine() = default;
// Core operations (no UI)
bool LoadGraph(const std::string& filename);
bool SaveGraph(const std::string& filename);
bool ValidateGraph();
bool ExecuteGraph();
std::string ExportGraph(const std::string& format);
GraphState& GetGraph() { return m_Graph; }
protected:
GraphState m_Graph;
// ... core state
};
// app/blueprints_app.h
class BlueprintsApp : public BlueprintsEngine {
public:
BlueprintsApp(const char* name, const ArgsMap& args);
// UI operations
bool Create();
int Run();
void OnFrame(float deltaTime);
private:
Application m_Application;
ax::NodeEditor::EditorContext* m_Editor;
// ... UI state
// UI methods
void RenderNodes();
void HandleInput();
};
// cli/blueprints_cli.h
class BlueprintsCLI : public BlueprintsEngine {
public:
BlueprintsCLI(const ArgsMap& args);
int Execute();
private:
ArgsMap m_Args;
int CommandValidate();
int CommandExport();
int CommandExecute();
};
```
```cpp
// blueprints-example.cpp (GUI)
int Main(const ArgsMap& args)
{
BlueprintsApp app("Blueprints", args);
if (app.Create())
return app.Run();
return 0;
}
// blueprints-cli.cpp (CLI)
int Main(const ArgsMap& args)
{
BlueprintsCLI cli(args);
return cli.Execute();
}
```
**Usage:**
```bash
# Same executable, mode determined by arguments
blueprints-example-console.exe --headless --file graph.json --command validate
blueprints-example.exe --file graph.json # GUI mode
```
---
### Option 4: Composition Pattern
**Concept:** Core engine is a separate component, Application "has-a" engine.
**Pros:**
- Very clean separation
- Engine easily testable
- Can swap out UI completely
- Multiple UIs can share same engine
**Cons:**
- More indirection
- Need to manage engine lifetime
- Communication between engine and UI
**Implementation:**
```cpp
// core/graph_engine.h
class GraphEngine {
public:
struct Config {
std::string filename;
bool autoSave = false;
// ... options
};
GraphEngine(const Config& config);
bool Load();
bool Save();
bool Validate();
void Execute();
// Query state
const GraphState& GetState() const { return m_State; }
// Modify state
void AddNode(const NodeConfig& config);
void RemoveNode(NodeId id);
void AddLink(PinId from, PinId to);
private:
Config m_Config;
GraphState m_State;
};
// app/blueprints_app.h
class BlueprintsApp {
public:
BlueprintsApp(std::unique_ptr<GraphEngine> engine);
bool Create();
int Run();
private:
std::unique_ptr<GraphEngine> m_Engine;
Application m_Application;
// UI renders m_Engine->GetState()
};
// cli/blueprints_cli.h
class BlueprintsCLI {
public:
BlueprintsCLI(std::unique_ptr<GraphEngine> engine, const ArgsMap& args);
int Execute();
private:
std::unique_ptr<GraphEngine> m_Engine;
ArgsMap m_Args;
};
```
```cpp
// blueprints-example.cpp
int Main(const ArgsMap& args)
{
bool headless = GetBoolArg(args, "headless", false);
GraphEngine::Config config;
config.filename = GetStringArg(args, "file", "");
auto engine = std::make_unique<GraphEngine>(config);
if (headless) {
BlueprintsCLI cli(std::move(engine), args);
return cli.Execute();
} else {
BlueprintsApp app(std::move(engine));
if (app.Create())
return app.Run();
return 0;
}
}
```
---
### Option 5: Command Pattern
**Concept:** CLI commands are objects, both GUI and CLI execute commands.
**Pros:**
- Undo/redo support naturally
- Commands are testable
- GUI and CLI share exact same logic
- Easy to add scripting
**Cons:**
- Everything must be a command
- More abstraction overhead
- Complex for simple operations
**Implementation:**
```cpp
// commands/command.h
class Command {
public:
virtual ~Command() = default;
virtual bool Execute(GraphState& state) = 0;
virtual std::string GetName() const = 0;
};
// commands/validate_command.h
class ValidateCommand : public Command {
public:
bool Execute(GraphState& state) override {
// Validation logic
return ValidateGraphImpl(state);
}
std::string GetName() const override { return "validate"; }
};
// commands/export_command.h
class ExportCommand : public Command {
public:
ExportCommand(const std::string& outputPath, const std::string& format)
: m_OutputPath(outputPath), m_Format(format) {}
bool Execute(GraphState& state) override {
return ExportGraphImpl(state, m_OutputPath, m_Format);
}
std::string GetName() const override { return "export"; }
private:
std::string m_OutputPath;
std::string m_Format;
};
// cli/command_runner.h
class CommandRunner {
public:
CommandRunner(GraphState& state) : m_State(state) {}
bool Run(std::unique_ptr<Command> cmd) {
printf("Executing: %s\n", cmd->GetName().c_str());
return cmd->Execute(m_State);
}
private:
GraphState& m_State;
};
```
```cpp
// blueprints-cli.cpp
int Main(const ArgsMap& args)
{
bool headless = GetBoolArg(args, "headless", false);
std::string filename = GetStringArg(args, "file", "");
GraphState state;
if (!state.Load(filename)) {
fprintf(stderr, "Failed to load graph\n");
return 1;
}
if (headless) {
std::string command = GetStringArg(args, "command", "validate");
CommandRunner runner(state);
std::unique_ptr<Command> cmd;
if (command == "validate") {
cmd = std::make_unique<ValidateCommand>();
} else if (command == "export") {
std::string output = GetStringArg(args, "output", "");
std::string format = GetStringArg(args, "format", "json");
cmd = std::make_unique<ExportCommand>(output, format);
}
return runner.Run(std::move(cmd)) ? 0 : 1;
} else {
// GUI mode - commands triggered by UI actions
BlueprintsApp app(state);
return app.Run();
}
}
```
---
## Comparison Matrix
| Criteria | Option 1<br>Headless Flag | Option 2<br>Separate Exe | Option 3<br>Base Class | Option 4<br>Composition | Option 5<br>Commands |
|----------|----------|----------|----------|----------|----------|
| **Implementation Effort** | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐ Moderate | ⭐⭐ Hard | ⭐⭐ Hard | ⭐ Very Hard |
| **Maintainability** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good |
| **Code Reuse** | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
| **Testing** | ⭐⭐ Fair | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
| **Clean Separation** | ⭐⭐ Fair | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great |
| **Binary Size (CLI)** | Large (full UI) | ⭐⭐⭐⭐⭐ Small | Medium | Medium | Medium |
| **Flexibility** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
## Recommended Approach
### Phase 1: Quick Win (Option 1)
Start with **Headless Flag** approach:
- Immediate CLI functionality
- Minimal code changes
- Validates the CLI use cases
### Phase 2: Refactor (Option 3 or 4)
Once CLI use cases are clear:
- Extract core logic
- Either use **Base Class** (if inheritance makes sense) or **Composition** (if more flexibility needed)
- Better testing and separation
### Phase 3: Advanced (Optional)
If you need undo/redo, scripting, or complex workflows:
- Add **Command Pattern** on top of Phase 2
## CLI Argument Reuse
All options can reuse `entry_point.cpp` CLI parsing:
```cpp
// entry_point.cpp - add CLI options
app.add_flag("--headless", "Run without GUI");
app.add_option("--command", "Command to execute")->capture_default_str();
app.add_option("--output", "Output file path")->capture_default_str();
app.add_option("--format", "Output format")->capture_default_str();
app.add_flag("--validate", "Validate graph and exit");
app.add_flag("--execute", "Execute graph and exit");
```
The `ArgsMap` is already passed to `Main()`, so all these arguments are available.
## Example CLI Commands
```bash
# Validate
blueprints-console.exe --headless --file graph.json --command validate
# Execute headless
blueprints-console.exe --headless --file graph.json --command execute
# Export to XML
blueprints-console.exe --headless --file graph.json --command export --output graph.xml --format xml
# Convert format
blueprints-console.exe --headless --file graph.json --command convert --output graph.yaml
# Batch processing
for file in *.json; do
blueprints-console.exe --headless --file "$file" --command validate
done
```
## Next Steps
1. **Decision:** Choose starting approach (recommend Option 1 for quick start)
2. **Identify:** List specific CLI operations needed (validate, export, execute, convert, etc.)
3. **Implement:** Add headless flag and CLI operations
4. **Test:** Verify CLI mode works without creating windows
5. **Refactor:** If needed, move to Option 3 or 4 for better architecture
## See Also
- [Console Variants Guide](console-variants.md)
- [CMake Options](cmake-options.md)
- [Debugging Guide](../DEBUGGING.md)

View File

@ -0,0 +1,99 @@
# CMake Configuration Options
## Console Variant Options
### BUILD_CONSOLE_VARIANTS
**Default:** `ON`
Controls whether console variants of applications are built on Windows.
```bash
# Build both GUI and console variants (default)
cmake -S examples -B build
# Build only GUI variants
cmake -S examples -B build -DBUILD_CONSOLE_VARIANTS=OFF
```
**Effect:**
- `ON`: Creates both `blueprints-example` and `blueprints-example-console` targets
- `OFF`: Creates only `blueprints-example` (GUI variant)
### USE_CONSOLE_AS_STARTUP
**Default:** `ON`
Sets which variant is the default Visual Studio startup project (only applies when `BUILD_CONSOLE_VARIANTS=ON`).
```bash
# Console variant as startup (default)
cmake -S examples -B build -DUSE_CONSOLE_AS_STARTUP=ON
# GUI variant as startup
cmake -S examples -B build -DUSE_CONSOLE_AS_STARTUP=OFF
```
**Effect:**
- `ON`: Press F5 in Visual Studio → debugs console variant
- `OFF`: Press F5 in Visual Studio → debugs GUI variant
## Quick Examples
### For Development (Console Debugging)
```bash
# Default configuration - optimal for development
cmake -S examples -B build -G "Visual Studio 17 2022" -A x64
# Both variants built, console is default startup
# Open build\imgui-node-editor.sln and press F5 to debug
```
### For Production Build
```bash
# Build only GUI variants
cmake -S examples -B build -G "Visual Studio 17 2022" -A x64 \
-DBUILD_CONSOLE_VARIANTS=OFF
# Only blueprints-example target is created
```
### Build Console Variant Only
```bash
# Configure with both variants
cmake -S examples -B build -G "Visual Studio 17 2022" -A x64
# Build only console variant
cmake --build build --config Debug --target blueprints-example-console
# Or use script
./scripts/build.sh Debug console
```
### Prefer GUI for Debugging
```bash
# Build both, but GUI is startup project
cmake -S examples -B build -G "Visual Studio 17 2022" -A x64 \
-DUSE_CONSOLE_AS_STARTUP=OFF
# Open build\imgui-node-editor.sln
# Press F5 → debugs GUI variant
```
## All Options Summary
| Option | Default | Description | Platform |
|--------|---------|-------------|----------|
| `BUILD_CONSOLE_VARIANTS` | `ON` | Build console variants in addition to GUI | Windows only |
| `USE_CONSOLE_AS_STARTUP` | `ON` | Set console variant as VS startup project | Windows only |
## See Also
- [Console Variants User Guide](console-variants.md)
- [Console Variant Setup Details](CONSOLE_VARIANT_SETUP.md)
- [Quick Start Guide](../CONSOLE_VARIANT_QUICKSTART.md)

View File

@ -0,0 +1,69 @@
# Protocol Buffers (Protobuf) Integration
This project supports optional Protocol Buffers integration via [vcpkg](https://vcpkg.io/).
## Installation
Install protobuf via vcpkg:
```bash
vcpkg install protobuf[zlib] protobuf[zlib]:x64-windows
```
## CMake Configuration
### Option 1: Using vcpkg Toolchain (Recommended)
When configuring CMake, specify the vcpkg toolchain file:
```bash
cmake -S applications -B build --toolchain [path to vcpkg]/scripts/buildsystems/vcpkg.cmake
```
Or set it as an environment variable:
```powershell
$env:CMAKE_TOOLCHAIN_FILE = "[path to vcpkg]/scripts/buildsystems/vcpkg.cmake"
cmake -S applications -B build
```
### Option 2: Disable Protobuf (if not needed)
```bash
cmake -S applications -B build -DENABLE_PROTOBUF=OFF
```
## CMake Options
- `ENABLE_PROTOBUF` (default: `ON`) - Enable or disable Protocol Buffers support
## Usage in Code
When protobuf is enabled, the `ENABLE_PROTOBUF` preprocessor macro is defined:
```cpp
#ifdef ENABLE_PROTOBUF
#include <google/protobuf/...>
// Use protobuf APIs
#else
// Fallback code when protobuf is disabled
#endif
```
## Targets
All executables automatically link to protobuf when available:
- `nodehub`
- `nodehub-console`
- `core_tests`
Protobuf is linked via the `protobuf_interface` interface library, which provides:
- `protobuf::libprotobuf` - Main protobuf library
- `protobuf::libprotoc` - Protocol compiler library
- `ENABLE_PROTOBUF=1` compile definition
## References
- [Protobuf CMake README](https://github.com/protocolbuffers/protobuf/blob/main/cmake/README.md)
- [vcpkg Documentation](https://vcpkg.io/)

View File

@ -0,0 +1,105 @@
# Console Application Variants
## Overview
The project now supports building both GUI and console variants of applications on Windows. This is useful for debugging, automation, or when you need console output visible by default.
## Build Configuration
### CMake Option
The `BUILD_CONSOLE_VARIANTS` option (default: `ON`) controls whether console variants are built:
```bash
# Build with console variants (default)
cmake -S examples -B build -G "Visual Studio 16 2019" -A x64
# Build without console variants
cmake -S examples -B build -G "Visual Studio 16 2019" -A x64 -DBUILD_CONSOLE_VARIANTS=OFF
```
### What Gets Built
When `BUILD_CONSOLE_VARIANTS=ON` on Windows:
- **GUI Application**: `blueprints-example_d.exe` (Debug) / `blueprints-example.exe` (Release)
- Windows GUI application (subsystem: WINDOWS)
- Console attaches to parent process if launched from terminal
- Creates its own console window if no parent console exists
- **Console Application**: `blueprints-example-console_d.exe` (Debug) / `blueprints-example-console.exe` (Release)
- Windows console application (subsystem: CONSOLE)
- Always shows console window
- Better for debugging and automation
## Usage
### Running the Console Variant
From PowerShell:
```powershell
# Build
sh ./scripts/build.sh
# Run console variant
./build/bin/blueprints-example-console_d.exe
# Run with arguments
./build/bin/blueprints-example-console_d.exe --file "myfile.json"
```
From Command Prompt:
```cmd
build\bin\blueprints-example-console_d.exe
```
### Visual Studio
Both targets appear in the solution (`build\imgui-node-editor.sln`):
- `blueprints-example` - GUI application
- `blueprints-example-console` - Console application
**By default, `blueprints-example-console` is set as the startup project**, so you can just press F5 to debug the console variant.
To change the default startup project:
```bash
# Use GUI variant as startup project
cmake -S examples -B build -DUSE_CONSOLE_AS_STARTUP=OFF
# Use console variant as startup project (default)
cmake -S examples -B build -DUSE_CONSOLE_AS_STARTUP=ON
```
Or manually in Visual Studio: Right-click either project and select "Set as StartUp Project".
## Differences
| Feature | GUI Variant | Console Variant |
|---------|-------------|-----------------|
| Subsystem | WINDOWS | CONSOLE |
| Console Window | Only if attached/allocated | Always visible |
| Entry Point | WinMain() | main() |
| Use Case | Normal usage | Debugging, automation |
| Logging | Visible if launched from console | Always visible |
## Implementation Details
- Both variants share the same source code
- The console variant defines `_CONSOLE`, which triggers the `main()` entry point in `entry_point.cpp`
- The GUI variant uses `WinMain()` and includes logic to attach/allocate console when needed
- All command-line arguments work the same in both variants
## When to Use Console Variant
- **Debugging**: See printf/std::cout output immediately
- **Automation**: Easier to capture stdout/stderr in scripts
- **Testing**: Better integration with CI/CD pipelines
- **Development**: Quicker access to logs without checking files
## When to Use GUI Variant
- **Distribution**: Normal end-user deployment
- **Production**: Cleaner user experience without console window
- **Default**: Standard usage for GUI applications

View File

@ -0,0 +1,311 @@
# Container API Design
## Overview
Containers manage nodes, links, and nested containers. The app supports multiple root containers (one per file), and each root can contain groups (nested containers).
**Container State Flags:**
- **Hidden**: Container not visible (but still exists)
- **Active/Disabled**: Container enabled or disabled (disabled containers don't execute)
- **Running**: Container is currently executing
- **Error**: Container has encountered an error
## Container Base Class
```cpp
class Container
{
public:
// Identification
ed::NodeId GetID() const { return m_ID; }
const std::string& GetName() const { return m_Name; }
void SetName(const std::string& name) { m_Name = name; }
// Container flags/state
bool IsHidden() const { return m_Flags & ContainerFlag_Hidden; }
void SetHidden(bool hidden);
bool IsActive() const { return !(m_Flags & ContainerFlag_Disabled); }
void SetActive(bool active) { SetDisabled(!active); }
bool IsDisabled() const { return m_Flags & ContainerFlag_Disabled; }
void SetDisabled(bool disabled);
bool IsRunning() const { return m_Flags & ContainerFlag_Running; }
void SetRunning(bool running);
bool HasError() const { return m_Flags & ContainerFlag_Error; }
void SetError(bool error);
// Ownership
std::vector<Node*> m_Nodes; // Nodes in this container
std::vector<Link*> m_Links; // Links in this container (between nodes in this container)
std::vector<Container*> m_Children; // Nested containers (groups)
Container* m_Parent; // Parent container (nullptr for root containers)
// Interface pins (for groups only - root containers have no interface)
std::vector<GroupPin> m_InputFlowPins;
std::vector<GroupPin> m_OutputFlowPins;
std::vector<GroupPin> m_InputParamPins;
std::vector<GroupPin> m_OutputParamPins;
// Virtual connections (group pin → inner node pin)
std::map<ed::PinId, ed::PinId> m_GroupToInnerPins; // Group input pin → Inner input pin
std::map<ed::PinId, ed::PinId> m_InnerToGroupPins; // Inner output pin → Group output pin
// Management
virtual void AddNode(Node* node);
virtual void RemoveNode(Node* node);
virtual void AddLink(Link* link);
virtual void RemoveLink(Link* link);
virtual void AddChildContainer(Container* container);
virtual void RemoveChildContainer(Container* container);
// Query
Node* FindNode(NodeId nodeId);
Link* FindLink(LinkId linkId);
Pin* FindPin(PinId pinId);
Container* FindContainer(NodeId nodeId); // Recursive search (this + children)
bool ContainsNode(NodeId nodeId) const;
bool ContainsLink(LinkId linkId) const;
// Execution (for BehaviorGraph containers)
virtual void Run(App* app); // Execute container contents (only if active)
// Rendering
virtual void Render(App* app, Pin* newLinkPin); // Only if not hidden
// State management
uint32_t GetFlags() const { return m_Flags; }
void SetFlag(ContainerFlags flag, bool value);
bool HasFlag(ContainerFlags flag) const { return (m_Flags & flag) != 0; }
// Serialization
virtual void Serialize(crude_json::value& json) const;
virtual void Deserialize(const crude_json::value& json, App* app);
// Validation
virtual bool CanAddNode(Node* node) const { return true; }
virtual bool CanAddLink(Link* link) const; // Check if link is valid for this container
protected:
ed::NodeId m_ID;
std::string m_Name;
GroupDisplayMode m_DisplayMode; // For groups only
ImVec2 m_Size; // For groups only (ed::Group size)
// Container flags
enum ContainerFlags {
ContainerFlag_Hidden = 1 << 0,
ContainerFlag_Disabled = 1 << 1,
ContainerFlag_Running = 1 << 2,
ContainerFlag_Error = 1 << 3
};
uint32_t m_Flags = 0; // Bitmask of ContainerFlags
};
```
## Root Container
```cpp
class RootContainer : public Container
{
public:
RootContainer(const std::string& filename, int id);
// Root-specific
const std::string& GetFilename() const { return m_Filename; }
void SetFilename(const std::string& filename) { m_Filename = filename; }
bool IsDirty() const { return m_IsDirty; }
void SetDirty(bool dirty) { m_IsDirty = dirty; }
// Root has no interface pins
void Run(App* app) override; // Execute all blocks in root
void Render(App* app, Pin* newLinkPin) override; // Render root + children
private:
std::string m_Filename;
bool m_IsDirty = false;
};
```
## BehaviorGraph Container (Group)
```cpp
class BehaviorGraph : public Container, public ParameterizedBlock
{
public:
BehaviorGraph(int id, const char* name);
// Container interface
void AddNode(Node* node) override;
void RemoveNode(Node* node) override;
bool CanAddLink(Link* link) const override; // Validate group boundaries
// Block interface (from ParameterizedBlock)
void Build(Node& node, App* app) override;
void Render(Node& node, App* app, Pin* newLinkPin) override;
int Run(Node& node, App* app) override; // Propagate to inner blocks
// Container execution
void Run(App* app) override; // Execute inner blocks (called from Run(Node&, App*))
// Group-specific
GroupDisplayMode GetDisplayMode() const { return m_DisplayMode; }
void SetDisplayMode(GroupDisplayMode mode) { m_DisplayMode = mode; }
void AddInputFlowPin(App* app);
void AddOutputFlowPin(App* app);
void AddInputParamPin(App* app, PinType type);
void AddOutputParamPin(App* app, PinType type);
void RemovePin(ed::PinId pinId, App* app);
// Pin mapping
void MapGroupPinToInnerPin(ed::PinId groupPin, ed::PinId innerPin);
void UnmapGroupPin(ed::PinId groupPin);
private:
// Inherits pin vectors and mappings from Container
// Inherits execution logic from ParameterizedBlock
};
```
## App Structure with Multiple Root Containers
```cpp
class App
{
public:
// Root container management
RootContainer* GetActiveRootContainer() { return m_ActiveRootContainer; }
RootContainer* AddRootContainer(const std::string& filename);
void RemoveRootContainer(RootContainer* container);
void SetActiveRootContainer(RootContainer* container);
const std::vector<RootContainer*>& GetRootContainers() const { return m_RootContainers; }
// Node/Link/Pin lookups (search active root, or all roots)
Node* FindNode(NodeId nodeId); // Search all root containers
Link* FindLink(LinkId linkId); // Search all root containers
Pin* FindPin(PinId pinId); // Search all root containers
Container* FindContainerForNode(NodeId nodeId); // Which container owns this node?
// File operations
void LoadGraph(const std::string& filename); // Creates new root container
void SaveGraph(RootContainer* container); // Save specific root container
void SaveAllGraphs(); // Save all root containers
// Node creation (operates on active root)
Node* SpawnBlockNode(const char* blockType, int nodeId = -1);
Node* SpawnParameterNode(PinType paramType, int nodeId = -1, ...);
// Runtime execution
void ExecuteRuntimeStep(); // Execute active root container
// Rendering
void RenderNodes(Pin* newLinkPin); // Render active root container
private:
std::vector<RootContainer*> m_RootContainers;
RootContainer* m_ActiveRootContainer = nullptr;
// Backward compatibility (optional - maintain flattened view of active root)
// Or remove m_Nodes/m_Links entirely and force container API
std::vector<Node> m_Nodes; // Flattened view of active root (if kept)
std::vector<Link> m_Links; // Flattened view of active root (if kept)
};
```
## Key Operations
### Create Group (Move Nodes)
```cpp
void App::CreateGroupFromSelection(const std::vector<NodeId>& selectedNodes)
{
auto* activeRoot = GetActiveRootContainer();
// Create BehaviorGraph container
auto* group = new BehaviorGraph(GetNextId(), "Group");
activeRoot->AddChildContainer(group);
// Move selected nodes from root → group
for (auto nodeId : selectedNodes)
{
Node* node = activeRoot->FindNode(nodeId);
if (node)
{
// Break external links (links to/from nodes outside selection)
BreakExternalLinks(node, selectedNodes);
// Move node
activeRoot->RemoveNode(node);
group->AddNode(node);
}
}
// Move internal links (between selected nodes)
MoveInternalLinks(selectedNodes, activeRoot, group);
}
```
### Find Container for Node
```cpp
Container* App::FindContainerForNode(NodeId nodeId)
{
for (auto* root : m_RootContainers)
{
if (auto* container = root->FindContainer(nodeId))
return container;
}
return nullptr;
}
```
### Render Container Hierarchy
```cpp
void RootContainer::Render(App* app, Pin* newLinkPin)
{
// Render this container's nodes
for (auto* node : m_Nodes)
{
// Render node (existing logic)
RenderNode(node, app, newLinkPin);
}
// Render child containers (groups)
for (auto* child : m_Children)
{
if (auto* group = dynamic_cast<BehaviorGraph*>(child))
{
if (group->GetDisplayMode() == GroupDisplayMode::Expanded)
{
// Render group with ed::Group()
group->Render(app, newLinkPin);
}
else
{
// Render as collapsed node
RenderCollapsedGroup(group, app, newLinkPin);
}
}
}
}
```
## File Structure
```
examples/blueprints-example/
├── containers/
│ ├── container.h (NEW - base Container class)
│ ├── container.cpp (NEW)
│ ├── root_container.h (NEW - RootContainer)
│ ├── root_container.cpp (NEW)
│ └── behavior_graph.h (NEW - BehaviorGraph container)
│ └── behavior_graph.cpp (NEW)
└── (existing files...)
```
## Benefits of This Structure
1. **Multiple Graphs**: App can load multiple files, each is a root container
2. **No Duplication**: Nodes exist in exactly one container
3. **Natural Nesting**: Groups are containers within root containers
4. **Clean Ownership**: Container owns its nodes/links/children
5. **File Association**: Each root container maps to a file
6. **Independent Execution**: Each root container executes independently (or can cross-link?)

View File

@ -0,0 +1,238 @@
# BehaviorGraph Container-Based Architecture
**Core Principle:** All nodes, links, blocks exist within **containers**. Containers manage, run, and render their contents.
**Key Insight:** The app can have **multiple root containers**, each loaded from a separate file. Each root container represents a separate graph/document/workspace.
```
App
├── RootContainer1 (loaded from file1.json)
│ ├── Node A
│ ├── Node B
│ └── BehaviorGraph Container (Group 1)
│ ├── Node C
│ └── BehaviorGraph Container (Nested Group)
│ └── Node D
├── RootContainer2 (loaded from file2.json)
│ ├── Node E
│ └── Node F
└── RootContainer3 (loaded from file3.json)
└── BehaviorGraph Container (Group 2)
└── Node G
```
## Architecture Overview
### Container Hierarchy
- **App Level**
- Has **multiple root containers** (one per loaded file/graph)
- Each root container is a separate workspace/graph
- **Root Container** (one per file/graph)
- Contains top-level nodes for that graph
- Contains top-level groups for that graph
- Loaded from a single file
- **Group Containers** (BehaviorGraph instances)
- Contain inner nodes
- Can contain nested groups
- Have their own pin interface (for external connections)
### Key Benefits
1. **No Duplication**: Each node exists in exactly ONE container
2. **Clear Ownership**: Container owns its nodes/links
3. **Clean Separation**: Containers handle management, execution, rendering
4. **Natural Nesting**: Groups are just containers within containers
5. **Unified Interface**: Root and groups use same container interface
6. **State Management**: Containers have flags (hidden, active/disabled, running, error)
### App Structure Changes
### 2. Group Creation
**After:**
- Create BehaviorGraph container within **current active root container**
- Move selected nodes from active root container → group container
- Move links (internal links stay, external links need group pins)
- Group container owns these nodes now
- Groups belong to the root container they were created in
### 3. Rendering
- Render **active root container** (or all root containers if multi-view)
- Root container renders its nodes
- Root container renders its child containers (groups)
- Each container renders its own contents
- Different root containers can be in different tabs/views
### 4. Runtime Execution
**After:**
- **Active root container** runs (or all if running all graphs)
- Container iterates its nodes
- If node is a container (BehaviorGraph), call container->Run()
- Nested containers run recursively
- Each root container executes independently (or can be cross-container?)
### 5. Serialization
**Before:**
- Save all nodes
- Save groups with `m_ParentGroup` references
- Link up later
**After:**
- **Each root container saves to its own file**
- Save container hierarchy (root container + all nested groups)
- Each container saves its nodes/links
- Natural tree structure per file
- Multiple files = multiple root containers
### Introduces:
- ✅ Clean ownership model
- ✅ Natural nesting (containers in containers)
- ✅ Unified interface (root = container, groups = containers)
- ✅ Easier serialization (tree structure)
- ✅ Cleaner rendering (containers render themselves)
- ✅ State management (hidden, active/disabled, running, error flags)
### New Approach (Container)
```
App::m_RootContainers = [root1, root2]
RootContainer1::m_Nodes = [node1, node2]
RootContainer1::m_Children = [BehaviorGraphContainer1]
BehaviorGraphContainer1::m_Nodes = [node3, node4] // MOVED from root1
RootContainer2::m_Nodes = [node5, node6]
// Nodes exist in exactly ONE container
// No duplication, no parent tracking needed
// Each root container is independent (separate file/graph)
```
# Architecture Decision: Container-Based vs Node-Based
### Core Principle
**Everything lives in a container.** Containers own their contents (nodes, links, nested containers).
```
Root Container (implicit, always exists)
├── Node A
├── Node B
├── BehaviorGraph Container (Group 1)
│ ├── Node C
│ ├── Node D
│ └── BehaviorGraph Container (Nested Group)
│ └── Node E
└── Node F
```
### Key Concepts
1. **Single Ownership**: Each node/link exists in exactly ONE container
2. **Hierarchical**: Containers can contain other containers (natural nesting)
3. **Unified Interface**: Root and groups use the same container API
4. **Clean Separation**: Container handles management, execution, rendering
### Implementation Structure
```cpp
class Container
{
// Ownership
std::vector<Node*> m_Nodes; // Nodes in this container
std::vector<Link*> m_Links; // Links in this container
std::vector<Container*> m_Children; // Nested containers (groups)
// Interface (for groups only - root has no interface)
std::vector<GroupPin> m_InputFlowPins;
std::vector<GroupPin> m_OutputFlowPins;
std::vector<GroupPin> m_InputParamPins;
std::vector<GroupPin> m_OutputParamPins;
// Virtual connections (group pin → inner node pin)
std::map<ed::PinId, ed::PinId> m_GroupToInnerPins;
// Management
void AddNode(Node* node);
void RemoveNode(Node* node);
void AddLink(Link* link);
void RemoveLink(Link* link);
// Execution
void Run(App* app); // Execute container contents
// Rendering
void Render(App* app, Pin* newLinkPin);
// Query
Container* FindContainer(NodeId nodeId);
};
class BehaviorGraph : public Container, public ParameterizedBlock
{
// BehaviorGraph IS a container
// Also acts as a block (for runtime execution)
// Has group-specific UI (collapsed/expanded modes)
};
class App
{
Container* m_RootContainer; // Default container for top-level items
// Convenience methods
Container* GetRootContainer() { return m_RootContainer; }
Container* FindContainerForNode(NodeId nodeId);
// Backward compatibility (optional - can maintain flattened view)
std::vector<Node*> GetAllNodes(); // Recursive flattening
std::vector<Link*> GetAllLinks(); // Recursive flattening
};
```
## Migration Path
### Phase 2: Make BehaviorGraph a Container
1. BehaviorGraph inherits from Container
2. BehaviorGraph also inherits from ParameterizedBlock (block interface)
3. Group creation moves nodes from root → BehaviorGraph container
### Phase 3: Update All Systems
1. **Rendering**: Container-based (root renders self + children)
2. **Execution**: Container-based (root runs, nested containers run recursively)
3. **Serialization**: Container tree structure
4. **Queries**: Container-aware lookup methods
## Benefits Comparison
### Container-Based (New):
- ✅ Single ownership (node owned by exactly one container)
- ✅ No `m_ParentGroup` needed (container owns it)
- ✅ Unified patterns (all containers same interface)
- ✅ Natural serialization (save container tree)
- ✅ Clear ownership (container owns contents)
## Decision: Container-Based Architecture
**Alternative:** Can implement groups first with node-based approach, then refactor later (but creates technical debt).
## Next Steps
1. **Update Implementation Plan** - Reflect container architecture
2. **Design Container API** - Define exact interface
3. **Implement Root Container** - Move existing code
4. **Implement BehaviorGraph Container** - Group functionality
5. **Update All Systems** - Rendering, execution, serialization
See `docs/groups-container-architecture.md` for detailed container design.

View File

@ -0,0 +1,605 @@
# BehaviorGraph (Groups) Implementation Plan - Container Architecture
## Overview
This document outlines the implementation plan for adding **BehaviorGraph** groups using a **container-based architecture**. Groups are implemented as nested containers within root containers.
**Core Principle:** All nodes, links, blocks exist within **containers**. Containers manage, run, and render their contents. The app supports **multiple root containers** (one per loaded file), and each root can contain groups (nested containers).
## Architecture
### Container Hierarchy
```
App
├── RootContainer1 (loaded from file1.json)
│ ├── Node A (top-level)
│ ├── Node B (top-level)
│ └── BehaviorGraph Container (Group 1)
│ ├── Node C (moved here, owned by Group 1)
│ ├── Node D (moved here, owned by Group 1)
│ └── BehaviorGraph Container (Nested Group)
│ └── Node E (owned by nested group)
├── RootContainer2 (loaded from file2.json)
│ └── Node F
└── RootContainer3 (loaded from file3.json)
└── BehaviorGraph Container (Group 2)
└── Node G
```
### Key Concepts
1. **Container Base Class**: Base class for all containers (root and groups)
2. **Root Container**: One per file/graph, contains top-level nodes and groups
3. **BehaviorGraph Container**: Groups that inherit from Container + ParameterizedBlock
4. **Single Ownership**: Each node/link exists in exactly ONE container
5. **No Duplication**: Nodes moved between containers, not copied
## Data Structures
### BehaviorGraph Container
**File:** `examples/blueprints-example/containers/behavior_graph.h`
```cpp
class BehaviorGraph : public Container, public ParameterizedBlock
{
public:
BehaviorGraph(int id, const char* name);
// Container interface
void AddNode(Node* node) override;
void RemoveNode(Node* node) override;
bool CanAddLink(Link* link) const override; // Validate group boundaries
// Block interface (from ParameterizedBlock)
void Build(Node& node, App* app) override;
void Render(Node& node, App* app, Pin* newLinkPin) override;
int Run(Node& node, App* app) override; // Propagate to inner blocks
// Container execution (called from Run(Node&, App*))
void Run(App* app) override; // Only if active, sets Running flag during execution
// Group-specific
enum class GroupDisplayMode { Expanded, Collapsed };
GroupDisplayMode GetDisplayMode() const { return m_DisplayMode; }
void SetDisplayMode(GroupDisplayMode mode) { m_DisplayMode = mode; }
// Pin management
void AddInputFlowPin(App* app);
void AddOutputFlowPin(App* app);
void AddInputParamPin(App* app, PinType type);
void AddOutputParamPin(App* app, PinType type);
void RemovePin(ed::PinId pinId, App* app);
// Pin mapping (virtual connections)
void MapGroupPinToInnerPin(ed::PinId groupPin, ed::PinId innerPin);
void UnmapGroupPin(ed::PinId groupPin);
private:
GroupDisplayMode m_DisplayMode = GroupDisplayMode::Expanded;
ImVec2 m_GroupSize; // For ed::Group() size
};
```
### GroupPin Structure
```cpp
struct GroupPin
{
ed::PinId ID;
std::string Name;
PinType Type;
PinKind Kind; // Input or Output
bool IsFlow; // true = flow pin, false = parameter pin
};
```
## Implementation Phases
### Phase 2: BehaviorGraph Container (Groups)
#### Task 2.1: Create BehaviorGraph Container
**Files:** `containers/behavior_graph.h`, `containers/behavior_graph.cpp`
- Define `BehaviorGraph` class inheriting from `Container` and `ParameterizedBlock`
- Add `GroupDisplayMode` enum
- Implement basic `Build()`, `Render()`, `Run()` stubs
- Add pin management methods (AddInputFlowPin, etc.)
- Store pins in node's Inputs/Outputs vectors (for ParameterizedBlock compatibility)
**Dependencies:** Task 1.1, `block.h`
#### Task 2.2: Implement Variable Pin System
**Files:** `containers/behavior_graph.cpp`
- Implement `AddInputFlowPin()`, `AddOutputFlowPin()`
- Implement `AddInputParamPin()`, `AddOutputParamPin()`
- Store pins in container's pin vectors AND node's Inputs/Outputs vectors
- Pin editing similar to `parameter_node.h` (edit name, type, value)
- Update `Build()` to register pins with node editor
**Dependencies:** Task 2.1
#### Task 2.3: Implement Pin Removal
**Files:** `containers/behavior_graph.cpp`
- Implement `RemovePin()` method
- Handle link cleanup when pin removed
- Remove from both container vectors and node vectors
- Update virtual connection mappings
**Dependencies:** Task 2.2
#### Task 2.4: Context Menu for Pin Management
**Files:** `containers/behavior_graph.cpp`
- Extend `OnMenu()` (from ParameterizedBlock) to show:
- "Add Input Flow Pin"
- "Add Output Flow Pin"
- "Add Input Parameter Pin" (with type selection)
- "Add Output Parameter Pin" (with type selection)
- "Remove Pin" submenu (list all pins)
- Handle pin creation/removal from menu
**Dependencies:** Task 2.2, 2.3
### Phase 3: Display Modes
#### Task 3.1: Implement Expanded Mode Rendering
**Files:** `containers/behavior_graph.cpp`
- Render group container with title bar
- Use `ed::Group(m_GroupSize)` to define group bounds
- Render flow pins on left/right edges (using `FlowPinRenderer`)
- Render parameter pins on top/bottom edges (using `ParameterPinRenderer`)
- Render inner nodes (from `m_Nodes`) inside group bounds
- Allow user interaction inside group
**Reference:** `ref/sample.cpp` lines 1397-1421
**Dependencies:** Task 2.1
#### Task 3.2: Implement Collapsed Mode Rendering
**Files:** `containers/behavior_graph.cpp`
- Render as compact node (similar to block nodes)
- Show group name/title
- Render pins only (no inner nodes visible)
- Use `FlowPinRenderer` and `ParameterPinRenderer` for compact display
- Inner nodes not visible but remain in container (positions preserved)
**Dependencies:** Task 3.1
#### Task 3.3: Mode Toggle
**Files:** `containers/behavior_graph.cpp`
- Add mode toggle to context menu
- Add keyboard shortcut (optional: 'T' key)
- Store mode in container state
- Persist in serialization
**Dependencies:** Task 3.1, 3.2
#### Task 3.4: Implement Container Flags
**Files:** `containers/container.cpp`, `containers/behavior_graph.cpp`
- Implement flag management (hidden, active/disabled, running, error)
- **Hidden**: Container not rendered (but still exists)
- **Active/Disabled**: Disabled containers don't execute (skip Run())
- **Running**: Set during execution, cleared after (visual feedback)
- **Error**: Set when container encounters error (visual feedback)
- Update Render() to check IsHidden()
- Update Run() to check IsActive() / IsDisabled()
- Visual indicators for running/error states (e.g., red border for error, pulsing for running)
**Dependencies:** Task 1.1
### Phase 4: Group Creation
#### Task 4.1: Implement 'g' Key Shortcut for Grouping
**Files:** `app-render.cpp`, `app-logic.cpp`
- In `HandleKeyboardShortcuts()`, detect 'g' key press
- Get selected nodes from active root container
- Calculate bounding box of selected nodes
- Create new `BehaviorGraph` container
- **Move selected nodes** from active root container → group container:
- Remove nodes from root container's `m_Nodes`
- Add nodes to group container's `m_Nodes`
- **Break external links** (links from/to selected nodes to/from external nodes)
- Move internal links (between selected nodes) to group container
- Add group container to root container's `m_Children`
- Set group size to encompass selected nodes (nodes keep absolute positions)
**Dependencies:** Task 2.1, 1.3
#### Task 4.2: Link Breaking Logic
**Files:** `app-logic.cpp`
- When creating group, scan all links
- For links connecting selected nodes to external nodes:
- Remove link from root container
- Store for potential group pin creation later
- For links between selected nodes:
- Move link to group container
- Clean up link references
**Dependencies:** Task 4.1
### Phase 5: Pin Connections & Runtime Execution
#### Task 5.0: Implement Container Flags (if not done in Phase 3)
**Files:** `containers/container.cpp`
- Implement flag getters/setters
- Update Render() to respect IsHidden()
- Update Run() to respect IsActive() / IsDisabled()
- Visual feedback for Running and Error states
**Dependencies:** Task 1.1
#### Task 5.1: Implement Virtual Pin Connections
**Files:** `containers/behavior_graph.cpp`, `app-logic.cpp`
- **Group pins act as proxies** - users connect external nodes to group pins
- **Virtual connections** stored in maps (NOT actual Link objects):
- `m_GroupToInnerPins`: Maps group input pin → inner node input pin
- `m_InnerToGroupPins`: Maps inner node output pin → group output pin
- **Link validation:** Prevent inner nodes from connecting directly to external nodes
- Override/extend `CanCreateLink()` in App to reject: inner pin ↔ external pin
- Only allow: group pin ↔ external pin, group pin ↔ inner pin (via mapping), inner pin ↔ inner pin
- **Pin mapping UI:** When user connects group pin to inner pin (or vice versa), store in mapping
**Dependencies:** Task 2.2, 4.1
#### Task 5.2: Implement Run() Method for Flow Execution
**Files:** `containers/behavior_graph.cpp`
- Implement `Run(Node&, App*)` method (required for ParameterizedBlock):
**Step 1: Parameter propagation FIRST**
- Copy parameter values from group input pins to connected inner pins
- Use existing `GetInputParamValue*` helpers on group pins
- Use existing `SetOutputParamValue*` helpers (but apply to inner node pins)
- Use `m_GroupToInnerPins` mapping to find target inner pins
**Step 2: Flow activation**
- When group flow input is activated (via `IsInputActive()`):
- Find inner node connected via `m_GroupToInnerPins` mapping
- Activate the corresponding inner node's flow input (by index)
- Multiple activated inputs propagate independently
**Step 3: Output collection**
- After inner blocks execute (may be in next iteration), check inner node output states
- Iterate through `m_Nodes` (inner nodes)
- For each inner node output that's active, find mapped group output pin (via `m_InnerToGroupPins`)
- Activate corresponding group output pin
- Call `Run(App*)` from `Run(Node&, App*)` to execute inner blocks
**Execution Flow:**
1. Iteration N: Group input activated → `Run()` copies params, activates inner inputs → returns
2. Iteration N+1: Runtime detects inner nodes have activated inputs → executes inner blocks
3. Iteration N+2: Group `Run()` called again → collects inner outputs → activates group outputs
**Dependencies:** Task 5.1
#### Task 5.3: Update Runtime to Support Containers
**Files:** `app-runtime.cpp`, `containers/root_container.cpp`
- Update `ExecuteRuntimeStep()` to call `m_ActiveRootContainer->Run(app)` (only if active)
- Update `RootContainer::Run()` to:
- Check `IsActive()` / `IsDisabled()` - skip if disabled
- Set `SetRunning(true)` at start, `SetRunning(false)` at end
- Iterate through its nodes, find blocks with activated inputs
- Execute blocks
- For BehaviorGraph containers (in `m_Children`), call their `Run()` recursively (only if active)
- Container execution propagates through hierarchy naturally
- Respect disabled flag - disabled containers don't execute
**Dependencies:** Task 5.2
### Phase 6: Serialization
#### Task 6.1: Save Container Data
**Files:** `app-logic.cpp`, `containers/container.cpp`, `containers/behavior_graph.cpp`
- Extend `SaveGraph()` to save active root container:
- Container saves its nodes
- Container saves its links
- Container saves its child containers (recursively)
- BehaviorGraph saves pins, virtual mappings, display mode
- Each root container saves to its own file
**Dependencies:** Task 4.1, 5.1
#### Task 6.2: Load Container Data
**Files:** `app-logic.cpp`, `containers/container.cpp`, `containers/behavior_graph.cpp`
- **Loading order:** Nodes first, then links, then containers
- Extend `LoadGraph()` to:
1. Load nodes first (into temporary storage)
2. Load links (reference nodes by ID)
3. Load containers (root container first, then nested):
- Create container
- Assign nodes to container (by ID lookup)
- Assign links to container
- Restore pins, mappings, display mode
4. Establish virtual connections
**Dependencies:** Task 6.1
### Phase 7: Edge Cases & Polish
#### Task 7.1: Handle Edge Cases
**Files:** `containers/behavior_graph.cpp`, `app-logic.cpp`
- **Group nesting is allowed** - groups can contain other groups
- **Node deletion when inside group:**
- When node is deleted, remove from its container's `m_Nodes`
- Clean up any virtual pin mappings involving this node
- **Group deletion:**
- Remove group container from parent's `m_Children`
- Move inner nodes back to parent container (or delete)
- Break all links connected to group pins
- Clean up virtual pin mappings
- **Link deletion:**
- When link involving group pin is deleted, remove from virtual mappings
- When link between inner nodes is deleted, no special handling needed
- **Root container deletion:**
- Remove from `m_RootContainers`
- Delete all nodes/links/containers it owns
**Dependencies:** All previous tasks
#### Task 7.2: Visual Polish
**Files:** `containers/behavior_graph.cpp`
- Improve styling for collapsed vs expanded modes
- Add hover effects
- Add selection highlighting
- Improve pin layout/spacing
- Handle link rendering across group boundaries
**Dependencies:** Task 3.1, 3.2
## File Structure
```
examples/blueprints-example/
├── containers/
│ ├── container.h (NEW - base Container class)
│ ├── container.cpp (NEW)
│ ├── root_container.h (NEW)
│ ├── root_container.cpp (NEW)
│ ├── behavior_graph.h (NEW - BehaviorGraph container)
│ └── behavior_graph.cpp (NEW)
├── blocks/
│ └── (existing files...)
└── (existing files - updated)
```
## Key Implementation Details
### Node Movement (Not Duplication)
When creating a group:
```cpp
// Before: Nodes in root
rootContainer->m_Nodes = [node1, node2, node3]
// After grouping node1, node2:
rootContainer->m_Nodes = [node3]
rootContainer->m_Children = [behaviorGraph1]
behaviorGraph1->m_Nodes = [node1, node2] // MOVED, not duplicated
```
### Link Validation
```cpp
bool App::CanCreateLink(Pin* a, Pin* b)
{
// ... existing validation ...
// Check if pins are in different root containers (reject)
Container* containerA = FindContainerForNode(a->Node->ID);
Container* containerB = FindContainerForNode(b->Node->ID);
if (GetRootContainer(containerA) != GetRootContainer(containerB))
return false; // Cross-root connections not allowed
// Check group boundary violations
if (IsInnerPin(a) && IsExternalPin(b))
return false; // Inner pins can't connect to external pins
if (IsExternalPin(a) && IsInnerPin(b))
return false;
// ... rest of validation ...
}
```
### Container Rendering
```cpp
void RootContainer::Render(App* app, Pin* newLinkPin)
{
// Only render if not hidden
if (IsHidden())
return;
// Render this container's nodes
for (auto* node : m_Nodes)
{
RenderNode(node, app, newLinkPin);
}
// Render links between nodes in this container
for (auto* link : m_Links)
{
RenderLink(link, app);
}
// Render child containers (groups) - only if not hidden
for (auto* child : m_Children)
{
if (child->IsHidden())
continue;
if (auto* group = dynamic_cast<BehaviorGraph*>(child))
{
if (group->GetDisplayMode() == GroupDisplayMode::Expanded)
{
// Render expanded group with ed::Group()
group->Render(app, newLinkPin);
}
else
{
// Render as collapsed node
RenderCollapsedGroup(group, app, newLinkPin);
}
}
}
}
```
### Container Execution
```cpp
void RootContainer::Run(App* app)
{
// Only execute if active (not disabled)
if (IsDisabled())
return;
// Set running flag
SetRunning(true);
// Execute blocks in this container
for (auto* node : m_Nodes)
{
if (node->IsBlockBased() && node->BlockInstance)
{
// Check if input activated, execute, etc. (existing logic)
}
}
// Execute child containers (groups) - only if active
for (auto* child : m_Children)
{
if (child->IsDisabled())
continue;
if (auto* group = dynamic_cast<BehaviorGraph*>(child))
{
// Group's Run() is called by runtime system when group input is activated
// But we can also call it here if needed for recursive execution
}
}
// Clear running flag
SetRunning(false);
}
```
## Final TODO List
### Phase 2: BehaviorGraph Container
4. **Create BehaviorGraph container** (`containers/behavior_graph.h/cpp`)
- Inherit from Container and ParameterizedBlock
- Add GroupDisplayMode
- Implement Build(), Render(), Run() stubs
5. **Implement variable pin management** (`behavior_graph.cpp`)
- AddInputFlowPin, AddOutputFlowPin
- AddInputParamPin, AddOutputParamPin
- RemovePin
- Pin editing (name, type, value)
6. **Implement context menu for pins** (`behavior_graph.cpp`)
- Add/remove pins via menu
- Similar to parameter_node.h patterns
### Phase 3: Display Modes
7. **Implement expanded mode rendering** (`behavior_graph.cpp`)
- Render with ed::Group()
- Render pins and inner nodes
8. **Implement collapsed mode rendering** (`behavior_graph.cpp`)
- Compact node view with pins only
9. **Implement mode toggle** (`behavior_graph.cpp`)
- Context menu and keyboard shortcut
10. **Implement container flags** (`container.cpp`)
- Hidden: Container not rendered
- Active/Disabled: Disabled containers don't execute
- Running: Visual feedback during execution
- Error: Visual feedback for errors
- Update Render() and Run() to respect flags
### Phase 4: Group Creation
10. **Implement 'g' key shortcut** (`app-render.cpp`)
- Detect 'g' key press
- Move selected nodes from root → group container
- Break external links
- Move internal links
### Phase 5: Pin Connections & Runtime
11. **Implement virtual pin connections** (`behavior_graph.cpp`)
- Store mappings (group pin → inner pin)
- Link validation (prevent inner ↔ external)
- Pin mapping UI
12. **Implement Run() method** (`behavior_graph.cpp`)
- Parameter propagation
- Flow activation
- Output collection
13. **Update runtime system** (`app-runtime.cpp`, `root_container.cpp`)
- Container-based execution
- Recursive container running
### Phase 6: Serialization
14. **Implement container serialization** (`container.cpp`, `behavior_graph.cpp`)
- Save container hierarchy
- Load in correct order (nodes → links → containers)
### Phase 7: Polish
16. **Handle edge cases** (`behavior_graph.cpp`, `app-logic.cpp`)
- Node deletion
- Group deletion
- Link deletion
- Root container deletion
17. **Visual polish** (`behavior_graph.cpp`)
- Visual indicators for flags (running, error, disabled, hidden)
- Styling, hover effects, selection
## Notes
- **Container-based architecture** eliminates duplication - nodes owned by exactly one container
- **Multiple root containers** - app supports multiple files, each is a root container
- **BehaviorGraph is a container** - inherits Container + ParameterizedBlock
- **No m_ParentGroup field** - container owns node, find via `FindContainerForNode()`
- **Group pins act as proxies** - virtual connections stored in maps
- **Link validation prevents** inner pins connecting directly to external pins
- **Group nesting allowed** - containers within containers
- **Serialization per file** - each root container saves to its own file
- Reference `app-runtime.cpp` for execution patterns
- Reference `parameter_node.h` for pin editing patterns
- Reference `ref/sample.cpp` for group rendering
- Reference `block.cpp` for pin rendering patterns
## Migration Strategy
1. **Start with container foundation** - Move existing code to use root container
2. **Then add BehaviorGraph** - Groups as nested containers
3. **Incremental migration** - Can maintain flattened view temporarily for compatibility
This architecture provides clean separation, no duplication, and natural multi-file support!

View File

@ -0,0 +1,93 @@
# Block Reference
This document provides a brief overview of the available blocks, their purpose, and their parameters. For a guide on how to create and connect these blocks programmatically, see [Creating Blocks Programmatically](./create-blocks.md).
## Logic Blocks
### [`Logic.Test`](./applications/nodehub/blocks/logic_blocks.cpp)
Compares two input values (`A` and `B`) based on a configurable operator and triggers a `True` or `False` flow output.
- **Flow Inputs**:
- `Execute`
- **Parameter Inputs**:
- `A` (Bool, Int, Float, or String): The first value for comparison.
- `B` (Bool, Int, Float, or String): The second value for comparison.
- **Flow Outputs**:
- `True`: Executed if the comparison is true.
- `False`: Executed if the comparison is false.
- **Configurable**:
- **Parameter Type**: Can be set to Bool, Int, Float, or String.
- **Operator**: Can be set to Equal, Not Equal, Less, Less or Equal, Greater, or Greater or Equal.
## Math Blocks
### [`Math.Add`](./applications/nodehub/blocks/math_blocks.cpp)
Adds two integer values (`A` and `B`) and outputs the sum.
- **Flow Inputs**:
- `Execute`
- **Parameter Inputs**:
- `A` (Int): The first integer.
- `B` (Int): The second integer.
- **Flow Outputs**:
- `Done`
- **Parameter Outputs**:
- `Result` (Int): The sum of `A` and `B`.
### [`Math.Multiply`](./applications/nodehub/blocks/math_blocks.cpp)
Multiplies two integer values (`A` and `B`) and outputs the product.
- **Flow Inputs**:
- `Execute`
- **Parameter Inputs**:
- `A` (Int): The first integer.
- `B` (Int): The second integer.
- **Flow Outputs**:
- `Done`
- **Parameter Outputs**:
- `Result` (Int): The product of `A` and `B`.
### [`Math.Compare`](./applications/nodehub/blocks/math_blocks.cpp)
Compares two integer values (`A` and `B`) and provides boolean outputs for equality, less than, and greater than.
- **Flow Inputs**:
- `Execute`
- **Parameter Inputs**:
- `A` (Int): The first integer.
- `B` (Int): The second integer.
- **Flow Outputs**:
- `Done`
- **Parameter Outputs**:
- `==` (Bool): True if `A` is equal to `B`.
- `<` (Bool): True if `A` is less than `B`.
- `>` (Bool): True if `A` is greater than `B`.
## Utility Blocks
### [`Log`](./applications/nodehub/blocks/log_block.cpp)
Writes a timestamped log entry to a JSON file. It has one fixed input for the file path and can be configured with additional variable parameters to be included in the log.
- **Flow Inputs**:
- `Execute`
- **Parameter Inputs**:
- `FilePath` (String): The path to the output JSON file.
- *(Variable Parameters)*: Additional parameters of type Bool, Int, Float, or String can be added via the context menu.
- **Flow Outputs**:
- `Done`
- **Configurable**:
- Can add/remove variable input parameters.
- Settings for appending to the file vs. overwriting, and for logging to the console.
### [`Start`](./applications/nodehub/blocks/start_block.cpp)
Acts as an entry point for the graph execution. It has no inputs and a single flow output.
- **Flow Inputs**:
- *(None)*
- **Flow Outputs**:
- `Start`

View File

@ -0,0 +1,46 @@
# Command-Line Interface (CLI) Guide
This document provides a guide to using the application's command-line interface for headless execution.
## Basic Usage
The application can be run in a headless (no GUI) mode, which is ideal for scripting, testing, and server environments. The executable is located at `build/bin/nodehub_d.exe`.
The general command structure is:
```bash
./build/bin/nodehub_d.exe [ARGUMENTS]
```
## Key Arguments
The following are some of the most common arguments used for headless execution:
- `--headless`
- **Required** for CLI mode. This flag prevents the GUI from launching.
- `--run`
- Instructs the application to execute the graph's runtime logic.
- `--graph=<path/to/graph.json>`
- Specifies the path to the graph file to load and execute. The path should be relative to the executable's directory (`build/bin/`).
- `--log-level=<level>`
- Sets the verbosity of the console output. Common levels include `info`, `debug`, and `warn`.
- `--max-steps=<number>`
- Defines the maximum number of execution steps to run before the application terminates. This is a safeguard to prevent infinite loops in graphs.
## Example
The following command, taken from [`run-console.sh`](../../run-console.sh), demonstrates a typical headless execution:
```bash
./build/bin/nodehub_d.exe --graph=./BlueprintsGraph.json --run --headless --log-level=info --max-steps=1000
```
This command will:
1. Load the `BlueprintsGraph.json` file.
2. Run the graph in headless mode.
3. Output logs at the `info` level.
4. Stop after a maximum of 1000 execution steps.

View File

@ -0,0 +1,75 @@
# Creating Blocks Programmatically
This guide provides a concise overview of how to add and configure block nodes programmatically in a command-line or testing environment. This is essential for creating automated unit tests. We assume a working `App` instance is available, as set up in headless mode (see `main.cpp`).
## Process Overview
Creating and connecting a block involves several steps:
1. **Get Active Container**: Obtain a pointer to the current `RootContainer` from the `App` instance.
2. **Spawn Nodes**: Create the necessary nodes. For example, to use a `Log` block, you need to spawn the `Log` block itself and also any parameter nodes that will provide its inputs (like the file path).
3. **Configure Parameter Nodes**: Set the values for the parameter nodes you've created.
4. **Connect Nodes**: Find the corresponding input and output pins on the nodes and create a `Link` to connect them.
5. **Set Positions (Optional)**: If you plan to save the graph for visual inspection, set the node positions using `ed::SetNodePosition()`.
6. **Save Graph**: Persist the changes by calling `App::SaveGraph()`, passing the container and a file path.
## Example: Creating and Connecting a `Log` Block
The following function demonstrates adding a "Log" block, providing its required "FilePath" input via a "String" parameter node, connecting them, and saving the graph.
```cpp
#include "app.h"
#include "containers/root_container.h"
#include "blocks/parameter_node.h" // Required for ParameterInstance
// Assumes 'app' is an initialized App instance
void AddAndConnectLogBlock(App& app, const std::string& logFilePath, const std::string& savePath)
{
// 1. Get container
RootContainer* container = app.GetActiveRootContainer();
if (!container)
return;
// 2. Spawn the log block and a parameter node for the file path
Node* logNode = app.SpawnBlockNode("Log", -1);
Node* pathNode = app.SpawnParameterNode(PinType::String, -1);
if (!logNode || !pathNode)
return;
// 3. Configure the parameter node's value
pathNode->StringValue = logFilePath;
if (pathNode->ParameterInstance) {
pathNode->ParameterInstance->SetString(logFilePath.c_str());
}
// 4. Connect the nodes
// The String parameter node has one output.
Pin* pathOutputPin = &pathNode->Outputs[0];
// The Log block's 'FilePath' is its first non-flow input pin.
// Input[0] is 'Execute' (flow), Input[1] is 'FilePath'.
Pin* logInputPin = nullptr;
if (logNode->Inputs.size() > 1) {
logInputPin = &logNode->Inputs[1];
}
if (pathOutputPin && logInputPin)
{
Link newLink(app.GetNextLinkId(), pathOutputPin->ID, logInputPin->ID);
container->AddLink(newLink);
}
// 5. Set positions (optional, requires an editor context)
ed::SetNodePosition(logNode->ID, ImVec2(200.0f, 100.0f));
ed::SetNodePosition(pathNode->ID, ImVec2(0.0f, 100.0f));
// 6. Save the updated graph
app.SaveGraph(savePath, container);
}
```
This more detailed example provides a solid foundation for scripting graph creation for unit tests, ensuring that blocks are not only created but also correctly configured and connected.
## Block Reference
For a complete list of available blocks and their parameters, see the [Block Reference](./blocks.md).

View File

@ -0,0 +1,54 @@
# Testing Guide
This project uses a combination of C++-based unit/functional tests and TypeScript-based end-to-end (E2E) tests to ensure code quality and correctness.
## 1. Unit & Functional Tests (C++/Google Test)
The C++ tests are built on the Google Test framework and are designed to test individual classes and functions in isolation.
- **Location:** All C++ tests are located in the [`applications/tests/`](../../applications/tests/) directory.
- **Test Scaffolding:** A common test fixture is defined in [`applications/tests/commons-test.h`](../../applications/tests/commons-test.h) and [`applications/tests/commons-test.cpp`](../../applications/tests/commons-test.cpp). This scaffolding provides a headless `App` instance, which is useful for testing components that rely on the application context.
- **Example:** See [`applications/tests/block_test.cpp`](../../applications/tests/block_test.cpp) for an example of how to write a test and use the `AppTest` fixture.
### How to Run
To run the entire C++ test suite, use the corresponding `npm` script from the project root:
```bash
npm run test:core
```
This command will:
1. Compile the test executable (`core_tests`).
2. Run the tests using CTest.
### How to Add a New Test
1. Create a new `_test.cpp` file inside the [`applications/tests/`](../../applications/tests/) directory.
2. Include `"commons-test.h"` and any other necessary headers.
3. Write your tests using the Google Test `TEST_F` macro, inheriting from the `AppTest` fixture if you need an application instance.
4. Add the name of your new file to the `add_executable` command in [`applications/tests/CMakeLists.txt`](../../applications/tests/CMakeLists.txt).
## 2. End-to-End Tests (TypeScript/Vitest)
The E2E tests are built with Vitest and are designed to test the compiled command-line application (`nodehub_d.exe`) as a whole.
- **Location:** All E2E tests are located in the [`tests/`](../../tests/) directory at the project root.
- **Test Runner:** A helper function for executing the binary is located in [`tests/commons.ts`](../../tests/commons.ts). This helper manages the command-line arguments and process execution.
- **Example:** See [`tests/e2e.test.ts`](../../tests/e2e.test.ts) for an example of how to use the `runCli` helper to test the application's behavior.
### How to Run
To run the entire E2E test suite, use the corresponding `npm` script from the project root:
```bash
npm run test:e2e:core
```
This command will invoke Vitest, which will discover and run all `*.test.ts` files inside the `tests/` directory.
### How to Add a New Test
1. Create a new `_test.ts` file inside the [`tests/`](../../tests/) directory.
2. Import `describe`, `it`, and `expect` from `"vitest"`, and the `runCli` helper from `"./commons"`.
3. Write your tests. Each test should call `runCli` with the desired command-line arguments and then use `expect` to assert the correctness of the `stdout`, `stderr`, or exit `code`.

View File

@ -0,0 +1,765 @@
# ImGui Node Editor - Architecture Overview
## Table of Contents
1. [Introduction](#introduction)
2. [Core Architecture](#core-architecture)
3. [Editor Context](#editor-context)
4. [Rendering System](#rendering-system)
5. [Pin Rendering](#pin-rendering)
6. [Block System](#block-system)
7. [Link System](#link-system)
8. [Helper Functions](#helper-functions)
---
## Introduction
The imgui-node-editor is a visual node graph editor built on top of Dear ImGui. This document describes the current architecture after recent refactoring that focused on modularity and maintainability.
### Key Characteristics
- **Immediate Mode**: No retained scene graph - everything rebuilt each frame
- **Type-Safe IDs**: `NodeId`, `PinId`, `LinkId` prevent mixing different entity types
- **Modular Design**: Functionality split into focused files (_render, _links, _store, _selection, _tools, _animation)
- **Precise Pin Positioning**: Pin renderers track exact pivot positions and bounds for accurate link connections
---
## Core Architecture
### File Structure
The library is organized into specialized modules:
```
imgui-node-editor/
├── imgui_node_editor.h # Public API
├── imgui_node_editor_internal.h # Internal structures and declarations
├── imgui_node_editor.cpp # Core editor context implementation
├── imgui_node_editor_render.cpp # Rendering logic
├── imgui_node_editor_links.cpp # Link interaction and control points
├── imgui_node_editor_store.cpp # Object storage and queries
├── imgui_node_editor_selection.cpp # Selection management
├── imgui_node_editor_tools.cpp # Utility functions
├── imgui_node_editor_animation.cpp # Animation controllers
└── imgui_node_editor_api.cpp # Public API implementation
```
### Namespace Structure
```cpp
namespace ax::NodeEditor::Detail {
// Internal implementation
class EditorContext { /* ... */ };
// Common alias used throughout
namespace ed = ax::NodeEditor::Detail;
}
```
---
## Editor Context
### EditorContext Class
The heart of the editor is `EditorContext` (defined in `imgui_node_editor_internal.h`), which manages the entire editor state.
**Key Responsibilities**:
- Canvas navigation (pan, zoom)
- Object management (nodes, pins, links)
- User interactions (create, delete, select)
- Drawing coordination
- Animation playback
### The End() Method - Main Frame Coordinator
`void ed::EditorContext::End()` in `imgui_node_editor_render.cpp` is the main orchestrator called at the end of each frame. It coordinates all rendering and interaction logic.
```cpp
void ed::EditorContext::End()
{
auto& io = ImGui::GetIO();
const auto control = BuildControl(IsFocused());
// 1. Update hover/interaction state
UpdateControlState(control);
// 2. Handle control point dragging for guided links
HandleControlPointDragging();
// 3. Draw all visual elements
DrawNodes(); // Node visuals
DrawLinks(); // Link curves
DrawSelectionHighlights(control); // Selected objects
DrawHoverHighlight(control, IsSelecting); // Hovered objects
DrawAnimations(); // Flow animations
// 4. Process user actions
ProcessCurrentAction(control); // Active action (dragging, etc.)
SelectNextAction(control); // Determine next action
// 5. Manage Z-order and channels
BringActiveNodeToFront(control, isDragging);
SortNodesByGroupAndZOrder(sortGroups);
ArrangeNodeChannels();
// 6. Finalize rendering
DrawGrid(); // Background grid
FinalizeDrawChannels(); // Transform clip rects
MergeChannelsAndFinishCanvas(); // Merge and draw border
// 7. Cleanup
PostFrameCleanup(); // Reset state, save settings
}
```
**Location**: `imgui_node_editor_render.cpp:394-445`
### Extracted Helper Methods
The `End()` method was refactored from ~620 lines into focused helper methods:
#### Rendering Helpers (`imgui_node_editor_render.cpp`)
- `UpdateControlState()` - Updates hover and double-click states
- `DrawNodes()` - Renders all visible nodes
- `DrawLinks()` - Renders all visible links
- `DrawSelectionHighlights()` - Highlights selected objects
- `DrawHoverHighlight()` - Highlights hovered object
- `DrawAnimations()` - Renders animation effects
- `DrawGrid()` - Draws background grid
- `ArrangeNodeChannels()` - Manages ImDrawList channels for proper Z-order
- `FinalizeDrawChannels()` - Transforms clip rects for channels
- `MergeChannelsAndFinishCanvas()` - Merges channels and draws border
- `PostFrameCleanup()` - Post-frame cleanup and state reset
#### Link Interaction Helpers (`imgui_node_editor_links.cpp`)
- `HandleControlPointDragging()` - Manages dragging of link control points
- `HandleGuidedLinkInteractions()` - Double-click to add/remove control points
- `AddControlPointToGuidedLink()` - Adds a waypoint to a guided link
- `ConvertLinkToGuidedMode()` - Converts auto link to guided mode
- `ShowControlPointHoverCursor()` - Sets cursor when hovering control points
#### Action Helpers (`imgui_node_editor_render.cpp`)
- `ProcessCurrentAction()` - Processes the currently active action
- `ProcessNavigateAction()` - Handles canvas navigation
- `SelectNextAction()` - Determines next action based on priority
#### Z-Order Helpers (`imgui_node_editor_render.cpp`)
- `BringActiveNodeToFront()` - Brings active node/group to front
- `SortNodesByGroupAndZOrder()` - Sorts nodes for correct rendering order
---
## Rendering System
### Drawing Channels
The editor uses ImGui's channel system to control draw order:
```cpp
// Channel allocation (from imgui_node_editor.cpp)
const int c_BackgroundChannel_SelectionRect = 0; // Selection rectangle
const int c_UserChannel_Content = 1; // User canvas content
const int c_UserChannel_Grid = 2; // Background grid
const int c_UserChannel_HintsBackground = 3; // Hint backgrounds
const int c_UserChannel_Hints = 4; // Hint content
const int c_LinkChannel_Selection = 5; // Selected links
const int c_LinkChannel_Links = 6; // Regular links
const int c_LinkChannel_Flow = 7; // Flow animations
const int c_LinkChannel_NewLink = 8; // Link being created
const int c_NodeStartChannel = 9; // First node channel
// Each node gets 2 channels (background + foreground)
const int c_ChannelsPerNode = 2;
```
### Node Rendering
Nodes are drawn by the application using the builder pattern. The editor provides the infrastructure:
```cpp
ed::Begin("Node Editor");
// Application draws nodes
for (auto& node : nodes) {
ed::BeginNode(node.ID);
// ... custom rendering ...
ed::EndNode();
}
// Application draws links
for (auto& link : links) {
ed::Link(link.ID, link.StartPin, link.EndPin);
}
ed::End(); // Coordinates everything via EditorContext::End()
```
---
## Pin Rendering
### PinRenderer Architecture
The blueprints example uses a specialized pin rendering system for precise positioning.
**Location**: `examples/blueprints-example/utilities/pin_renderer.h/cpp`
### Class Hierarchy
```cpp
// Base class for all pin renderers
class PinRendererBase
{
public:
virtual void BeginPin(ed::PinId id, ed::PinKind kind) = 0;
virtual void EndPin() = 0;
virtual ImVec2 GetPivotPosition() const = 0; // Link connection point
virtual ImRect GetRenderBounds() const = 0; // Hit-test area
virtual ImVec2 GetRelativeOffset() const = 0; // Offset from node origin
protected:
float m_Alpha = 1.0f;
ImVec2 m_LastPivotPosition; // Cached pivot (screen space)
ImRect m_LastRenderBounds; // Cached bounds (screen space)
};
```
### ParameterPinRenderer
Renders data parameter pins (Int, Float, String, etc.) with icons and labels.
```cpp
class ParameterPinRenderer : public PinRendererBase
{
public:
struct Config {
float iconSize = 24.0f;
float iconInnerScale = 0.75f;
float spacing = 4.0f;
bool showLabels = true;
ImVec2 padding = ImVec2(8, 4);
};
void Render(ed::PinId pinId, ed::PinKind kind, const Pin& pin,
bool isLinked, App* app, const Config* overrideConfig = nullptr);
// Returns exact pivot point where links should connect
ImVec2 GetPivotPosition() const override;
// Returns full visual bounds for hit-testing
ImRect GetRenderBounds() const override;
};
```
**Key Features**:
- Renders icon + label for parameter pins
- Automatically positions icon based on pin direction (left/right)
- Tracks exact pivot position for link connection
- Supports alpha blending for inactive pins
- Configurable appearance via `Config` struct
**Usage**:
```cpp
ParameterPinRenderer paramRenderer;
// Render input pin
paramRenderer.Render(pin.ID, ed::PinKind::Input, pin, isLinked, app);
// Store position data for link routing
pin.LastPivotPosition = paramRenderer.GetPivotPosition();
pin.LastRenderBounds = paramRenderer.GetRenderBounds();
pin.HasPositionData = true;
```
### FlowPinRenderer
Renders execution flow pins with square markers at node edges.
```cpp
class FlowPinRenderer : public PinRendererBase
{
public:
struct Config {
float pinSize = 12.0f;
float edgeOffset = 4.0f;
float rounding = 2.0f;
float borderWidth = 2.0f;
ImU32 fillColor = IM_COL32(255, 255, 255, 255);
ImU32 borderColor = IM_COL32(32, 32, 32, 255);
};
void Render(ed::PinId pinId, ed::PinKind kind, const Pin& pin,
App* app, const Config* overrideConfig = nullptr);
};
```
**Key Features**:
- Renders small square at node edge
- Positioned precisely at block boundary
- Distinct visual from parameter pins
- Minimal screen space usage
### Pin Position Tracking
The `Pin` struct stores rendered position data:
```cpp
struct Pin
{
ed::PinId ID;
Node* Node;
std::string Name;
PinType Type;
PinKind Kind;
// Position tracking (updated during rendering)
ImVec2 LastPivotPosition; // Where links connect (screen space)
ImRect LastRenderBounds; // Full pin area for hit testing
bool HasPositionData; // True after first render
// Get position relative to node
ImVec2 GetRelativePivotPosition() const;
};
```
**Why This Matters**: Accurate pin positions are critical for:
- Link waypoint generation
- Link end-point alignment
- Hit-testing during link creation
- Visual polish (no gaps or misalignments)
---
## Block System
### ParameterizedBlock
Represents a standard blueprint-style block with parameters and flow pins.
**Location**: `examples/blueprints-example/blocks/block.cpp`
**Structure**:
```cpp
class ParameterizedBlock : public BlockBase
{
std::vector<Pin> InputParameters; // Data inputs (top area)
std::vector<Pin> OutputParameters; // Data outputs (top area)
Pin FlowInputPin; // Execution input (left edge)
Pin FlowOutputPin; // Execution output (right edge)
};
```
**Rendering Pattern** (lines 25-220):
```cpp
void ParameterizedBlock::Render()
{
ed::BeginNode(ID);
ImGui::BeginVertical("node");
// Header
ImGui::BeginHorizontal("header");
// ... render title ...
ImGui::EndHorizontal();
// Input Parameters (left side)
ImGui::BeginHorizontal("inputs");
ImGui::BeginVertical("input_params");
for (auto& pin : InputParameters) {
ParameterPinRenderer renderer;
renderer.Render(pin.ID, ed::PinKind::Input, pin, isLinked, app);
// Store position for link routing
pin.LastPivotPosition = renderer.GetPivotPosition();
pin.LastRenderBounds = renderer.GetRenderBounds();
pin.HasPositionData = true;
}
ImGui::EndVertical();
// Middle spacer
ImGui::Spring(1);
// Output Parameters (right side)
ImGui::BeginVertical("output_params");
for (auto& pin : OutputParameters) {
ParameterPinRenderer renderer;
renderer.Render(pin.ID, ed::PinKind::Output, pin, isLinked, app);
pin.LastPivotPosition = renderer.GetPivotPosition();
pin.LastRenderBounds = renderer.GetRenderBounds();
pin.HasPositionData = true;
}
ImGui::EndVertical();
ImGui::EndHorizontal();
// Flow Pins (edges)
FlowPinRenderer flowRenderer;
// Flow input (left edge)
flowRenderer.Render(FlowInputPin.ID, ed::PinKind::Input, FlowInputPin, app);
FlowInputPin.LastPivotPosition = flowRenderer.GetPivotPosition();
FlowInputPin.HasPositionData = true;
// Flow output (right edge)
flowRenderer.Render(FlowOutputPin.ID, ed::PinKind::Output, FlowOutputPin, app);
FlowOutputPin.LastPivotPosition = flowRenderer.GetPivotPosition();
FlowOutputPin.HasPositionData = true;
ImGui::EndVertical();
ed::EndNode();
}
```
### ParameterNode
Compact nodes representing variables or constants (no flow pins).
**Location**: `examples/blueprints-example/blocks/parameter_node.cpp`
**Three Display Modes**:
1. **Name Only** (lines 96-154) - Just the variable name
2. **Name + Value** (lines 156-252) - Name with editable value
3. **Small Box** (lines 254-348) - Minimal representation
**Rendering Example** (Name Only mode):
```cpp
void ParameterNode::RenderNameOnly()
{
ed::BeginNode(ID);
// Just the name and output pin
ImGui::BeginHorizontal("content");
ImGui::TextUnformatted(Name.c_str());
ImGui::Spring(0);
// Output pin
ParameterPinRenderer paramRenderer;
paramRenderer.Render(output.ID, ed::PinKind::Output, output, isLinked, app);
// Store position
output.LastPivotPosition = paramRenderer.GetPivotPosition();
output.LastRenderBounds = paramRenderer.GetRenderBounds();
output.HasPositionData = true;
ImGui::EndHorizontal();
ed::EndNode();
}
```
---
## Link System
### Link Structure
```cpp
struct Link
{
ed::LinkId ID;
ed::PinId StartPinID; // Output pin
ed::PinId EndPinID; // Input pin
ImColor Color;
// Guided link waypoints
std::vector<ImVec2> ControlPoints; // User-defined waypoints
bool IsGuided = false; // Guided vs auto-routed
};
```
### Link Modes
**Auto Mode** (default):
- Editor automatically routes link curve
- Uses bezier or spline curves
- No user-defined waypoints
**Guided Mode**:
- User controls link path via waypoints
- Double-click link to add control point
- Drag control points to adjust path
- Double-click control point to remove
### Link Rendering
Links are drawn in `DrawLinks()` (`imgui_node_editor_render.cpp:129-170`):
```cpp
void EditorContext::DrawLinks()
{
for (auto& link : m_Links)
{
if (!link.IsVisible())
continue;
if (link.IsGuided()) {
// Draw guided link with control points
ed::Link(link.ID, link.StartPinID, link.EndPinID,
link.Color, 2.0f, link.ControlPoints);
} else {
// Draw auto-routed link
ed::Link(link.ID, link.StartPinID, link.EndPinID,
link.Color, 2.0f);
}
}
}
```
### Control Point Interaction
**Dragging** (`HandleControlPointDragging()` in `imgui_node_editor_links.cpp:25-50`):
```cpp
void EditorContext::HandleControlPointDragging()
{
if (!m_DraggedControlPoint.IsValid())
return;
// Update position while dragging
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
m_DraggedControlPoint.Position = ImGui::GetMousePos();
}
// Finish drag
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
m_DraggedControlPoint.Clear();
}
}
```
**Adding/Removing** (`HandleGuidedLinkInteractions()` in `imgui_node_editor_links.cpp:52-85`):
```cpp
void EditorContext::HandleGuidedLinkInteractions(const Control& control)
{
if (control.HotLink && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
auto link = FindLink(control.HotLink);
if (!link) return;
if (control.HoveredControlPoint >= 0) {
// Remove control point
link->ControlPoints.erase(
link->ControlPoints.begin() + control.HoveredControlPoint);
} else {
// Add control point at mouse position
AddControlPointToGuidedLink(link, ImGui::GetMousePos());
}
}
}
```
### Waypoint Generation
When creating a link, waypoints are generated to connect pins accurately.
**Critical Function**: `GetPinPosition()` in `app-render.cpp:48-112`
```cpp
static ImVec2 GetPinPosition(Pin* pin, const ImVec2& nodePos, const ImVec2& nodeSize)
{
// Use stored position data from PinRenderer if available
if (pin->HasPositionData)
{
// Get current node position
ImVec2 currentNodePos = ed::GetNodePosition(pin->Node->ID);
// Calculate relative offset
ImVec2 relativeOffset = pin->LastPivotPosition - currentNodePos;
// Apply offset to requested position
// (Handles cases where nodePos != currentNodePos during creation)
return nodePos + relativeOffset;
}
// Fallback to approximation for unrendered pins
// ... (approximation based on node size and pin type) ...
}
```
**Why This Works**:
1. Pin renderers store exact pivot position during rendering
2. Position is stored in screen space
3. Convert to relative offset from node origin
4. Apply offset to current/target node position
5. Result: pixel-perfect link connection
### Link Direction
Link direction is determined by pin type and position:
```cpp
static ImVec2 GetPinDirection(Pin* pin)
{
// Use pin renderer data if available
if (pin->HasPositionData) {
// Calculate from pin position relative to node center
ImVec2 nodePos = ed::GetNodePosition(pin->Node->ID);
ImVec2 nodeSize = ed::GetNodeSize(pin->Node->ID);
ImVec2 nodeCenter = nodePos + nodeSize * 0.5f;
ImVec2 pinPos = pin->LastPivotPosition;
ImVec2 dir = pinPos - nodeCenter;
float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
return len > 0.0f ? ImVec2(dir.x / len, dir.y / len) : ImVec2(1, 0);
}
// Fallback: standard left/right
return pin->Kind == PinKind::Input ? ImVec2(-1, 0) : ImVec2(1, 0);
}
```
---
## Helper Functions
### app-render.cpp Utilities
**Location**: `examples/blueprints-example/app-render.cpp`
Key helper functions used throughout the blueprints example:
#### Pin Queries (lines 48-145)
```cpp
// Get exact pin position for link routing
static ImVec2 GetPinPosition(Pin* pin, const ImVec2& nodePos, const ImVec2& nodeSize);
// Get pin direction vector for link curves
static ImVec2 GetPinDirection(Pin* pin);
// Check if pin has an active connection
bool IsPinLinked(ed::PinId pinId);
```
#### Icon Rendering (lines 147-320)
```cpp
// Draw pin icon (circle, square, flow, etc.)
void DrawPinIcon(const Pin& pin, bool connected, int alpha, const ImVec2& size);
// Get icon color based on pin type
ImColor GetIconColor(PinType type);
// Icon types
enum class IconType { Flow, Circle, Square, Grid, RoundSquare, Diamond };
```
#### Link Rendering (lines 550-890)
```cpp
// Render a single link with proper curve
void DrawLink(const Link& link);
// Render flow animation on link
void DrawFlowAnimation(const Link& link, float time);
// Get link curve points for rendering
std::vector<ImVec2> GetLinkCurve(const Link& link);
```
#### Interaction Helpers (lines 1450-1610)
```cpp
// Handle link creation
void HandleLinkCreation();
// Handle node/link deletion
void HandleDeletion();
// Handle node selection
void HandleSelection();
// Show context menu
void ShowContextMenu();
```
---
## Quick Reference
### Core Editor Flow
```
Frame Start
ImGui::NewFrame()
ed::Begin()
App Renders Nodes/Links
ed::End() → EditorContext::End()
├─ UpdateControlState()
├─ HandleControlPointDragging()
├─ DrawNodes()
├─ DrawLinks()
├─ DrawSelectionHighlights()
├─ DrawHoverHighlight()
├─ DrawAnimations()
├─ ProcessCurrentAction()
├─ SelectNextAction()
├─ BringActiveNodeToFront()
├─ SortNodesByGroupAndZOrder()
├─ ArrangeNodeChannels()
├─ DrawGrid()
├─ FinalizeDrawChannels()
├─ MergeChannelsAndFinishCanvas()
└─ PostFrameCleanup()
ImGui::Render()
Frame End
```
### Key Files Reference
| File | Purpose | Key Functions |
|------|---------|---------------|
| `imgui_node_editor.cpp` | Core editor implementation | `Begin()`, node/link creation |
| `imgui_node_editor_render.cpp` | Rendering coordination | `End()`, `DrawNodes()`, `DrawLinks()` |
| `imgui_node_editor_links.cpp` | Link interactions | `HandleControlPointDragging()`, waypoint management |
| `imgui_node_editor_internal.h` | Internal declarations | `EditorContext` class, all helper declarations |
| `utilities/pin_renderer.h/cpp` | Pin rendering | `ParameterPinRenderer`, `FlowPinRenderer` |
| `blocks/block.cpp` | Standard blocks | `ParameterizedBlock::Render()` |
| `blocks/parameter_node.cpp` | Parameter nodes | `ParameterNode::Render()` modes |
| `app-render.cpp` | Rendering helpers | `GetPinPosition()`, `DrawPinIcon()` |
### Data Flow for Pin Positioning
```
1. App calls pin renderer
ParameterPinRenderer::Render()
2. Renderer calculates layout
Layout layout = CalculateLayout(...)
3. Renderer stores position
m_LastPivotPosition = layout.pivotPoint
m_LastRenderBounds = layout.combinedRect
4. App stores in Pin struct
pin.LastPivotPosition = renderer.GetPivotPosition()
pin.LastRenderBounds = renderer.GetRenderBounds()
pin.HasPositionData = true
5. Link system uses stored position
GetPinPosition(pin) → returns pin.LastPivotPosition
6. Waypoints generated with exact positions
CreateLink() → GenerateWaypoints() → pixel-perfect alignment
```
---
## Summary
The ImGui Node Editor architecture is built around:
1. **EditorContext** - Central coordinator managing all state
2. **Modular Files** - Focused modules for rendering, links, selection, etc.
3. **End() Method** - Main frame orchestrator split into focused helpers
4. **Pin Renderers** - Precise positioning system for perfect link alignment
5. **Block System** - Flexible node types (blocks, parameters, comments)
6. **Link System** - Auto and guided modes with waypoint editing
7. **Helper Functions** - Utilities in app-render.cpp for common operations
This design provides a clean separation of concerns while maintaining the immediate-mode paradigm of Dear ImGui.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,808 @@
# Window State Persistence Investigation
**Date:** November 5, 2025
**Status:** ✅ Investigation Complete + Implementation Verified
**Scope:** Application window position, size, monitor, and node editor canvas state
> **UPDATE:** Window state persistence has been successfully implemented for Win32+DirectX!
> See `WINDOW_STATE_TEST_REPORT.md` for implementation details and test results.
## Executive Summary
~~The application **does NOT restore** OS window state (position, size, monitor) between sessions.~~ **UPDATE: This has been fixed!**
**Current State (November 5, 2025):**
- ✅ **OS Window State:** NOW RESTORED (position, size, monitor) via `Blueprints_window.json`
- ✅ **Node Editor Canvas:** Restored (zoom, panning) via `Blueprints.json`
- ✅ **ImGui UI Windows:** Restored (internal panels) via `Blueprints.ini`
The application now provides **complete session persistence** across all layers!
---
## Findings
### ❌ OS Window State (NOT PERSISTED)
The following OS-level window properties are **not saved or restored**:
1. **Window Position** (screen X, Y coordinates)
2. **Window Size** (width, height)
3. **Monitor Selection** (which display the window was on)
4. **Window State** (maximized, minimized, fullscreen)
#### Evidence
**Win32 Backend** (`platform_win32.cpp:133-159`):
```cpp
bool PlatformWin32::OpenMainWindow(const char* title, int width, int height)
{
m_MainWindowHandle = CreateWindow(
m_WindowClass.lpszClassName,
Utf8ToNative(title).c_str(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, // ← Default position (no persistence)
width < 0 ? CW_USEDEFAULT : width,
height < 0 ? CW_USEDEFAULT : height,
nullptr, nullptr, m_WindowClass.hInstance, nullptr);
// No code to load saved position/size
}
```
**GLFW Backend** (`platform_glfw.cpp:73-167`):
```cpp
bool PlatformGLFW::OpenMainWindow(const char* title, int width, int height)
{
glfwWindowHint(GLFW_VISIBLE, 0);
width = width < 0 ? 1440 : width; // Hardcoded default
height = height < 0 ? 800 : height; // Hardcoded default
m_Window = glfwCreateWindow(width, height, title, nullptr, nullptr);
// No code to load saved position/size/monitor
}
```
**Application Layer** (`application.cpp:89-119`):
```cpp
bool Application::Create(int width /*= -1*/, int height /*= -1*/)
{
if (!m_Platform->OpenMainWindow("NodeHub", width, height))
return false;
// No window position/monitor restoration logic
}
```
### ✅ Node Editor Canvas State (PERSISTED)
The node editor canvas properties **are saved and restored** via `Blueprints.json`:
1. **Canvas Zoom** (`m_ViewZoom`)
2. **Canvas Pan/Scroll** (`m_ViewScroll`)
3. **Visible Rectangle** (`m_VisibleRect`)
4. **Selection State** (which nodes/links are selected)
#### Evidence
**Settings Serialization** (`imgui_node_editor_store.cpp:185-225`):
```cpp
std::string Settings::Serialize()
{
json::value result;
auto& view = result["view"];
view["scroll"]["x"] = m_ViewScroll.x;
view["scroll"]["y"] = m_ViewScroll.y;
view["zoom"] = m_ViewZoom;
view["visible_rect"]["min"]["x"] = m_VisibleRect.Min.x;
// ... etc
}
```
**Save/Restore Implementation** (`app-logic.cpp:877-928`):
```cpp
size_t App::LoadViewSettings(char* data)
{
std::ifstream file("Blueprints.json");
// Loads canvas zoom, pan, selection from JSON
}
bool App::SaveViewSettings(const char* data, size_t size)
{
// Saves only view state (scroll, zoom, visible_rect, selection)
// to Blueprints.json
}
```
**Restoration Logic** (`EditorContext.cpp:2059-2062`):
```cpp
void EditorContext::LoadSettings()
{
m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll;
m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom;
}
```
### ⚠️ ImGui Window State (PARTIAL)
ImGui saves **internal window** positions/sizes to `.ini` files, but this is for ImGui windows (like "Edit Block Parameters", "Style" panel), **NOT the main OS window**.
#### Evidence
**ImGui .ini Format** (`build/bin/Blueprints.ini`):
```ini
[Window][Edit Block Parameters]
Pos=483,145
Size=458,424
Collapsed=0
```
**Application Setup** (`application.cpp:100-105`):
```cpp
m_IniFilename = m_Name + ".ini";
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = m_IniFilename.c_str(); // ← Saves ImGui window state only
```
#### How ImGui Window Persistence Works
**Key Pattern from ImGui Demo** (`imgui_demo.cpp:337-339`):
```cpp
const ImGuiViewport* main_viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20),
ImGuiCond_FirstUseEver); // ← Only applies first time
ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_FirstUseEver);
```
**Important Flags:**
1. **`ImGuiCond_FirstUseEver`** - Position/size applied only on first use, then ImGui remembers it in `.ini`
2. **`ImGuiWindowFlags_NoSavedSettings`** - Explicitly disables saving to `.ini` file
**Examples from imgui_demo.cpp:**
```cpp
// Saved to .ini (uses ImGuiCond_FirstUseEver)
ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
ImGui::Begin("Example: Console", &show);
// NOT saved to .ini (uses ImGuiWindowFlags_NoSavedSettings)
ImGui::Begin("overlay", nullptr,
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDecoration);
```
**Viewport Concepts** (`imgui_demo.cpp:7116-7118`):
```cpp
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 work_pos = viewport->WorkPos; // Work area (excludes taskbar/menubar)
ImVec2 work_size = viewport->WorkSize; // Work area size
```
⚠️ **Critical Note:** `ImGuiViewport` represents the **main rendering area**, not the OS window. It's relative coordinates within the application, not screen coordinates. This is why ImGui can save window positions in the `.ini` file - they're relative to the viewport, not the screen.
---
## Key Insight: OS Window vs ImGui Windows
This is the **fundamental distinction** that explains the current behavior:
### OS Window (The Main Application Window)
- Created by **Win32 `CreateWindow()`** or **GLFW `glfwCreateWindow()`**
- Has **screen coordinates** (absolute position on monitor)
- Managed by **Operating System**
- ❌ **Not controlled by ImGui**
- ❌ **Not saved by ImGui's .ini system**
- Position set once at creation, never queried or restored
### ImGui Windows (Internal UI Panels)
- Created by **`ImGui::Begin()`** calls
- Have **viewport-relative coordinates** (relative to the OS window's client area)
- Managed by **ImGui library**
- ✅ **Controlled by ImGui**
- ✅ **Saved to .ini files automatically** (unless `ImGuiWindowFlags_NoSavedSettings`)
- Position/size remembered via `ImGuiCond_FirstUseEver` pattern
**Visual Representation:**
```
┌─────────────────────────────────────────────────┐ ← OS Window (Win32/GLFW)
│ Screen Pos: (100, 100) ❌ NOT SAVED │ ❌ No persistence
│ Screen Size: (1920, 1080) ❌ NOT SAVED │
│ Monitor: 2 ❌ NOT SAVED │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ImGui Viewport (0,0 relative) │ │
│ │ │ │
│ │ ┌────────────────────┐ ← ImGui Window │ │
│ │ │ "Edit Parameters" │ ✅ Saved to │ │
│ │ │ Pos: (483, 145) │ Blueprints.ini│ │
│ │ │ Size: (458, 424) │ │ │
│ │ └────────────────────┘ │ │
│ │ │ │
│ │ Node Editor Canvas: │ │
│ │ - Zoom: 1.5x ✅ Saved to │ │
│ │ - Pan: (500, 300) ✅ Blueprints.json │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
---
## Technical Architecture
### Persistence Layers
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: OS Window (Win32/GLFW) │
│ ❌ NOT PERSISTED │
│ - Window position (x, y) │
│ - Window size (width, height) │
│ - Monitor selection │
│ - Maximized/fullscreen state │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 2: ImGui UI (.ini file) │
│ ✅ PERSISTED (Blueprints.ini) │
│ - Internal ImGui window positions │
│ - Internal ImGui window sizes │
│ - Collapsed states │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Node Editor Canvas (JSON file) │
│ ✅ PERSISTED (Blueprints.json) │
│ - Canvas zoom level │
│ - Canvas pan/scroll position │
│ - Visible rectangle │
│ - Selection state │
└─────────────────────────────────────────────────────────┘
```
### Key Files
| File | Purpose | Persists |
|------|---------|----------|
| `platform_win32.cpp` | Win32 backend | Nothing |
| `platform_glfw.cpp` | GLFW backend | Nothing |
| `application.cpp` | Application framework | Nothing (delegates to ImGui) |
| `Blueprints.ini` | ImGui window state | Internal UI windows only |
| `Blueprints.json` | Editor state | Canvas zoom, pan, selection |
---
## Root Cause Analysis
### Why is OS window state not saved?
1. **No API calls**: Neither `platform_win32.cpp` nor `platform_glfw.cpp` contain any calls to:
- Query window position (`GetWindowPos`, `glfwGetWindowPos`)
- Save window state to disk
- Restore window state from disk
2. **No storage mechanism**: There is no configuration file or registry entry for OS window state.
3. **Hard-coded defaults**:
- Win32: `CW_USEDEFAULT` (Windows decides placement)
- GLFW: `1440x800` at default position (GLFW decides placement)
4. **Design decision**: The application framework (`Application` class) has no interface for window state persistence. The `Platform` interface only has:
```cpp
virtual bool OpenMainWindow(const char* title, int width, int height) = 0;
```
No parameters for position, monitor, or state.
+++
5. **ImGui can't help**: ImGui's `.ini` persistence system **cannot save OS window state** because:
- ImGui works with **viewport-relative coordinates**, not screen coordinates
- ImGui has no access to OS window APIs (`HWND`, `GLFWwindow*`)
- ImGui's `SetNextWindowPos()` positions **internal UI windows**, not the OS window
- The main "Content" window uses `ImGuiWindowFlags_NoSavedSettings` (line 203 of `application.cpp`)
**Evidence from application.cpp:197-203:**
```cpp
ImGui::SetNextWindowPos(ImVec2(0, 0)); // Always at (0,0) viewport origin
ImGui::SetNextWindowSize(io.DisplaySize); // Always fills entire viewport
ImGui::Begin("Content", nullptr, GetWindowFlags());
// GetWindowFlags() includes ImGuiWindowFlags_NoSavedSettings
```
The main content window is explicitly **not saved** and always fills the entire OS window.
---
## User Impact
### Current Behavior
1. **First Launch**: Window appears at OS default position
2. **Move/Resize**: User positions and sizes the window as desired
3. **Close Application**: Window state is lost
4. **Relaunch**: Window reappears at OS default position (state forgotten)
### Working Features
✅ Node editor canvas remembers zoom and pan position
✅ ImGui internal windows remember their positions
✅ Application loads last-opened graph file
### Missing Features
❌ Main window position not remembered
❌ Main window size not remembered
❌ Monitor selection not remembered (multi-monitor setups)
❌ Maximized state not remembered
---
## Related Code References
### Platform Abstraction
- `examples/application/source/platform.h` - Platform interface (lines 8-61)
- `examples/application/source/platform_win32.cpp` - Win32 implementation
- `examples/application/source/platform_glfw.cpp` - GLFW implementation
### Application Framework
- `examples/application/source/application.cpp` - Main application class
- `examples/application/source/entry_point.cpp` - Entry point and CLI parsing
- `examples/application/include/application.h` - Application interface
### Node Editor State
- `EditorContext.cpp` - Editor context and state management (lines 2059-2124)
- `imgui_node_editor_store.cpp` - Settings serialization (lines 185-225)
- `examples/blueprints-example/app-logic.cpp` - App-level save/load (lines 877-928)
### ImGui Integration
- `external/imgui/imgui.cpp` - ImGui window settings handler (lines 11384-11455)
- `external/imgui/imgui_internal.h` - ImGui internal structures (lines 1354-1384)
---
## Recommendations for Future Implementation
### Option 1: Extend Platform Interface
Add to `platform.h`:
```cpp
struct WindowState {
int x, y, width, height;
int monitor;
bool maximized;
};
virtual bool SaveWindowState(const WindowState& state) = 0;
virtual bool LoadWindowState(WindowState& state) = 0;
```
### Option 2: Use ImGui Ini Handler
Register a custom ImGui settings handler for OS window state:
```cpp
ImGuiSettingsHandler handler;
handler.TypeName = "OSWindow";
handler.ReadOpenFn = OSWindowHandler_ReadOpen;
handler.ReadLineFn = OSWindowHandler_ReadLine;
handler.WriteAllFn = OSWindowHandler_WriteAll;
ImGui::AddSettingsHandler(&handler);
```
### Option 3: Separate Config File
Create `window_state.json` alongside `Blueprints.json`:
```json
{
"window": {
"x": 100,
"y": 100,
"width": 1920,
"height": 1080,
"monitor": 0,
"maximized": false
}
}
```
### Platform-Specific Considerations
**Win32:**
- Use `GetWindowPlacement()` / `SetWindowPlacement()` for full state
- Use `MonitorFromWindow()` to detect monitor
**GLFW:**
- Use `glfwGetWindowPos()` / `glfwSetWindowPos()` for position
- Use `glfwGetWindowSize()` / `glfwSetWindowSize()` for size
- Use `glfwGetWindowMonitor()` for fullscreen monitor
- GLFW 3.3+ has `glfwGetWindowContentScale()` for DPI-aware positioning
---
## Conclusion
The application correctly restores **node editor canvas state** (zoom, panning) but does **not restore OS window state** (position, size, monitor). This is due to:
1. Platform layer (`platform_win32.cpp`, `platform_glfw.cpp`) lacking save/restore logic
2. No storage mechanism for window coordinates
3. Hard-coded default window creation parameters
The node editor's canvas state persistence works as intended through the existing JSON serialization system (`Blueprints.json`).
---
## Appendix: Test Verification
### Test 1: Canvas State Persistence ✅
1. Launch application
2. Navigate canvas (zoom: 1.5x, pan: 500,300)
3. Close application
4. Relaunch → Canvas zoom and pan restored correctly
### Test 2: Window Position Persistence ❌
1. Launch application
2. Move window to (200, 200)
3. Resize window to 1024x768
4. Close application
5. Relaunch → Window appears at OS default position (state lost)
### Test 3: Multi-Monitor Persistence ❌
1. Launch application on Monitor 1
2. Move window to Monitor 2
3. Close application
4. Relaunch → Window appears on Monitor 1 (original monitor lost)
---
## Summary of Findings from imgui_demo.cpp
### What imgui_demo.cpp Taught Us
1. **ImGuiCond_FirstUseEver Pattern** - Standard way to set initial window pos/size while allowing persistence:
```cpp
ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(w, h), ImGuiCond_FirstUseEver);
ImGui::Begin("My Window"); // Position/size saved to .ini automatically
```
2. **ImGuiWindowFlags_NoSavedSettings** - Prevents persistence (used for overlays, temp windows):
```cpp
ImGui::Begin("Overlay", nullptr,
ImGuiWindowFlags_NoSavedSettings | ...);
```
3. **Viewport vs Screen Coordinates**:
- `ImGuiViewport::Pos` - OS window position on screen (read-only for ImGui)
- `ImGuiViewport::WorkPos` - Usable area (excluding OS taskbar/menubar)
- ImGui windows use positions **relative to viewport**, not screen
4. **Why This Doesn't Help Us**:
- The main "Content" window **explicitly uses `ImGuiWindowFlags_NoSavedSettings`** (confirmed in `application.cpp:297`)
- Even if we removed that flag, it would only save the Content window's position **relative to the viewport**
- The **OS window itself** (created by Win32/GLFW) exists **outside ImGui's control**
- ImGui has **no API** to set OS window position - it only renders **inside** the OS window
### The Architectural Gap
```
ImGui's World (what CAN be saved):
ImGui::Begin("Window")
→ Position relative to viewport
→ Saved to .ini automatically
→ Works perfectly ✅
OS Window (what CANNOT be saved by ImGui):
CreateWindow() / glfwCreateWindow()
→ Position on screen
→ ImGui has no API for this
→ Requires platform-specific code ❌
```
---
## ✅ IMPLEMENTATION COMPLETE (Win32 + DirectX)
**All phases have been successfully implemented and tested!**
See `WINDOW_STATE_TEST_REPORT.md` for full test results.
---
## ORIGINAL TODO: Implementation Plan (Win32 + DirectX)
_Note: This was the original plan. All items marked below have been completed._
### Phase 1: Add Window State Querying ✅ COMPLETE
**File:** `examples/application/source/platform_win32.cpp`
- [x] Add method to query current window state
```cpp
struct WindowState {
int x, y, width, height;
int monitor;
bool maximized;
bool minimized;
};
WindowState GetWindowState() const;
```
- [x] Implement `GetWindowState()` using Win32 APIs:
- Use `GetWindowPlacement()` to get position, size, and maximized state ✅
- Use `MonitorFromWindow(m_MainWindowHandle, MONITOR_DEFAULTTONEAREST)` for monitor ✅
- Store monitor index by enumerating monitors with `EnumDisplayMonitors()`
**File:** `examples/application/source/platform.h`
- [x] Add virtual methods to Platform interface:
```cpp
struct WindowState {
int x, y, width, height;
int monitor;
bool maximized;
};
virtual WindowState GetWindowState() const = 0;
virtual bool SetWindowState(const WindowState& state) = 0;
```
### Phase 2: Add Window State Storage ✅ COMPLETE
**File:** `examples/application/source/application.cpp`
- [x] Add window state member variable:
```cpp
struct WindowStateConfig {
int x = -1, y = -1;
int width = 1440, height = 800;
int monitor = 0;
bool maximized = false;
};
WindowStateConfig m_WindowState;
```
- [x] Add save/load methods: ✅
```cpp
bool SaveWindowState();
bool LoadWindowState();
```
- [x] Create `Blueprints_window.json`
### Phase 3: Hook Into Application Lifecycle ✅ COMPLETE
**File:** `examples/application/source/application.cpp`
- [x] Load window state before creating window: ✅
```cpp
bool Application::Create(int width, int height)
{
// Load saved window state
WindowStateConfig state;
if (LoadWindowState("window_state.json")) {
width = state.width;
height = state.height;
}
m_Platform->OpenMainWindow("NodeHub", width, height);
// Restore position AFTER window creation
if (state.x >= 0 && state.y >= 0) {
m_Platform->SetWindowPosition(state.x, state.y);
}
if (state.maximized) {
m_Platform->MaximizeWindow();
}
}
```
- [x] Save window state on shutdown: ✅
```cpp
Application::~Application()
{
// Save window state before cleanup
if (m_Platform) {
auto state = m_Platform->GetWindowState();
SaveWindowState("window_state.json", state);
}
// ... existing cleanup code ...
}
```
### Phase 4: Win32-Specific Implementation ✅ COMPLETE
**File:** `examples/application/source/platform_win32.cpp`
- [x] Implement `GetWindowState()`: ✅
```cpp
WindowState PlatformWin32::GetWindowState() const
{
WindowState state;
WINDOWPLACEMENT placement = { sizeof(WINDOWPLACEMENT) };
GetWindowPlacement(m_MainWindowHandle, &placement);
state.x = placement.rcNormalPosition.left;
state.y = placement.rcNormalPosition.top;
state.width = placement.rcNormalPosition.right - placement.rcNormalPosition.left;
state.height = placement.rcNormalPosition.bottom - placement.rcNormalPosition.top;
state.maximized = (placement.showCmd == SW_SHOWMAXIMIZED);
// Get monitor index
HMONITOR hMonitor = MonitorFromWindow(m_MainWindowHandle, MONITOR_DEFAULTTONEAREST);
state.monitor = GetMonitorIndex(hMonitor);
return state;
}
```
- [x] Implement `SetWindowState()`: ✅
```cpp
bool PlatformWin32::SetWindowState(const WindowState& state)
{
if (!m_MainWindowHandle) return false;
WINDOWPLACEMENT placement = { sizeof(WINDOWPLACEMENT) };
placement.rcNormalPosition.left = state.x;
placement.rcNormalPosition.top = state.y;
placement.rcNormalPosition.right = state.x + state.width;
placement.rcNormalPosition.bottom = state.y + state.height;
placement.showCmd = state.maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL;
return SetWindowPlacement(m_MainWindowHandle, &placement);
}
```
- [x] Add monitor enumeration helper: ✅
- Implemented inline in Get/SetWindowState methods
- Uses lambda callbacks with EnumDisplayMonitors()
- [x] Validate monitor still exists: ✅
- Falls back to primary monitor if specified monitor doesn't exist
- Implemented in SetWindowState()
### Phase 5: JSON Persistence ✅ COMPLETE
**Option A: Separate File** ✅ IMPLEMENTED
- [x] Create `Blueprints_window.json`: ✅
```json
{
"window": {
"x": 100,
"y": 100,
"width": 1920,
"height": 1080,
"monitor": 0,
"maximized": false
}
}
```
**Option B: Extend Blueprints.json**
- [ ] Add window section to existing `Blueprints.json`: ❌ NOT USED (chose separate file)
```json
{
"window": { ... },
"view": { ... },
"selection": [ ... ]
}
```
### Phase 6: Edge Cases & Validation ✅ COMPLETE
- [x] Handle invalid saved positions (off-screen): ✅
```cpp
bool IsPositionValid(int x, int y) {
// Check if position is within any monitor's bounds
// Use MonitorFromPoint() to verify
}
```
- [ ] Handle DPI changes between sessions: ⏸️ DEFERRED (future enhancement)
```cpp
// Save DPI-independent coordinates
// Scale on restore based on current DPI
```
- [x] Handle monitor configuration changes: ✅
```cpp
// Validate monitor index still exists
// Fall back to primary monitor if not
```
- [x] Handle window too large for current monitor: ✅
```cpp
// Clamp to monitor work area
// Don't restore maximized if current monitor is smaller
```
### Phase 7: Testing Checklist
- [x] Test: Normal position/size restoration ✅ PASS
- [ ] Test: Maximized state restoration ⏸️ DEFERRED
- [x] Test: Multi-monitor restoration ✅ IMPLEMENTED (monitor 0 tested)
- [x] Test: Monitor disconnected (fall back gracefully) ✅ CODE IMPLEMENTED
- [x] Test: Invalid coordinates in config (off-screen) ✅ CODE IMPLEMENTED (50px minimum visible)
- [ ] Test: DPI change between sessions ⏸️ DEFERRED
- [x] Test: First launch (no config file) ✅ PASS
- [x] Test: Corrupted config file (JSON parse error) ✅ HANDLED (try/catch fallback)
### Phase 8: GLFW Implementation (Partial) ⏸️
**File:** `examples/application/source/platform_glfw.cpp`
- [x] Implement equivalent functionality for GLFW: ⚠️ PARTIAL (stubs only)
- `glfwGetWindowPos()` / `glfwSetWindowPos()`
- `glfwGetWindowSize()` / `glfwSetWindowSize()`
- `glfwGetMonitorPos()` for multi-monitor
- `glfwMaximizeWindow()` for maximized state
### Implementation Priority
1. ✅ **High Priority**: Basic position/size restoration (Win32) - **DONE**
2. ✅ **High Priority**: Maximized state restoration - **CODE COMPLETE** (not tested)
3. ✅ **Medium Priority**: Monitor selection (multi-monitor users) - **DONE**
4. ⬜ **Low Priority**: DPI-aware scaling - **DEFERRED**
5. ⬜ **Low Priority**: GLFW backend implementation - **PARTIAL STUBS**
### Files to Modify
| File | Changes | Lines Est. |
|------|---------|-----------|
| `platform.h` | Add WindowState struct + virtual methods | +15 |
| `platform_win32.cpp` | Implement Get/SetWindowState | +80 |
| `application.h` | Add SaveWindowState/LoadWindowState | +5 |
| `application.cpp` | Hook into Create/Destructor | +40 |
| `platform_glfw.cpp` | Implement Get/SetWindowState (future) | +60 |
**Estimated Total:** ~200 lines of new code
**Actual Total:** ~268 lines of new code ✅
---
## IMPLEMENTATION COMPLETE ✅
**Date Completed:** November 5, 2025
### What Was Implemented
**Win32 Window State Persistence** - Fully functional
- Window position (x, y) saved and restored
- Window size (width, height) saved and restored
- Monitor selection supported
- Maximized state code complete
- Edge case validation (off-screen, missing monitor)
- JSON persistence (`Blueprints_window.json`)
### Test Results Summary
- ✅ First launch (no config) works correctly
- ✅ Save window state on shutdown works
- ✅ Restore window position on startup works
- ✅ Restore window size on startup works
- ✅ JSON file format is clean and human-readable
- ✅ Console logging provides clear feedback
- ✅ No compilation errors
- ✅ No regressions in existing functionality
### Files Created
- `Blueprints_window.json` - Window state storage (auto-generated)
- `docs/WINDOW_STATE_TEST_REPORT.md` - Comprehensive test report
### Files Modified
- `examples/application/include/application.h` (+17 lines)
- `examples/application/source/application.cpp` (+93 lines)
- `examples/application/source/platform.h` (+3 lines)
- `examples/application/source/platform_win32.cpp` (+127 lines)
- `examples/application/source/platform_glfw.cpp` (+28 lines, stubs)
**Total:** 268 lines of production code
---
**End of Investigation & Implementation**

View File

@ -0,0 +1,331 @@
# Waypoints Pathfinding Consolidation Plan
## Overview
This document outlines the refactoring plan for consolidating `pathfinding.cpp` to improve maintainability, readability, and extensibility. The current `GenerateWaypoints` function is a monolithic 700+ line function with deeply nested conditionals. We will refactor it into a modular system with dedicated routing strategies.
## Current Issues
1. **Monolithic Function**: `GenerateWaypoints` is 700+ lines with deeply nested conditionals
2. **Parameter Proliferation**: 9+ individual parameters make the API hard to use and extend
3. **Code Duplication**: Similar routing logic is repeated in multiple branches
4. **Limited Context**: No access to node type, pin type, or other metadata that could improve routing decisions
5. **Hard to Test**: Large function makes unit testing individual strategies difficult
6. **Hard to Maintain**: Adding new routing cases requires modifying the central function
## Goals
1. **Extract Routing Strategies**: Each routing pattern (Z-shape, U-shape, same-block, horizontal flow, etc.) becomes its own function
2. **Add Context Structure**: Pass `NodeContext` objects instead of individual parameters
3. **Improve Modularity**: Clear separation between routing strategy selection and execution
4. **Better Extensibility**: Easy to add new routing strategies without touching existing code
5. **Maintainability**: Each function has a single, clear responsibility
## Proposed Structure
### 1. Context Structures
```cpp
namespace PathFinding
{
// Pin context - contains all information about a pin
struct PinContext
{
ImVec2 Position; // Pin position
ImVec2 Direction; // Flow direction (normalized)
PinType Type; // Pin type (flow, parameter, etc.)
PinKind Kind; // Input or Output
bool IsFlowPin; // Flow control pin (vs parameter)
bool IsParameterPin; // Parameter pin (data)
};
// Node context - contains all information about a node
struct NodeContext
{
ImVec2 Min; // Top-left corner
ImVec2 Max; // Bottom-right corner
NodeType Type; // Node type (Blueprint, Parameter, etc.)
std::string BlockType; // Block type name (if applicable)
bool IsGroup; // Is this a group node?
float ZPosition; // Z-order
};
// Routing context - complete context for pathfinding
struct RoutingContext
{
PinContext StartPin; // Source pin context
PinContext EndPin; // Target pin context
NodeContext StartNode; // Source node context
NodeContext EndNode; // Target node context
float Margin; // Clearance margin
std::vector<Obstacle> Obstacles; // Other nodes to avoid
};
}
```
### 2. Routing Strategy Functions
Each routing pattern becomes a dedicated function:
```cpp
namespace PathFinding
{
// Strategy functions - each handles one routing pattern
// Same-block routing (output param to input param on same block)
std::vector<ImVec2> RouteSameBlock(
const RoutingContext& ctx);
// Z-shape routing (down -> across -> up)
std::vector<ImVec2> RouteZShape(
const RoutingContext& ctx,
float horizontalY); // Y position for horizontal segment
// U-shape routing (around blocks)
std::vector<ImVec2> RouteUShape(
const RoutingContext& ctx,
bool routeAbove); // Route above or below blocks
// Horizontal flow routing (side-to-side flow pins)
std::vector<ImVec2> RouteHorizontalFlow(
const RoutingContext& ctx);
// Simple L-shape routing (generic fallback)
std::vector<ImVec2> RouteLShape(
const RoutingContext& ctx);
// Complex routing around obstacles
std::vector<ImVec2> RouteAroundObstacles(
const RoutingContext& ctx,
bool preferLeft); // Prefer routing left or right
}
```
### 3. Helper Functions
Extract common logic into reusable helpers:
```cpp
namespace PathFinding
{
// Strategy selection helpers
enum class RoutingStrategy
{
SameBlock, // Same-block routing
ZShape, // Simple Z-shape
ZShapeAboveBlock, // Z-shape with horizontal above block
ZShapeBelowBlock, // Z-shape with horizontal below block
UShapeAbove, // U-shape above blocks
UShapeBelow, // U-shape below blocks
HorizontalFlow, // Side-to-side flow
AroundObstacles, // Complex routing around obstacles
LShape, // Simple L-shape fallback
Direct // Straight line (no waypoints)
};
// Determine which routing strategy to use
RoutingStrategy SelectStrategy(const RoutingContext& ctx);
// Geometry helpers
bool IsSameBlock(const NodeContext& start, const NodeContext& end);
bool NodesOverlapX(const NodeContext& start, const NodeContext& end);
bool EndIsBelowStart(const RoutingContext& ctx);
bool PinIsAtTop(const PinContext& pin, const NodeContext& node);
bool PinIsOnLeft(const PinContext& pin, const NodeContext& node);
// Clearance calculation helpers
float CalculateHorizontalClearanceY(
const RoutingContext& ctx,
bool aboveBlock);
float CalculateVerticalClearanceX(
const RoutingContext& ctx,
bool leftSide);
// Obstacle detection helpers
bool HorizontalSegmentIntersectsNode(
const NodeContext& node,
float y,
float x1,
float x2);
bool SegmentIntersectsObstacles(
const ImVec2& p1,
const ImVec2& p2,
const RoutingContext& ctx);
}
```
### 4. Main Entry Point
The new `GenerateWaypoints` becomes a thin dispatcher:
```cpp
namespace PathFinding
{
// Main entry point - dispatches to appropriate routing strategy
std::vector<ImVec2> GenerateWaypoints(const RoutingContext& ctx)
{
// Strategy selection
auto strategy = SelectStrategy(ctx);
// Dispatch to appropriate routing function
switch (strategy)
{
case RoutingStrategy::SameBlock:
return RouteSameBlock(ctx);
case RoutingStrategy::ZShape:
return RouteZShape(ctx, CalculateHorizontalClearanceY(ctx, false));
case RoutingStrategy::ZShapeAboveBlock:
return RouteZShape(ctx, CalculateHorizontalClearanceY(ctx, true));
case RoutingStrategy::UShapeAbove:
return RouteUShape(ctx, true);
case RoutingStrategy::UShapeBelow:
return RouteUShape(ctx, false);
case RoutingStrategy::HorizontalFlow:
return RouteHorizontalFlow(ctx);
case RoutingStrategy::AroundObstacles:
return RouteAroundObstacles(ctx, DeterminePreferredSide(ctx));
case RoutingStrategy::LShape:
return RouteLShape(ctx);
case RoutingStrategy::Direct:
return {}; // No waypoints needed
default:
return RouteLShape(ctx); // Fallback
}
}
// Convenience overload for backward compatibility
std::vector<ImVec2> GenerateWaypoints(
const ImVec2& startPos,
const ImVec2& endPos,
const ImVec2& startDir,
const ImVec2& endDir,
const ImVec2& startNodeMin,
const ImVec2& startNodeMax,
const ImVec2& endNodeMin,
const ImVec2& endNodeMax,
float margin = 10.0f,
const std::vector<Obstacle>& obstacles = {})
{
// Build context from parameters (for backward compatibility)
RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.Margin = margin;
ctx.Obstacles = obstacles;
return GenerateWaypoints(ctx);
}
}
```
## Implementation Plan
### Phase 1: Add Context Structures (Low Risk)
- [ ] Define `PinContext` structure in `pathfinding.h`
- [ ] Define `NodeContext` structure in `pathfinding.h`
- [ ] Define `RoutingContext` structure in `pathfinding.h`
- [ ] Add `RoutingStrategy` enum in `pathfinding.h`
- [ ] Update includes if needed (may need node/pin type definitions)
### Phase 2: Extract Helper Functions (Medium Risk)
- [ ] Extract geometry helpers (`IsSameBlock`, `NodesOverlapX`, etc.)
- [ ] Extract clearance calculation helpers
- [ ] Extract obstacle detection helpers
- [ ] Test each helper function independently
- [ ] Update existing code to use helpers where possible
### Phase 3: Extract Routing Strategy Functions (High Risk)
- [ ] Extract `RouteSameBlock` (lines ~161-200 in current code)
- [ ] Extract `RouteZShape` (lines ~213-637 in current code)
- [ ] Handle simple Z-shape case
- [ ] Handle Z-shape with obstacle routing
- [ ] Handle Z-shape above/below block variants
- [ ] Extract `RouteUShape` (lines ~641-787 in current code)
- [ ] Handle U-shape above blocks
- [ ] Handle U-shape below blocks
- [ ] Handle side routing variants
- [ ] Extract `RouteHorizontalFlow` (lines ~790-847 in current code)
- [ ] Extract `RouteLShape` (lines ~848-855 in current code)
- [ ] Extract `RouteAroundObstacles` (complex routing logic)
- [ ] Test each routing function with existing test cases
### Phase 4: Implement Strategy Selection (Medium Risk)
- [ ] Implement `SelectStrategy` function
- [ ] Handle same-block detection
- [ ] Handle vertical flow (down to up)
- [ ] Handle horizontal flow
- [ ] Handle generic cases
- [ ] Implement main `GenerateWaypoints` dispatcher
- [ ] Test strategy selection logic
### Phase 5: Add Enhanced Context Support (Future Enhancement)
- [ ] Modify calling code to pass `Node*` and `Pin*` objects instead of just positions
- [ ] Populate `PinContext` with actual pin metadata (type, kind, etc.)
- [ ] Populate `NodeContext` with actual node metadata (type, block type, etc.)
- [ ] Use metadata to make smarter routing decisions
- [ ] Example: Use pin type to determine if special routing is needed
### Phase 6: Backward Compatibility & Testing (Low Risk)
- [ ] Keep old function signature as convenience overload
- [ ] Test all existing call sites still work
- [ ] Run regression tests
- [ ] Update any documentation
## File Organization
```
pathfinding.h
- Context structures (PinContext, NodeContext, RoutingContext)
- RoutingStrategy enum
- Function declarations
pathfinding.cpp
- Helper functions
- Strategy selection (SelectStrategy)
- Routing strategy implementations
- Main GenerateWaypoints dispatcher
- Backward compatibility overload
```
## Testing Strategy
1. **Unit Tests for Helpers**: Test each helper function with known inputs
2. **Integration Tests**: Test each routing strategy with sample scenarios
3. **Regression Tests**: Verify existing call sites produce same results
4. **Visual Tests**: Manually verify routing looks correct in the UI
## Benefits
1. **Maintainability**: Each routing strategy is isolated and easy to modify
2. **Testability**: Small functions are easier to unit test
3. **Extensibility**: Adding new routing strategies is straightforward
4. **Readability**: Clear separation of concerns, easier to understand
5. **Context Awareness**: Can make smarter decisions with full node/pin context
6. **Performance**: Potential for optimization within individual strategies
## Migration Notes
- Keep old function signature for backward compatibility
- Gradually migrate call sites to use context objects
- All existing code should continue to work during transition
## Open Questions
1. **Node/Pin Type Dependencies**: Do we need to include full `Node*` and `Pin*` pointers, or just metadata?
- Answer: Start with metadata, can add pointers later if needed
2. **Obstacle Collection**: Should obstacles be part of context or passed separately?
- Answer: Part of context for consistency
3. **Error Handling**: How should we handle invalid contexts (e.g., invalid bounds)?
- Answer: Return empty waypoint list and log warning
4. **Constants**: Should clearance constants stay in namespace or move to context?
- Answer: Keep in namespace Constants for now, can be made configurable later

View File

@ -0,0 +1,112 @@
cmake_minimum_required(VERSION 3.5)
add_example_executable(nodehub
main.cpp
types.h
app.h
app.cpp
app-logic.cpp
app-render.cpp
app-screenshot.cpp
app-runtime.cpp
containers/container.h
containers/container.cpp
containers/root_container.h
containers/root_container.cpp
core/graph_state.h
core/graph_state.cpp
core/Object.h
core/Object.cpp
core/Parameter.h
core/Parameter.cpp
core/ParameterIn.h
core/ParameterIn.cpp
core/ParameterOut.h
core/ParameterOut.cpp
core/ParameterManager.h
core/ParameterManager.cpp
core/BaseManager.h
core/BaseManager.cpp
core/Context.h
core/Context.cpp
blocks/NodeEx.h
blocks/NodeEx.cpp
blocks/block.h
blocks/block.cpp
blocks/math_blocks.h
blocks/math_blocks.cpp
blocks/logic_blocks.h
blocks/logic_blocks.cpp
blocks/start_block.h
blocks/start_block.cpp
blocks/log_block.h
blocks/log_block.cpp
blocks/parameter_operation.h
blocks/parameter_operation.cpp
blocks/group_block.h
blocks/group_block.cpp
blocks/parameter_node.h
blocks/parameter_node.cpp
blocks/block_edit_dialog.h
blocks/block_edit_dialog.cpp
blocks/parameter_edit_dialog.h
blocks/parameter_edit_dialog.cpp
utilities/node_renderer_base.h
utilities/pathfinding.h
utilities/edge_editing.h
utilities/pin_renderer.h
utilities/style_manager.h
utilities/uuid_generator.h
utilities/uuid_id_manager.h
utilities/node_renderer_base.cpp
utilities/pathfinding.cpp
utilities/edge_editing.cpp
utilities/pin_renderer.cpp
utilities/style_manager.cpp
utilities/uuid_generator.cpp
utilities/uuid_id_manager.cpp
Logging.h
Logging.cpp
stats.cpp
)
target_include_directories(nodehub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../application/include")
target_include_directories(nodehub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../nodehub/core")
target_include_directories(nodehub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../nodehub/blocks")
# Add local spdlog include directory (we copied it into our project)
target_include_directories(nodehub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/external")
target_compile_definitions(nodehub PRIVATE FMT_HEADER_ONLY=1)
# Add /utf-8 compiler flag for spdlog on MSVC
if (MSVC)
target_compile_options(nodehub PRIVATE /utf-8)
endif()
# Link protobuf if available
if (PROTOBUF_AVAILABLE AND TARGET protobuf_interface)
target_link_libraries(nodehub PRIVATE protobuf_interface)
endif()
# Also add to console variant if it exists
if (WIN32 AND BUILD_CONSOLE_VARIANTS)
target_include_directories(nodehub-console PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../application/include")
target_include_directories(nodehub-console PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/external")
target_compile_definitions(nodehub-console PRIVATE FMT_HEADER_ONLY=1)
if (MSVC)
target_compile_options(nodehub-console PRIVATE /utf-8)
endif()
# Link protobuf to console variant if available
if (PROTOBUF_AVAILABLE AND TARGET protobuf_interface)
target_link_libraries(nodehub-console PRIVATE protobuf_interface)
endif()
endif()
file(GLOB NodeHubResources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
add_custom_command(
TARGET nodehub
PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data"
"$<TARGET_FILE_DIR:nodehub>/data"
)

View File

@ -0,0 +1,129 @@
#include "Logging.h"
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <algorithm>
#include <cctype>
#include <vector>
// Global logger instance
std::shared_ptr<spdlog::logger> g_logger = nullptr;
void InitLogger(const char* app_name, bool console, bool file, const char* filename)
{
try
{
// Create sinks vector
std::vector<spdlog::sink_ptr> sinks;
// Add console sink if requested (with color support)
if (console)
{
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::trace);
console_sink->set_pattern("[%H:%M:%S:%e] [%^%l%$] [%n] %v");
sinks.push_back(console_sink);
}
// Add file sink if requested
if (file && filename)
{
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filename, true);
file_sink->set_level(spdlog::level::trace);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S:%e] [%l] [%n] %v");
sinks.push_back(file_sink);
}
// Create logger with multiple sinks
g_logger = std::make_shared<spdlog::logger>(app_name, sinks.begin(), sinks.end());
// Register logger with spdlog
spdlog::register_logger(g_logger);
// Set as default logger
spdlog::set_default_logger(g_logger);
// Flush on every log (can be adjusted for performance)
g_logger->flush_on(spdlog::level::trace);
// Default to debug verbosity unless configured otherwise
SetLoggerLevel(spdlog::level::debug);
}
catch (const spdlog::spdlog_ex& ex)
{
// fprintf(stderr, "Log initialization failed: %s\n", ex.what());
}
}
void ShutdownLogger()
{
if (g_logger)
{
g_logger->flush();
g_logger.reset();
}
spdlog::shutdown();
}
spdlog::level::level_enum ParseLogLevel(const std::string& level_name, bool* out_valid)
{
if (out_valid)
*out_valid = true;
if (level_name.empty())
return spdlog::level::debug;
auto first = std::find_if_not(level_name.begin(), level_name.end(), [](unsigned char c) {
return std::isspace(c);
});
auto last = std::find_if_not(level_name.rbegin(), level_name.rend(), [](unsigned char c) {
return std::isspace(c);
}).base();
if (first == level_name.end() || first >= last)
return spdlog::level::debug;
std::string normalized;
normalized.resize(static_cast<size_t>(last - first));
std::transform(first, last, normalized.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (normalized == "trace" || normalized == "t")
return spdlog::level::trace;
if (normalized == "debug" || normalized == "d")
return spdlog::level::debug;
if (normalized == "info" || normalized == "information" || normalized == "i")
return spdlog::level::info;
if (normalized == "warn" || normalized == "warning" || normalized == "w")
return spdlog::level::warn;
if (normalized == "error" || normalized == "err" || normalized == "e")
return spdlog::level::err;
if (normalized == "critical" || normalized == "fatal" || normalized == "c" || normalized == "f")
return spdlog::level::critical;
if (normalized == "off" || normalized == "none" || normalized == "disable" || normalized == "disabled")
return spdlog::level::off;
if (out_valid)
*out_valid = false;
return spdlog::level::debug;
}
void SetLoggerLevel(spdlog::level::level_enum level)
{
spdlog::set_level(level);
if (g_logger)
{
g_logger->set_level(level);
for (const auto& sink : g_logger->sinks())
{
if (sink)
{
sink->set_level(level);
}
}
}
}

View File

@ -0,0 +1,140 @@
#ifndef LOGGING_H
#define LOGGING_H
#include <memory>
#include <string>
#include <spdlog/fmt/bundled/printf.h>
// Include spdlog for header-only mode
#ifndef DISABLE_LOGGING
#define SPDLOG_HEADER_ONLY
#include <spdlog/spdlog.h>
#include <spdlog/logger.h>
// Global spdlog logger instance
extern std::shared_ptr<spdlog::logger> g_logger;
#else
// When logging disabled, stub out
namespace spdlog {
class logger;
namespace level {
enum level_enum : int;
}
}
extern std::shared_ptr<spdlog::logger> g_logger;
#endif
// Initialize logging system
void InitLogger(const char* app_name = "app", bool console = true, bool file = true, const char* filename = "app.log");
// Shutdown logging system
void ShutdownLogger();
// Parse log level string helper (returns debug on invalid input)
spdlog::level::level_enum ParseLogLevel(const std::string& level_name, bool* out_valid = nullptr);
// Update the global logger and sinks to the provided level
void SetLoggerLevel(spdlog::level::level_enum level);
// printf-style helpers (preserve existing formatting strings)
template<typename... Args>
inline void LogTracef(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->trace(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
template<typename... Args>
inline void LogDebugf(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->debug(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
template<typename... Args>
inline void LogInfof(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->info(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
template<typename... Args>
inline void LogWarnf(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->warn(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
template<typename... Args>
inline void LogErrorf(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->error(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
template<typename... Args>
inline void LogCriticalf(const char* fmt, Args&&... args)
{
#ifndef DISABLE_LOGGING
if (g_logger)
g_logger->critical(fmt::sprintf(fmt, std::forward<Args>(args)...));
#else
(void)fmt;
((void)std::initializer_list<int>{((void)args, 0)...});
#endif
}
// -----------------------------------------------------------------------------
// Logging macros - can be disabled via DISABLE_LOGGING
// -----------------------------------------------------------------------------
#ifndef DISABLE_LOGGING
// Main logging macros using spdlog
#define LOG_TRACE(...) if (g_logger) g_logger->trace(__VA_ARGS__)
#define LOG_DEBUG(...) if (g_logger) g_logger->debug(__VA_ARGS__)
#define LOG_INFO(...) if (g_logger) g_logger->info(__VA_ARGS__)
#define LOG_WARN(...) if (g_logger) g_logger->warn(__VA_ARGS__)
#define LOG_ERROR(...) if (g_logger) g_logger->error(__VA_ARGS__)
#define LOG_CRITICAL(...) if (g_logger) g_logger->critical(__VA_ARGS__)
// Legacy compatibility (map old names to spdlog levels)
#define LOG_FATAL(...) LOG_CRITICAL(__VA_ARGS__)
#define LOG_VERBOSE(...) LOG_TRACE(__VA_ARGS__)
#else
// Stub implementations when logging is disabled
#define LOG_TRACE(...) (void)0
#define LOG_DEBUG(...) (void)0
#define LOG_INFO(...) (void)0
#define LOG_WARN(...) (void)0
#define LOG_ERROR(...) (void)0
#define LOG_CRITICAL(...) (void)0
#define LOG_FATAL(...) (void)0
#define LOG_VERBOSE(...) (void)0
#endif
#endif // LOGGING_H

View File

@ -0,0 +1,14 @@
#pragma once
// v2
#include "commons.h"
#include "core/Object.h"
#include "core/Parameter.h"
#include "utilities/uuid_generator.h"
//legacy blocks - v1
#include "blocks/block.h"
#include "blocks/parameter_node.h"
#include "blocks/parameter_operation.h"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,451 @@
//------------------------------------------------------------------------------
// Runtime execution system for blocks
// Processes activated blocks and propagates execution through links
// NOTE: Pure execution logic - no rendering/visualization dependencies
//------------------------------------------------------------------------------
#include "app.h"
#include "types.h"
#include "blocks/block.h"
#include "Logging.h"
#include <vector>
#include <set>
bool App::ExecuteRuntimeStep()
{
static bool firstCall = true;
static int totalCalls = 0;
static int framesWithWork = 0;
totalCalls++;
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] ExecuteRuntimeStep: Called (from ed::End)");
firstCall = false;
}
// Debug: Log every 60 frames to show we're being called
if (totalCalls % 60 == 1) // Log on frame 1, 61, 121, etc.
{
LOG_TRACE("[Runtime] ExecuteRuntimeStep called {} times total, {} frames had work", totalCalls, framesWithWork);
}
// Execute in a loop until no more blocks need to run
// This propagates execution through the entire chain in one frame
const int maxIterations = 50; // Safety limit to prevent infinite loops
int iteration = 0;
bool didAnyWork = false; // Track if we found any work to do overall
while (iteration < maxIterations)
{
iteration++;
// Step 0: First, propagate any activated outputs to connected inputs
// This handles the case where outputs were activated manually (e.g., via 'R' key)
// Track which outputs we propagate so we can deactivate them after
std::vector<std::pair<Node *, int>> outputsToDeactivate; // (node, outputIndex)
LOG_DEBUG("[Runtime DEBUG] Iteration {}: Starting Step 0 (check for activated outputs)", iteration);
// Get nodes from active root container if available, otherwise use m_Nodes
auto *container = GetActiveRootContainer();
std::vector<Node *> nodesToProcess;
if (container)
{
// Use GetNodes() to resolve IDs to pointers (safe from reallocation)
nodesToProcess = container->GetNodes(this);
}
else
{
// No container - get nodes from active root container
if (GetActiveRootContainer())
{
nodesToProcess = GetActiveRootContainer()->GetAllNodes();
}
}
for (auto *nodePtr : nodesToProcess)
{
if (!nodePtr)
continue;
// CRITICAL: Skip parameter nodes - they don't have BlockInstance
// Check node type FIRST before any BlockInstance access
// Use try-catch to safely access Type field on potentially freed memory
NodeType nodeType;
try
{
nodeType = nodePtr->Type;
}
catch (...)
{
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Failed to access node Type field");
continue;
}
if (nodeType == NodeType::Parameter || !nodePtr->IsBlockBased())
continue;
// Use safe getter to validate BlockInstance
Block *blockInstance = nodePtr->GetBlockInstance();
if (!blockInstance)
continue;
// Validate BlockInstance ID matches node ID (prevents dangling pointer from ID reuse)
// Only call GetID() if pointer is valid (not corrupted)
int blockId = -1;
try
{
blockId = blockInstance->GetID();
}
catch (...)
{
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Failed to call GetID() on BlockInstance for node {}",
nodePtr->ID.Get());
nodePtr->BlockInstance = nullptr;
continue;
}
if (blockId != nodePtr->ID.Get())
{
// ID mismatch = dangling pointer, clear it
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Node {} has BlockInstance with mismatched ID! Node ID={}, Block ID={}",
nodePtr->ID.Get(), nodePtr->ID.Get(), blockId);
nodePtr->BlockInstance = nullptr;
continue;
}
auto &node = *nodePtr;
// Check all flow outputs for activation
int outputIndex = 0;
for (const auto &pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
{
if (blockInstance->IsOutputActive(outputIndex))
{
didAnyWork = true;
framesWithWork++;
// Track this output for deactivation
outputsToDeactivate.push_back({&node, outputIndex});
// Find all links connected to this activated output pin
// Get links from active root container if available, otherwise use m_Links
std::vector<Link *> fallbackLinks;
std::vector<Link *> linksToProcess;
if (container)
{
// Get links from container (uses GetLinks which resolves IDs to pointers)
linksToProcess = container->GetLinks(this);
}
else
{
// No container - get all links from active root container
if (GetActiveRootContainer())
{
linksToProcess = GetActiveRootContainer()->GetAllLinks();
}
}
for (auto *linkPtr : linksToProcess)
{
if (!linkPtr)
continue;
auto &link = *linkPtr;
if (link.StartPinID == pin.ID)
{
// Find target node and determine input index
auto *targetPin = FindPin(link.EndPinID);
if (targetPin && targetPin->Node &&
targetPin->Node->Type != NodeType::Parameter &&
targetPin->Node->IsBlockBased() && targetPin->Node->BlockInstance)
{
// Find which input index this pin corresponds to
int targetInputIndex = 0;
for (const auto &targetInputPin : targetPin->Node->Inputs)
{
if (targetInputPin.Type == PinType::Flow)
{
if (targetInputPin.ID == link.EndPinID)
{
targetPin->Node->BlockInstance->ActivateInput(targetInputIndex, true);
break;
}
targetInputIndex++;
}
}
}
}
}
}
outputIndex++;
}
}
}
// Step 1: Find all blocks with activated inputs
std::vector<Node *> blocksToRun;
for (auto *nodePtr : nodesToProcess)
{
// CRITICAL: Validate node pointer is not corrupted/freed before accessing ANY members
if (!nodePtr)
continue;
// CRITICAL: Skip parameter nodes - they don't have BlockInstance
// Check node type FIRST before any BlockInstance access
// Use try-catch to safely access Type field on potentially freed memory
NodeType nodeType;
try
{
nodeType = nodePtr->Type;
}
catch (...)
{
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Failed to access node Type field");
continue;
}
if (nodeType == NodeType::Parameter || !nodePtr->IsBlockBased())
continue;
// Use safe getter to validate BlockInstance
Block *blockInstance = nodePtr->GetBlockInstance();
if (!blockInstance)
continue;
// Validate BlockInstance ID matches node ID (prevents dangling pointer from ID reuse)
// Only call GetID() if pointer is valid (not corrupted)
int blockId = -1;
try
{
blockId = blockInstance->GetID();
}
catch (...)
{
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Failed to call GetID() on BlockInstance for node {}",
nodePtr->ID.Get());
nodePtr->BlockInstance = nullptr;
continue;
}
if (blockId != nodePtr->ID.Get())
{
// ID mismatch = dangling pointer, clear it
LOG_ERROR("[ERROR] ExecuteRuntimeStep: Node {} has BlockInstance with mismatched ID! Node ID={}, Block ID={}",
nodePtr->ID.Get(), nodePtr->ID.Get(), blockId);
nodePtr->BlockInstance = nullptr;
continue;
}
auto &node = *nodePtr;
// Check if any flow input is activated
bool hasActivatedInput = false;
int inputIndex = 0;
for (const auto &pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
{
if (blockInstance->IsInputActive(inputIndex))
{
hasActivatedInput = true;
break;
}
inputIndex++;
}
}
if (hasActivatedInput)
{
LOG_TRACE("[Runtime] Step 1: Adding node {} to execution queue", node.ID.Get());
blocksToRun.push_back(&node);
}
}
// If no blocks to run, we're done
if (blocksToRun.empty())
{
if (didAnyWork)
{
LOG_TRACE("[Runtime] Iteration {}: No blocks to run (but had work in Step 0)", iteration);
}
break;
}
didAnyWork = true;
LOG_TRACE("[Runtime] Step 2: Executing {} block(s)", blocksToRun.size());
// Step 2: Execute blocks and collect activations to propagate
std::vector<std::pair<Node *, int>> activationsToPropagate; // (target node, input index)
for (Node *node : blocksToRun)
{
// CRITICAL: Skip parameter nodes
if (!node || node->Type == NodeType::Parameter || !node->IsBlockBased() || !node->BlockInstance)
continue;
// Run the block (pure execution - no logging/visualization here)
node->BlockInstance->Run(*node, this);
// Activate first flow output (index 0 = "Done") when block opts-in
// Blocks can override ShouldAutoActivateDefaultOutput() to handle flow routing manually
if (node->BlockInstance->ShouldAutoActivateDefaultOutput())
{
node->BlockInstance->ActivateOutput(0, true);
}
// Step 3: Find links from activated outputs and prepare to activate targets
int outputIndex = 0;
for (const auto &pin : node->Outputs)
{
if (pin.Type == PinType::Flow)
{
if (node->BlockInstance->IsOutputActive(outputIndex))
{
// Find all links connected to this output pin
// Get links from active root container if available
std::vector<Link *> linksToProcess2;
if (container)
{
linksToProcess2 = container->GetLinks(this);
}
else if (GetActiveRootContainer())
{
linksToProcess2 = GetActiveRootContainer()->GetAllLinks();
}
for (auto *linkPtr : linksToProcess2)
{
if (!linkPtr)
continue;
auto &link = *linkPtr;
if (link.StartPinID == pin.ID)
{
// Find target node and determine input index
auto *targetPin = FindPin(link.EndPinID);
if (targetPin && targetPin->Node && targetPin->Node->IsBlockBased())
{
// Find which input index this pin corresponds to
int targetInputIndex = 0;
for (const auto &targetInputPin : targetPin->Node->Inputs)
{
if (targetInputPin.Type == PinType::Flow)
{
if (targetInputPin.ID == link.EndPinID)
{
// Queue activation for next iteration
activationsToPropagate.push_back({targetPin->Node, targetInputIndex});
break;
}
targetInputIndex++;
}
}
}
}
}
outputIndex++;
}
}
}
// Step 4: Visualize execution (only if rendering - headless mode skips this)
// Collect affected links from executed blocks for visualization
std::vector<ed::LinkId> affectedLinks;
for (Node *node : blocksToRun)
{
// CRITICAL: Skip parameter nodes
if (!node || node->Type == NodeType::Parameter || !node->IsBlockBased())
continue;
// Collect all output links (flow + parameter)
// Get links from active root container if available
std::vector<Link *> linksToProcess3;
if (container)
{
linksToProcess3 = container->GetLinks(this);
}
else if (GetActiveRootContainer())
{
linksToProcess3 = GetActiveRootContainer()->GetAllLinks();
}
for (const auto &pin : node->Outputs)
{
for (auto *linkPtr : linksToProcess3)
{
if (!linkPtr)
continue;
auto &link = *linkPtr;
if (link.StartPinID == pin.ID)
{
affectedLinks.push_back(link.ID);
}
}
}
}
// Visualize (only in rendering mode - will be no-op in headless)
VisualizeRuntimeExecution(blocksToRun, affectedLinks);
// Step 5: Deactivate all outputs and inputs after execution
// First, deactivate outputs that were propagated in Step 0 (to prevent re-propagation)
for (const auto &outputInfo : outputsToDeactivate)
{
Node *node = outputInfo.first;
int outputIndex = outputInfo.second;
if (node && node->IsBlockBased() && node->BlockInstance)
{
node->BlockInstance->ActivateOutput(outputIndex, false);
}
}
// Then deactivate all outputs and inputs of executed blocks
for (Node *node : blocksToRun)
{
// CRITICAL: Skip parameter nodes
if (!node || node->Type == NodeType::Parameter || !node->IsBlockBased() || !node->BlockInstance)
continue;
// Deactivate all outputs
int outputCount = node->BlockInstance->GetOutputCount();
for (int i = 0; i < outputCount; ++i)
{
node->BlockInstance->ActivateOutput(i, false);
}
// Also deactivate inputs (blocks should deactivate their inputs after running)
int inputCount = node->BlockInstance->GetInputCount();
for (int i = 0; i < inputCount; ++i)
{
node->BlockInstance->ActivateInput(i, false);
}
}
// Step 6: Activate inputs for next iteration (same frame)
for (const auto &activation : activationsToPropagate)
{
Node *targetNode = activation.first;
int inputIndex = activation.second;
if (targetNode && targetNode->IsBlockBased() && targetNode->BlockInstance)
{
targetNode->BlockInstance->ActivateInput(inputIndex, true);
}
}
// Continue loop to execute newly activated blocks
}
return didAnyWork;
}
return false;
}

View File

@ -0,0 +1,76 @@
#include "app.h"
#include <imgui.h>
#include <cstdio>
#include <string>
#include <ctime>
void App::TakeScreenshot(const char* filename)
{
printf("\n========== SCREENSHOT REQUEST ==========\n");
printf("App::TakeScreenshot called with filename: %s\n", filename ? filename : "nullptr");
// Generate default filename if not provided
std::string fname;
if (!filename)
{
time_t now = time(nullptr);
char buffer[64];
strftime(buffer, sizeof(buffer), "screenshot_%Y%m%d_%H%M%S.png", localtime(&now));
fname = buffer;
printf("App::TakeScreenshot - Generated filename: %s\n", fname.c_str());
}
else
{
fname = filename;
printf("App::TakeScreenshot - Using provided filename: %s\n", fname.c_str());
}
// Delegate to Application base class which calls the renderer
bool success = Application::TakeScreenshot(fname.c_str());
printf("App::TakeScreenshot - Result: %s\n", success ? "SUCCESS" : "FAILED");
printf("==========================================\n\n");
if (success)
{
m_ScreenshotMessage = "Screenshot saved: " + fname;
m_ScreenshotMessageTime = 5.0f;
}
else
{
m_ScreenshotMessage = "Failed to save screenshot: " + fname;
m_ScreenshotMessageTime = 5.0f;
}
}
void App::RenderScreenshotNotification(float deltaTime)
{
if (m_ScreenshotMessageTime > 0.0f)
{
m_ScreenshotMessageTime -= deltaTime;
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 10, 10), ImGuiCond_Always, ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.7f); // More visible background
if (ImGui::Begin("Screenshot Notification", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings))
{
// Use different color based on success/failure
if (m_ScreenshotMessage.find("FAILED") != std::string::npos)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); // Red for error
ImGui::Text("%s", m_ScreenshotMessage.c_str());
ImGui::PopStyleColor();
}
else
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); // Green for success
ImGui::Text("%s", m_ScreenshotMessage.c_str());
ImGui::PopStyleColor();
}
}
ImGui::End();
}
}

View File

@ -0,0 +1,334 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include "app.h"
#include "containers/root_container.h"
#include "Logging.h"
#include <imgui_node_editor.h>
#include <imgui_node_editor_internal.h>
#include <app-types.h>
namespace ed = ax::NodeEditor;
ed::EditorContext* m_Editor = nullptr;
App::App(const char* name) : Application(name){
m_Context = new NH_Context();
initLogger();
// m_Context->SetParameterManager(new NH_ParameterManager(m_Context));
}
App::App(const char* name, const ArgsMap& args) : Application(name, args){
m_Context = new NH_Context();
initLogger();
}
App::App() : Application("Blueprints"){
m_Context = new NH_Context();
initLogger();
}
NH_Context* App::GetContext()
{
return m_Context;
}
void App::initLogger()
{
// Determine desired log level from CLI args
std::string logLevelArg = "debug";
auto logLevelIt = m_Args.find("log-level");
if (logLevelIt != m_Args.end() && logLevelIt->second.Type == ArgValue::Type::String && !logLevelIt->second.String.empty())
{
logLevelArg = logLevelIt->second.String;
}
bool logLevelValid = true;
auto parsedLogLevel = ParseLogLevel(logLevelArg, &logLevelValid);
// Initialize spdlog logger system
InitLogger("blueprints", true, true, "blueprints.log");
SetLoggerLevel(parsedLogLevel);
if (parsedLogLevel != spdlog::level::off)
{
spdlog::log(parsedLogLevel, "Logger level set to {}", spdlog::level::to_string_view(parsedLogLevel));
}
if (!logLevelValid)
{
LOG_WARN("Unknown log level '{}', defaulting to 'debug'", logLevelArg);
}
}
void App::OnStart()
{
// Get graph filename from CLI args (--graph), default to BlueprintsGraph.json
// Supports both relative and absolute paths
auto it = m_Args.find("graph");
if (it != m_Args.end() && it->second.Type == ArgValue::Type::String)
{
m_GraphFilename = it->second.String;
LOG_INFO("Using graph file: {}", m_GraphFilename);
}
else
{
LOG_INFO("Using default graph file: {}", m_GraphFilename);
}
// Create default root container from graph filename
AddRootContainer(m_GraphFilename);
LOG_TRACE("[CHECKPOINT] OnStart: Root container created, active={:p}",
static_cast<const void*>(GetActiveRootContainer()));
ed::Config config;
// Use custom settings callbacks instead of SettingsFile
// Graph data (nodes, links, positions, control points) → specified file or BlueprintsGraph.json (handled in SaveGraph/LoadGraph)
// View state only (scroll, zoom, selection) → Blueprints.json (handled via callbacks)
config.SettingsFile = "Blueprints.json";
config.UserPointer = this;
config.LoadSettings = [](char* data, void* userPointer) -> size_t
{
auto self = static_cast<App*>(userPointer);
return self->LoadViewSettings(data);
};
config.SaveSettings = [](const char* data, size_t size, ed::SaveReasonFlags reason, void* userPointer) -> bool
{
auto self = static_cast<App*>(userPointer);
return self->SaveViewSettings(data, size);
};
// Disable per-node settings - we handle everything in BlueprintsGraph.json
config.SaveNodeSettings = nullptr;
config.LoadNodeSettings = nullptr;
// Provide callbacks to access container nodes/links for BuildControl
config.GetContainerNodeIds = [](void* userPointer, ed::NodeId* nodeIdsOut, int maxNodes) -> int
{
auto self = static_cast<App*>(userPointer);
auto* container = self->GetActiveRootContainer();
if (!container)
return 0;
// Get all container nodes
auto nodes = container->GetNodes(self);
if (nodeIdsOut == nullptr)
return static_cast<int>(nodes.size()); // Return count only
// Fill node IDs array
int count = 0;
for (auto* node : nodes)
{
if (node && count < maxNodes)
{
nodeIdsOut[count] = node->ID;
count++;
}
}
return count;
};
config.GetContainerLinkIds = [](void* userPointer, ed::LinkId* linkIdsOut, int maxLinks) -> int
{
auto self = static_cast<App*>(userPointer);
auto* container = self->GetActiveRootContainer();
if (!container)
return 0;
// Get all container links
auto links = container->GetLinks(self);
if (linkIdsOut == nullptr)
return static_cast<int>(links.size()); // Return count only
// Fill link IDs array
int count = 0;
for (auto* link : links)
{
if (link && count < maxLinks)
{
linkIdsOut[count] = link->ID;
count++;
}
}
return count;
};
// Provide callback to mark links as user-manipulated when user edits waypoints
config.MarkLinkUserManipulated = [](ed::LinkId linkId, void* userPointer) -> void
{
auto self = static_cast<App*>(userPointer);
self->MarkLinkUserManipulated(linkId);
};
// Provide callback when block display mode changes (triggers link auto-adjustment)
config.OnBlockDisplayModeChanged = [](ed::NodeId nodeId, void* userPointer) -> void
{
auto self = static_cast<App*>(userPointer);
// When display mode changes, the node size changes immediately
// Call AutoAdjustLinkWaypoints immediately - it will detect the size change
// by comparing current size with m_LastNodeSizes (which has the old size from last frame)
self->AutoAdjustLinkWaypoints();
// Update the last known size for this node after adjustment
self->UpdateLastNodeSize(nodeId);
LOG_TRACE("[CHECKPOINT] OnBlockDisplayModeChanged: AutoAdjustLinkWaypoints called");
};
LOG_TRACE("[CHECKPOINT] OnStart: About to create editor");
m_Editor = ed::CreateEditor(&config);
ed::SetCurrentEditor(m_Editor);
// Load custom node styles from JSON
if (m_StyleManager.LoadFromFile("styles.json"))
{
LOG_TRACE("[CHECKPOINT] OnStart: Custom styles loaded from styles.json");
}
else
{
}
m_StyleManager.ApplyToEditorStyle(m_Editor);
// Register runtime callback for block execution
ed::Detail::EditorContext::RegisterRuntimeCallback(this, [](void* app) {
static_cast<App*>(app)->ExecuteRuntimeStep();
});
LOG_TRACE("[CHECKPOINT] OnStart: Runtime callback registered");
// Set global style - no arrows on any pins
auto& style = ed::GetStyle();
style.PinArrowSize = 0.0f;
style.PinArrowWidth = 0.0f;
LOG_TRACE("[CHECKPOINT] OnStart: About to load graph");
// Load saved graph (nodes and links from BlueprintsGraph.json)
auto* activeContainer = GetActiveRootContainer();
if (activeContainer)
{
LoadGraph(m_GraphFilename, activeContainer);
}
LOG_TRACE("[CHECKPOINT] OnStart: Graph loaded, about to build nodes");
BuildNodes();
LOG_TRACE("[CHECKPOINT] OnStart: Nodes built, about to initialize context");
m_Context->init(this);
LOG_TRACE("[CHECKPOINT] OnStart: Context initialized, about to load textures");
m_HeaderBackground = LoadTexture("data/BlueprintBackground.png");
m_SaveIcon = LoadTexture("data/ic_save_white_24dp.png");
m_RestoreIcon = LoadTexture("data/ic_restore_white_24dp.png");
LOG_TRACE("[CHECKPOINT] OnStart: Textures loaded, about to navigate to content");
// Only navigate to content if we don't have saved view settings
// (m_NeedsInitialZoom is set to false in LoadViewSettings if Blueprints.json exists)
if (m_NeedsInitialZoom)
{
LOG_TRACE("[CHECKPOINT] OnStart: No saved view state, navigating to content");
ed::NavigateToContent(0.0f);
}
else
{
LOG_TRACE("[CHECKPOINT] OnStart: Saved view state will be restored, skipping initial zoom");
}
LOG_TRACE("[CHECKPOINT] OnStart: Complete");
}
void App::OnStop()
{
// Save graph before shutdown
auto* activeContainer = GetActiveRootContainer();
if (activeContainer)
{
SaveGraph(m_GraphFilename, activeContainer);
}
auto releaseTexture = [this](ImTextureID& id)
{
if (id)
{
DestroyTexture(id);
id = nullptr;
}
};
releaseTexture(m_RestoreIcon);
releaseTexture(m_SaveIcon);
releaseTexture(m_HeaderBackground);
if (m_Editor)
{
ed::DestroyEditor(m_Editor);
m_Editor = nullptr;
}
// Shutdown spdlog logger
ShutdownLogger();
}
// UUID Generation Methods (32-bit)
uint32_t App::GenerateRandomUuid()
{
return m_UuidGenerator.GenerateRandom();
}
uint32_t App::GenerateSequentialUuid()
{
return m_UuidGenerator.GenerateSequential();
}
std::string App::UuidToHexString(uint32_t uuid)
{
return UuidGenerator::ToHexString(uuid);
}
uint32_t App::HexStringToUuid(const std::string& hexString)
{
return UuidGenerator::FromHexString(hexString);
}
// UUID Generation Methods (64-bit using dual 32-bit words)
Uuid64 App::GenerateRandomUuid64()
{
return m_UuidGenerator.GenerateRandom64();
}
Uuid64 App::GenerateSequentialUuid64()
{
return m_UuidGenerator.GenerateSequential64();
}
std::string App::UuidToHexString64(const Uuid64& uuid)
{
return UuidGenerator::ToHexString64(uuid);
}
Uuid64 App::HexStringToUuid64(const std::string& hexString)
{
return UuidGenerator::FromHexString64(hexString);
}
// Standard UUID Format Conversion Methods
std::string App::UuidToStandardString(const Uuid64& uuid)
{
return uuid.ToStandardUuidString();
}
Uuid64 App::StandardStringToUuid(const std::string& uuidStr, bool takeLast64)
{
return Uuid64::FromStandardUuidString(uuidStr, takeLast64);
}

View File

@ -0,0 +1,203 @@
#pragma once
#include <base.h>
#include "types.h"
#include "blocks/parameter_node.h"
#include "utilities/edge_editing.h"
#include "utilities/style_manager.h"
#include "utilities/uuid_generator.h"
#include "utilities/uuid_id_manager.h"
#include "containers/container.h"
#include "containers/root_container.h"
#include "core/graph_state.h"
#include <map>
#include "app-types.h"
#include "core/Context.h"
namespace ed = ax::NodeEditor;
class App: public Application
{
public:
App();
App(const char* name);
App(const char* name, const ArgsMap& args);
using Application::Application;
// Application lifecycle
void OnStart() override;
void OnStop() override;
void OnFrame(float deltaTime) override;
// ID generation (delegates to GraphState)
int GetNextId();
ed::LinkId GetNextLinkId();
// UUID generation (32-bit)
uint32_t GenerateRandomUuid(); // Generate random UUID (e.g., 0x50626ea)
uint32_t GenerateSequentialUuid(); // Generate sequential UUID
std::string UuidToHexString(uint32_t uuid); // Convert UUID to hex string
uint32_t HexStringToUuid(const std::string& hexString); // Parse hex string to UUID
// UUID generation (64-bit using dual 32-bit words for microcontroller compatibility)
Uuid64 GenerateRandomUuid64(); // Generate random 64-bit UUID
Uuid64 GenerateSequentialUuid64(); // Generate sequential 64-bit UUID
std::string UuidToHexString64(const Uuid64& uuid); // Convert 64-bit UUID to hex string
Uuid64 HexStringToUuid64(const std::string& hexString); // Parse hex string to 64-bit UUID
// Standard UUID format conversion (RFC4122 with dashes)
std::string UuidToStandardString(const Uuid64& uuid); // Convert to "00000000-0000-0000-HHHHHHHH-LLLLLLLL"
Uuid64 StandardStringToUuid(const std::string& uuidStr, bool takeLast64 = true); // Parse "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
UuidGenerator& GetUuidGenerator() { return m_UuidGenerator; } // Direct access to generator
// Node/Pin/Link lookups
Node* FindNode(ed::NodeId id);
Link* FindLink(ed::LinkId id);
Pin* FindPin(ed::PinId id);
// Node state management
void TouchNode(ed::NodeId id);
float GetTouchProgress(ed::NodeId id);
void UpdateTouch();
// Graph persistence (nodes, links, positions, control points)
void SaveGraph(const std::string& filename, RootContainer* container);
void LoadGraph(const std::string& filename, RootContainer* container);
// View settings persistence (scroll, zoom, selection only)
size_t LoadViewSettings(char* data);
bool SaveViewSettings(const char* data, size_t size);
// Validation
bool IsPinLinked(ed::PinId id);
bool CanCreateLink(Pin* a, Pin* b);
bool IsLinkDuplicate(ed::PinId startPinId, ed::PinId endPinId);
Link* FindLinkConnectedToPin(ed::PinId pinId); // Find link where pin is StartPinID or EndPinID
// Runtime execution interface (for imgui_node_editor_runtime.cpp)
bool ExecuteRuntimeStep(); // Returns true if any work was processed this step
// Visualization helpers (only called when rendering)
void VisualizeRuntimeExecution(const std::vector<Node*>& executedNodes,
const std::vector<ed::LinkId>& affectedLinks);
// Node building
void BuildNode(Node* node);
void BuildNodes();
// Rendering
void DrawPinIcon(const Pin& pin, bool connected, int alpha);
void ShowStyleEditor(bool* show = nullptr);
void ShowLeftPane(float paneWidth);
ImColor GetIconColor(PinType type);
// OnFrame helpers (split from massive OnFrame function)
void RenderDebugInfo();
void HandleKeyboardShortcuts(std::vector<ed::NodeId>& selectedNodes, std::vector<ed::LinkId>& selectedLinks);
void RenderNodes(Pin* newLinkPin);
void RenderLinks();
// Link rendering functions for each link type
void RenderAutoLink(Link& link, ImColor linkColor, float thickness);
void RenderStraightLink(Link& link, ImColor linkColor, float thickness);
void RenderGuidedLink(Link& link, ImColor linkColor, float thickness);
void RenderLinkDelay(); // Show delay on hover and render edit box
void StartEditLinkDelay(ed::LinkId linkId); // Start editing delay for a link (called from keyboard shortcut or context menu)
void ApplyPendingGuidedLinks();
void AutoAdjustLinkWaypoints();
void UpdateLastNodePositions();
void UpdateLastNodeSize(ed::NodeId nodeId); // Update last known size for a node (used when display mode changes)
void UpdateGuidedLinks();
void MarkLinkUserManipulated(ed::LinkId linkId); // Mark link as user-manipulated (preserve waypoints)
void HandleLinkCreationAndDeletion();
void RenderDeferredTooltips();
void RenderContextMenus();
void RenderOrdinals();
// Root container management
// RootContainer is defined in containers/root_container.h (included via containers/container.h)
RootContainer* GetActiveRootContainer();
RootContainer* AddRootContainer(const std::string& filename);
void RemoveRootContainer(RootContainer* container);
void SetActiveRootContainer(RootContainer* container);
Container* FindContainerForNode(ed::NodeId nodeId); // Search all root containers
// NOTE: Nodes and Links are owned by RootContainer objects, managed by GraphState
// All container and graph data access goes through m_GraphState
GraphState m_GraphState; // Central graph data management
ed::StyleManager m_StyleManager; // Style management with JSON persistence
UuidGenerator m_UuidGenerator; // UUID generation for unique identifiers
UuidIdManager m_UuidIdManager; // UUID <-> Runtime ID mapping
const int m_PinIconSize = 24;
ImTextureID m_HeaderBackground = nullptr;
ImTextureID m_SaveIcon = nullptr;
ImTextureID m_RestoreIcon = nullptr;
// Style accessor for other classes
ed::StyleManager& GetStyleManager() { return m_StyleManager; }
// Node spawning helpers
Node* SpawnBlockNode(const char* blockType, int nodeId = -1);
Node* SpawnParameterNode(PinType paramType, int nodeId = -1, ParameterDisplayMode displayMode = ParameterDisplayMode::NameAndValue);
bool m_ShowOrdinals = false;
EdgeEditor m_EdgeEditor; // Edge dragging system
// Deferred tooltip system (tooltips must be rendered in ed::Suspend() to avoid coordinate issues)
Pin* m_HoveredPin = nullptr;
std::string m_HoveredPinTooltip;
// Helper to safely delete a node and its owned instances
void DeleteNodeAndInstances(Node& node);
// Screenshot functionality
void TakeScreenshot(const char* filename = nullptr);
void RenderScreenshotNotification(float deltaTime);
std::string m_ScreenshotMessage;
float m_ScreenshotMessageTime = 0.0f;
// Graph filename (can be set via CLI --graph argument)
std::string m_GraphFilename = "BlueprintsGraph.json";
// Node highlighting for Run() visualization (red borders)
// Made public so blocks can access it for visualization
std::map<ed::NodeId, float, NodeIdLess> m_RunningNodes; // Node ID -> expiration time
NH_Context* GetContext();
private:
void initLogger();
// Container management delegated to GraphState - see m_GraphState
// Link highlighting for Run() visualization
std::map<ed::LinkId, float, LinkIdLess> m_HighlightedLinks; // Link ID -> expiration time
// Initial zoom flag - zoom to content on first frame after load
bool m_NeedsInitialZoom = true;
private:
const float m_TouchTime = 1.0f;
std::map<ed::NodeId, float, NodeIdLess> m_NodeTouchTime;
std::map<ed::LinkId, std::vector<ImVec2>, LinkIdLess> m_PendingGuidedLinks; // Links waiting for guided mode setup
std::map<ed::LinkId, ax::NodeEditor::LinkMode, LinkIdLess> m_PendingLinkModes; // Links waiting for mode setup (Straight/Guided)
std::map<ed::NodeId, ImVec2, NodeIdLess> m_LastNodePositions; // Track node positions for movement detection
std::map<ed::NodeId, ImVec2, NodeIdLess> m_LastNodeSizes; // Track node sizes for size change detection
// UI state (moved from OnFrame static variables)
ed::NodeId m_ContextNodeId = 0;
ed::LinkId m_ContextLinkId = 0;
ed::PinId m_ContextPinId = 0;
bool m_CreateNewNode = false;
Pin* m_NewNodeLinkPin = nullptr;
Pin* m_NewLinkPin = nullptr;
float m_LeftPaneWidth = 400.0f;
float m_RightPaneWidth = 800.0f;
NH_Context *m_Context;
};

View File

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

View File

@ -0,0 +1,99 @@
// NodeEx.h - Extended Node Builder for Precise Pin Placement
// Provides PinEx function for placing pins at specific node edges
#pragma once
#include <imgui.h>
#include <imgui_node_editor.h>
#include <imgui_internal.h>
namespace ax {
namespace NodeEditor {
// Enum for pin edge locations
enum class PinEdge
{
Top,
Bottom,
Left,
Right
};
// Enum for pin states
enum class PinState
{
Normal, // Default state
Running, // Active/running state (e.g., green)
Deactivated, // Disabled/inactive state (e.g., gray)
Error, // Error state (e.g., red)
Warning // Warning state (e.g., yellow)
};
// Renderer function type for custom pin rendering
// Parameters:
// drawList: The ImGui draw list to draw to
// center: Center position of the pin
// pinSize: Size of the pin (ImVec2)
// fillColor: Fill color (ImU32)
// borderColor: Border color (ImU32)
// state: The pin state (for state-aware rendering)
typedef void (*PinRenderer)(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize, ImU32 fillColor, ImU32 borderColor, PinState state);
// Default renderers provided by NodeEx
void RenderPinCircle(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize, ImU32 fillColor, ImU32 borderColor, PinState state);
void RenderPinBox(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize, ImU32 fillColor, ImU32 borderColor, PinState state);
// Helper function to get colors based on pin kind and state
void GetPinColors(PinKind kind, PinState state, ImU32& fillColor, ImU32& borderColor);
// PinEx: Precise pin placement at node edges
// Must be called between BeginNode() and EndNode()
//
// Parameters:
// pinId: The pin ID
// kind: Input or Output pin kind
// edge: Edge location (Top, Bottom, Left, Right)
// offset: Offset along the edge (0.0 = start, 1.0 = end, 0.5 = center)
// direction: Distance from edge (0 = on edge, positive = outside, negative = inside)
// nodeRect: The node's bounds rectangle (can be obtained via ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()) after content)
// state: Pin state (Normal, Running, Deactivated, Error, Warning) - affects colors
// tooltip: Optional tooltip text shown on hover (nullptr = no tooltip)
// renderer: Optional custom renderer function. If nullptr, uses default (box for Top/Bottom, circle for Left/Right)
//
// Returns: The pin rectangle for reference
ImRect PinEx(
PinId pinId,
PinKind kind,
PinEdge edge,
float offset,
float direction,
const ImRect& nodeRect,
PinState state = PinState::Normal,
const char* tooltip = nullptr,
PinRenderer renderer = nullptr);
// Overload: PinEx with nodeId (for use after EndNode, when node bounds are finalized)
// Note: This version is less precise as it relies on GetNodePosition/GetNodeSize
// For precise placement, use the nodeRect version during node building
ImRect PinEx(
PinId pinId,
PinKind kind,
PinEdge edge,
float offset,
float direction,
NodeId nodeId,
PinState state = PinState::Normal,
const char* tooltip = nullptr,
PinRenderer renderer = nullptr);
// Helper: Process tooltips for pins (call this after EndNode, typically in a deferred section)
// This checks if any pin is hovered and displays its tooltip
// Note: Pin tooltips need to be processed in a deferred manner due to node editor coordinate space
// Call this after all nodes are built, typically in a Suspend/Resume block like widgets-example
void ProcessPinTooltips();
// Clear tooltip data (call at start of frame before building nodes)
void ClearPinTooltips();
} // namespace NodeEditor
} // namespace ax

View File

@ -0,0 +1,782 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include <crude_json.h>
#include "block.h"
#include "../app.h"
#include "../utilities/node_renderer_base.h"
#include "../containers/container.h"
#include "NodeEx.h"
#include "constants.h"
#include <imgui_node_editor.h>
#include <imgui_internal.h>
#include <set>
#include "../Logging.h"
namespace ed = ax::NodeEditor;
using namespace ax::NodeRendering;
using namespace NodeConstants;
// Storage for parameter pin rendering context (to work around function pointer limitations)
struct ParameterPinContext {
const Pin* pin;
bool isLinked;
App* app;
};
static ParameterPinContext s_CurrentParamPinContext = {nullptr, false, nullptr};
// Wrapper for parameter pins - uses NodeEx's RenderPinCircle
static void RenderParameterPin(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize,
ImU32 fillColor, ImU32 borderColor, ed::PinState state)
{
// Use NodeEx's RenderPinCircle directly
ed::RenderPinCircle(drawList, center, pinSize, fillColor, borderColor, state);
}
// Wrapper for flow pins - uses NodeEx's RenderPinBox
static void RenderFlowPin(ImDrawList* drawList, const ImVec2& center, const ImVec2& pinSize,
ImU32 fillColor, ImU32 borderColor, ed::PinState state)
{
// Use NodeEx's RenderPinBox directly
ed::RenderPinBox(drawList, center, pinSize, fillColor, borderColor, state);
}
void ParameterizedBlock::Render(Node& node, App* app, Pin* newLinkPin)
{
// Check if node is currently running (for red border visualization)
double currentTime = (double)ImGui::GetTime();
bool isRunning = false;
auto runningIt = app->m_RunningNodes.find(node.ID);
if (runningIt != app->m_RunningNodes.end())
{
isRunning = (currentTime < runningIt->second);
}
// Get styles from StyleManager
auto& styleManager = app->GetStyleManager();
auto& blockStyle = styleManager.BlockStyle;
ImColor borderColor = isRunning ? blockStyle.BorderColorRunning : blockStyle.BorderColor;
float activeBorderWidth = isRunning ? blockStyle.BorderWidthRunning : blockStyle.BorderWidth;
// Use RAII style scope for automatic cleanup
NodeStyleScope style(
blockStyle.BgColor, // bg
borderColor, // border (red if running)
blockStyle.Rounding, activeBorderWidth, // rounding, borderWidth (thicker if running)
blockStyle.Padding, // padding
ImVec2(0.0f, 1.0f), // source direction (down)
ImVec2(0.0f, -1.0f) // target direction (up)
);
ed::BeginNode(node.ID);
ImGui::PushID(node.ID.AsPointer());
ImGui::BeginVertical("node");
// Center - header (simple centered text)
ImGui::BeginHorizontal("content");
ImGui::Spring(1, 0.0f);
ImGui::BeginVertical("center");
ImGui::Dummy(ImVec2(styleManager.MinNodeWidth, 0)); // Minimum width
ImGui::Spring(1);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
ImGui::TextUnformatted(node.Name.c_str());
ImGui::PopStyleColor();
ImGui::Spring(1);
ImGui::EndVertical();
ImGui::Spring(1, 0.0f);
ImGui::EndHorizontal();
ImGui::EndVertical();
// Save cursor position before placing pins
ImVec2 contentEndPos = ImGui::GetCursorScreenPos();
// Get node bounds BEFORE EndNode (like in basic-interaction-example)
ImVec2 nodePos = ed::GetNodePosition(node.ID);
ImVec2 nodeSize = ed::GetNodeSize(node.ID);
// If node size is not yet determined (first frame), calculate from content
if (nodeSize.x <= 0 || nodeSize.y <= 0)
{
ImVec2 contentMin = ImGui::GetItemRectMin();
ImVec2 contentMax = ImGui::GetItemRectMax();
nodeSize = contentMax - contentMin;
}
// Calculate final node bounds
ImRect nodeRect = ImRect(nodePos, nodePos + nodeSize);
// Now place all pins using PinEx (BEFORE EndNode, using nodeRect)
// Count pins for offset calculation
std::vector<Pin*> inputParams;
std::vector<Pin*> outputParams;
std::vector<Pin*> flowInputs;
std::vector<Pin*> flowOutputs;
for (auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
flowInputs.push_back(&pin);
else
inputParams.push_back(&pin);
}
for (auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
flowOutputs.push_back(&pin);
else
outputParams.push_back(&pin);
}
// Render input parameters at top edge
if (!inputParams.empty())
{
float spacing = 1.0f / (inputParams.size() + 1);
for (size_t i = 0; i < inputParams.size(); ++i)
{
Pin* pin = inputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
bool isLinked = app->IsPinLinked(pin->ID);
// Set context for parameter pin renderer
s_CurrentParamPinContext.pin = pin;
s_CurrentParamPinContext.isLinked = isLinked;
s_CurrentParamPinContext.app = app;
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Input, ed::PinEdge::Top,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state, nullptr, RenderParameterPin);
// Store position data
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Min.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render output parameters at bottom edge
if (!outputParams.empty())
{
float spacing = 1.0f / (outputParams.size() + 1);
for (size_t i = 0; i < outputParams.size(); ++i)
{
Pin* pin = outputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
bool isLinked = app->IsPinLinked(pin->ID);
// Set context for parameter pin renderer
s_CurrentParamPinContext.pin = pin;
s_CurrentParamPinContext.isLinked = isLinked;
s_CurrentParamPinContext.app = app;
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Output, ed::PinEdge::Bottom,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state, nullptr, RenderParameterPin);
// Store position data
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Max.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render flow inputs at left edge
if (!flowInputs.empty())
{
float spacing = 1.0f / (flowInputs.size() + 1);
for (size_t i = 0; i < flowInputs.size(); ++i)
{
Pin* pin = flowInputs[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Input, ed::PinEdge::Left,
offset, styleManager.FlowPinEdgeOffset, nodeRect, state, nullptr, RenderFlowPin);
// Store position data
pin->LastPivotPosition = ImVec2(pinRect.Min.x, pinRect.GetCenter().y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render flow outputs at right edge
if (!flowOutputs.empty())
{
float spacing = 1.0f / (flowOutputs.size() + 1);
for (size_t i = 0; i < flowOutputs.size(); ++i)
{
Pin* pin = flowOutputs[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Output, ed::PinEdge::Right,
offset, styleManager.FlowPinEdgeOffset, nodeRect, state, nullptr, RenderFlowPin);
// Store position data
pin->LastPivotPosition = ImVec2(pinRect.Max.x, pinRect.GetCenter().y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Restore cursor to end of content (pins don't affect layout)
ImGui::SetCursorScreenPos(contentEndPos);
ImGui::PopID();
ed::EndNode();
// NodeStyleScope destructor handles cleanup automatically
}
void ParameterizedBlock::AddInputParameter(App* app, Node& node, NH_CSTRING name, PinType type)
{
int pinId = app->GetNextId();
m_InputParams.push_back(pinId);
// Add as input pin (will be rendered at top or connected from elsewhere)
node.Inputs.emplace_back(pinId, name, type);
}
void ParameterizedBlock::AddOutputParameter(App* app, Node& node, NH_CSTRING name, PinType type)
{
int pinId = app->GetNextId();
m_OutputParams.push_back(pinId);
// Add as output pin (will be rendered at bottom)
node.Outputs.emplace_back(pinId, name, type);
}
void ParameterizedBlock::AddInput(App* app, Node& node, NH_CSTRING name)
{
int pinId = app->GetNextId();
m_Inputs.push_back(pinId);
NH_CSTRING displayName = (name && *name) ? name : "";
node.Inputs.emplace_back(pinId, displayName, PinType::Flow);
}
void ParameterizedBlock::AddOutput(App* app, Node& node, NH_CSTRING name)
{
int pinId = app->GetNextId();
m_Outputs.push_back(pinId);
NH_CSTRING displayName = (name && *name) ? name : "";
node.Outputs.emplace_back(pinId, displayName, PinType::Flow);
}
// Block activation API implementation
void Block::ActivateOutput(int pos, bool active)
{
if (pos < 0) return;
if (pos >= static_cast<int>(m_OutputActive.size()))
m_OutputActive.resize(pos + 1, false);
m_OutputActive[pos] = active;
}
bool Block::IsOutputActive(int pos) const
{
if (pos < 0 || pos >= static_cast<int>(m_OutputActive.size()))
return false;
return m_OutputActive[pos];
}
void Block::ActivateInput(int pos, bool active)
{
if (pos < 0) return;
if (pos >= static_cast<int>(m_InputActive.size()))
m_InputActive.resize(pos + 1, false);
m_InputActive[pos] = active;
}
bool Block::IsInputActive(int pos) const
{
if (pos < 0 || pos >= static_cast<int>(m_InputActive.size()))
return false;
return m_InputActive[pos];
}
void ParameterizedBlock::OnMenu(Node& node, App* app)
{
ImGui::Separator();
const char* modeStr = "Unknown";
if (node.BlockDisplay == BlockDisplayMode::NameOnly) modeStr = "Name Only";
else if (node.BlockDisplay == BlockDisplayMode::NameAndParameters) modeStr = "Name + Parameters";
ImGui::Text("Display: %s", modeStr);
if (ImGui::MenuItem("Cycle Display Mode (Space)"))
{
if (node.BlockDisplay == BlockDisplayMode::NameOnly)
node.BlockDisplay = BlockDisplayMode::NameAndParameters;
else
node.BlockDisplay = BlockDisplayMode::NameOnly;
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node.ID);
}
if (ImGui::MenuItem("Run (R)"))
{
int result = Run(node, app);
// LOG_INFO("Block '{}' (ID: {}) Run() returned: {}", node.Name.c_str(), node.ID.Get(), result);
}
}
// Parameter value helper implementations
int ParameterizedBlock::GetInputParamValueInt(const Pin& pin, Node& node, App* app, int defaultValue)
{
// Check if connected to a parameter node or block
auto* link = app->FindLinkConnectedToPin(pin.ID);
if (link && link->EndPinID == pin.ID)
{
auto* sourcePin = app->FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node)
{
if (sourcePin->Node->Type == NodeType::Parameter)
{
// Run source parameter node first to get latest value
sourcePin->Node->ParameterInstance->Run(*sourcePin->Node, app);
return sourcePin->Node->IntValue;
}
else if (sourcePin->Node->IsBlockBased())
{
// Source is a block output - read from UnconnectedParamValues
const int sourcePinId = ToRuntimeId(sourcePin->ID);
auto& sourceParamValues = sourcePin->Node->UnconnectedParamValues;
if (sourceParamValues.find(sourcePinId) != sourceParamValues.end())
{
try {
return std::stoi(sourceParamValues[sourcePinId]);
} catch (...) {
return defaultValue;
}
}
}
}
}
// Not connected, use default value from UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
if (paramValues.find(pinId) != paramValues.end())
{
try {
return std::stoi(paramValues[pinId]);
} catch (...) {
return defaultValue;
}
}
return defaultValue;
}
float ParameterizedBlock::GetInputParamValueFloat(const Pin& pin, Node& node, App* app, float defaultValue)
{
// Check if connected to a parameter node or block
auto* link = app->FindLinkConnectedToPin(pin.ID);
if (link && link->EndPinID == pin.ID)
{
auto* sourcePin = app->FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node)
{
if (sourcePin->Node->Type == NodeType::Parameter)
{
// Run source parameter node first to get latest value
sourcePin->Node->ParameterInstance->Run(*sourcePin->Node, app);
return sourcePin->Node->FloatValue;
}
else if (sourcePin->Node->IsBlockBased())
{
// Source is a block output - read from UnconnectedParamValues
const int sourcePinId = ToRuntimeId(sourcePin->ID);
auto& sourceParamValues = sourcePin->Node->UnconnectedParamValues;
if (sourceParamValues.find(sourcePinId) != sourceParamValues.end())
{
try {
return std::stof(sourceParamValues[sourcePinId]);
} catch (...) {
return defaultValue;
}
}
}
}
}
// Not connected, use default value from UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
if (paramValues.find(pinId) != paramValues.end())
{
try {
return std::stof(paramValues[pinId]);
} catch (...) {
return defaultValue;
}
}
return defaultValue;
}
bool ParameterizedBlock::GetInputParamValueBool(const Pin& pin, Node& node, App* app, bool defaultValue)
{
// Check if connected to a parameter node or block
auto* link = app->FindLinkConnectedToPin(pin.ID);
if (link && link->EndPinID == pin.ID)
{
auto* sourcePin = app->FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node)
{
if (sourcePin->Node->Type == NodeType::Parameter)
{
// Run source parameter node first to get latest value
sourcePin->Node->ParameterInstance->Run(*sourcePin->Node, app);
return sourcePin->Node->BoolValue;
}
else if (sourcePin->Node->IsBlockBased())
{
// Source is a block output - read from UnconnectedParamValues
const int sourcePinId = ToRuntimeId(sourcePin->ID);
auto& sourceParamValues = sourcePin->Node->UnconnectedParamValues;
if (sourceParamValues.find(sourcePinId) != sourceParamValues.end())
{
const std::string& valueStr = sourceParamValues[sourcePinId];
if (valueStr == "true" || valueStr == "1")
return true;
else if (valueStr == "false" || valueStr == "0")
return false;
}
}
}
}
// Not connected, use default value from UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
if (paramValues.find(pinId) != paramValues.end())
{
const std::string& valueStr = paramValues[pinId];
if (valueStr == "true" || valueStr == "1")
return true;
else if (valueStr == "false" || valueStr == "0")
return false;
}
return defaultValue;
}
std::string ParameterizedBlock::GetInputParamValueString(const Pin& pin, Node& node, App* app, const std::string& defaultValue)
{
// Check if connected to a parameter node or block
auto* link = app->FindLinkConnectedToPin(pin.ID);
if (link && link->EndPinID == pin.ID)
{
auto* sourcePin = app->FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node)
{
if (sourcePin->Node->Type == NodeType::Parameter)
{
// Run source parameter node first to get latest value
sourcePin->Node->ParameterInstance->Run(*sourcePin->Node, app);
return sourcePin->Node->StringValue;
}
else if (sourcePin->Node->IsBlockBased())
{
// Source is a block output - read from UnconnectedParamValues
const int sourcePinId = ToRuntimeId(sourcePin->ID);
auto& sourceParamValues = sourcePin->Node->UnconnectedParamValues;
auto it = sourceParamValues.find(sourcePinId);
if (it != sourceParamValues.end())
{
return it->second;
}
}
}
}
// Not connected, use default value from UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
auto it = paramValues.find(pinId);
if (it != paramValues.end())
{
return it->second;
}
return defaultValue;
}
void ParameterizedBlock::SetOutputParamValueInt(const Pin& pin, Node& node, App* app, int value)
{
// Store output value in node's UnconnectedParamValues (output pins can be read by connected nodes)
const int pinId = ToRuntimeId(pin.ID);
node.UnconnectedParamValues[pinId] = std::to_string(value);
// Propagate to ALL connected parameter nodes (iterate through all links from active container)
auto* rootContainer = app->GetActiveRootContainer();
if (!rootContainer) return;
auto links = rootContainer->GetAllLinks();
for (Link* linkPtr : links)
{
if (!linkPtr) continue;
const auto& link = *linkPtr;
if (link.StartPinID == pin.ID) // We're the source (output pin)
{
auto* targetPin = app->FindPin(link.EndPinID);
if (targetPin && targetPin->Node && targetPin->Node->Type == NodeType::Parameter)
{
// Update connected parameter node
targetPin->Node->IntValue = value;
if (targetPin->Node->ParameterInstance)
targetPin->Node->ParameterInstance->SetInt(value);
}
}
}
}
void ParameterizedBlock::SetOutputParamValueFloat(const Pin& pin, Node& node, App* app, float value)
{
// Store output value in node's UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
char buf[32];
snprintf(buf, sizeof(buf), "%.6g", value);
node.UnconnectedParamValues[pinId] = buf;
// Propagate to ALL connected parameter nodes (iterate through all links from active container)
auto* rootContainer = app->GetActiveRootContainer();
if (!rootContainer) return;
auto links = rootContainer->GetAllLinks();
for (Link* linkPtr : links)
{
if (!linkPtr) continue;
const auto& link = *linkPtr;
if (link.StartPinID == pin.ID)
{
auto* targetPin = app->FindPin(link.EndPinID);
if (targetPin && targetPin->Node && targetPin->Node->Type == NodeType::Parameter)
{
targetPin->Node->FloatValue = value;
if (targetPin->Node->ParameterInstance)
targetPin->Node->ParameterInstance->SetFloat(value);
}
}
}
}
void ParameterizedBlock::SetOutputParamValueBool(const Pin& pin, Node& node, App* app, bool value)
{
// Store output value in node's UnconnectedParamValues
const int pinId = ToRuntimeId(pin.ID);
node.UnconnectedParamValues[pinId] = value ? "true" : "false";
// Propagate to ALL connected parameter nodes (iterate through all links from active container)
auto* rootContainer = app->GetActiveRootContainer();
if (!rootContainer) return;
auto links = rootContainer->GetAllLinks();
for (Link* linkPtr : links)
{
if (!linkPtr) continue;
const auto& link = *linkPtr;
if (link.StartPinID == pin.ID) // We're the source (output pin)
{
auto* targetPin = app->FindPin(link.EndPinID);
if (targetPin && targetPin->Node && targetPin->Node->Type == NodeType::Parameter)
{
// Update connected parameter node
targetPin->Node->BoolValue = value;
if (targetPin->Node->ParameterInstance)
targetPin->Node->ParameterInstance->SetBool(value);
}
}
}
}
void ParameterizedBlock::SetOutputParamValueString(const Pin& pin, Node& node, App* app, const std::string& value)
{
// Store output value in node's UnconnectedParamValues
int pinId = ToRuntimeId(pin.ID);
node.UnconnectedParamValues[pinId] = value;
// Propagate to ALL connected parameter nodes (iterate through all links from active container)
auto* rootContainer = app->GetActiveRootContainer();
if (!rootContainer) return;
auto links = rootContainer->GetAllLinks();
for (Link* linkPtr : links)
{
if (!linkPtr) continue;
const auto& link = *linkPtr;
if (link.StartPinID == pin.ID)
{
auto* targetPin = app->FindPin(link.EndPinID);
if (targetPin && targetPin->Node && targetPin->Node->Type == NodeType::Parameter)
{
targetPin->Node->StringValue = value;
if (targetPin->Node->ParameterInstance)
targetPin->Node->ParameterInstance->SetString(value);
}
}
}
}
void ParameterizedBlock::SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app)
{
// Save all input and output parameter values with their types, regardless of connection status
if (!app || !container)
return;
crude_json::value& inputs = nodeData["inputs"];
// Save all input parameter values (connected or not)
for (auto& pin : node.Inputs)
{
// Skip flow pins - only save parameter input pins
if (pin.Type == PinType::Flow)
continue;
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
auto it = paramValues.find(pinId);
// Save input value and type if it exists (save regardless of connection status)
if (it != paramValues.end() && !it->second.empty())
{
// Save pin ID, value as string, and type
crude_json::value paramEntry;
paramEntry["pin_id"] = (double)pinId;
paramEntry["value"] = it->second;
paramEntry["type"] = (double)static_cast<int>(pin.Type);
inputs.push_back(paramEntry);
}
else
{
// Even if no value stored, save the pin with its type (for structure preservation)
crude_json::value paramEntry;
paramEntry["pin_id"] = (double)pinId;
paramEntry["value"] = "";
paramEntry["type"] = (double)static_cast<int>(pin.Type);
inputs.push_back(paramEntry);
}
}
// Save all output parameter values (connected or not)
crude_json::value& outputs = nodeData["outputs"];
for (auto& pin : node.Outputs)
{
// Skip flow pins - only save parameter output pins
if (pin.Type == PinType::Flow)
continue;
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node.UnconnectedParamValues;
auto it = paramValues.find(pinId);
// Save output value and type if it exists (save regardless of connection status)
if (it != paramValues.end() && !it->second.empty())
{
// Save pin ID, value as string, and type
crude_json::value paramEntry;
paramEntry["pin_id"] = (double)pinId;
paramEntry["value"] = it->second;
paramEntry["type"] = (double)static_cast<int>(pin.Type);
outputs.push_back(paramEntry);
}
else
{
// Even if no value stored, save the pin with its type (for structure preservation)
crude_json::value paramEntry;
paramEntry["pin_id"] = (double)pinId;
paramEntry["value"] = "";
paramEntry["type"] = (double)static_cast<int>(pin.Type);
outputs.push_back(paramEntry);
}
}
}
void ParameterizedBlock::LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app)
{
// Load unconnected parameter input values
if (nodeData.contains("inputs"))
{
const auto& inputs = nodeData["inputs"];
if (inputs.is_array())
{
// Restore values to UnconnectedParamValues map
auto& paramValues = node.UnconnectedParamValues;
for (const auto& paramEntry : inputs.get<crude_json::array>())
{
if (!paramEntry.is_object() || !paramEntry.contains("pin_id") || !paramEntry.contains("value"))
continue;
int pinId = (int)paramEntry["pin_id"].get<double>();
std::string value = paramEntry["value"].get<crude_json::string>();
// Verify this pin ID exists in the node's inputs
// This handles cases where pin IDs might have changed (though they shouldn't)
bool pinExists = false;
for (const auto& pin : node.Inputs)
{
if (pin.ID.Get() == pinId)
{
pinExists = true;
// Store the value - pin type validation happens when reading
paramValues[pinId] = value;
break;
}
}
// Note: We store even if pin not found yet (in case node structure changes)
// The GetInputParamValue* functions will handle missing pins gracefully
if (!pinExists)
{
paramValues[pinId] = value;
}
}
}
}
// Load output parameter values (connected or not)
if (nodeData.contains("outputs"))
{
const auto& outputs = nodeData["outputs"];
if (outputs.is_array())
{
// Restore values to UnconnectedParamValues map
auto& paramValues = node.UnconnectedParamValues;
for (const auto& paramEntry : outputs.get<crude_json::array>())
{
if (!paramEntry.is_object() || !paramEntry.contains("pin_id") || !paramEntry.contains("value"))
continue;
int pinId = (int)paramEntry["pin_id"].get<double>();
std::string value = paramEntry["value"].get<crude_json::string>();
// Verify this pin ID exists in the node's outputs
bool pinExists = false;
for (const auto& pin : node.Outputs)
{
if (pin.ID.Get() == pinId)
{
pinExists = true;
// Store the value - pin type validation happens when reading
paramValues[pinId] = value;
break;
}
}
// Note: We store even if pin not found yet (in case node structure changes)
if (!pinExists)
{
paramValues[pinId] = value;
}
}
}
}
}

View File

@ -0,0 +1,190 @@
#pragma once
#include "../commons.h"
#include "../core/Object.h"
#include "../utilities/node_renderer_base.h"
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <imgui.h>
// Forward declarations
class App;
class Container;
namespace crude_json { struct value; }
class Block : public NH_Object
{
public:
Block(int id, NH_CSTRING name)
: NH_Object(id, name)
, m_Type(NodeType::Blueprint)
, m_Color(ImColor(255, 255, 255))
{
}
virtual ~Block() = default;
// Core identification
int GetID() const { return NH_Object::GetID(); }
NH_CSTRING GetName() const { return NH_Object::GetName(); }
NH_CSTRING GetTypeName() const { return NH_Object::GetTypeName(); }
NH_BEHAVIOR_FLAGS GetFlags() const { return GetBehaviorFlags(); }
void SetFlags(NH_BEHAVIOR_FLAGS flags) { SetBehaviorFlags(flags); }
void AddFlags(NH_BEHAVIOR_FLAGS flags) { AddBehaviorFlags(flags); }
void RemoveFlags(NH_BEHAVIOR_FLAGS flags) { RemoveBehaviorFlags(flags); }
// Node building (like NHBehavior's CreateInput/CreateOutput)
virtual void Build(Node& node, App* app) = 0;
// Rendering (each block renders itself)
virtual void Render(Node& node, App* app, Pin* newLinkPin) = 0;
// Serialization support
virtual NH_CSTRING GetBlockType() const = 0; // Unique type identifier
// State save/load callbacks (optional - subclasses can override to save/load custom state)
// These are called from App::SaveGraph() / App::LoadGraph()
virtual void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) {}
virtual void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) {}
// Parameter management (like NHBehavior)
virtual int GetInputParameterCount() const { return 0; }
virtual int GetOutputParameterCount() const { return 0; }
virtual int GetInputCount() const { return 0; } // Flow inputs (triggers)
virtual int GetOutputCount() const { return 0; } // Flow outputs (events)
// Execution (stub: returns E_OK = 0)
virtual int Run(Node& node, App* app) { return E_OK; }
// Activation API (for flow control)
virtual void ActivateOutput(int pos, bool active = true);
virtual bool IsOutputActive(int pos) const;
virtual void ActivateInput(int pos, bool active = true);
virtual bool IsInputActive(int pos) const;
// Context menu hook - add custom menu items
virtual void OnMenu(Node& node, App* app) {}
// Edit dialog UI hook - add custom UI elements to block edit dialog
// Called from RenderBlockEditDialog() to allow blocks to append their own settings
virtual void RenderEditUI(Node& node, App* app) {}
// Flow handling
virtual bool ShouldAutoActivateDefaultOutput() const { return true; }
protected:
// Activation state (stored per-block instance)
std::vector<bool> m_OutputActive; // Output activation states
std::vector<bool> m_InputActive; // Input activation states
NodeType m_Type;
ImColor m_Color;
};
// Extended block with parameters - like NHBehavior with params
class ParameterizedBlock : public Block, public ax::NodeRendering::NodeRendererBase
{
public:
ParameterizedBlock(int id, NH_CSTRING name)
: Block(id, name)
{
}
virtual ~ParameterizedBlock() = default;
// Parameter creation helpers (like NHBehavior::CreateInputParameter)
void AddInputParameter(App* app, Node& node, NH_CSTRING name, PinType type);
void AddOutputParameter(App* app, Node& node, NH_CSTRING name, PinType type);
// I/O creation (flow control)
void AddInput(App* app, Node& node, NH_CSTRING name);
void AddOutput(App* app, Node& node, NH_CSTRING name);
int GetInputParameterCount() const override { return (int)m_InputParams.size(); }
int GetOutputParameterCount() const override { return (int)m_OutputParams.size(); }
int GetInputCount() const override { return (int)m_Inputs.size(); }
int GetOutputCount() const override { return (int)m_Outputs.size(); }
// Default Virtools-style rendering
void Render(Node& node, App* app, Pin* newLinkPin) override;
// Context menu hook - default implementation for parameterized blocks
void OnMenu(Node& node, App* app) override;
// State save/load - override to save/load unconnected parameter input values
void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) override;
void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) override;
protected:
// Parameter value helpers (for use in Run() implementations)
// Get input parameter value - reads from connected parameter node or default value
static int GetInputParamValueInt(const Pin& pin, Node& node, App* app, int defaultValue = 0);
static float GetInputParamValueFloat(const Pin& pin, Node& node, App* app, float defaultValue = 0.0f);
static bool GetInputParamValueBool(const Pin& pin, Node& node, App* app, bool defaultValue = false);
static std::string GetInputParamValueString(const Pin& pin, Node& node, App* app, const std::string& defaultValue = "");
// Set output parameter value - stores value and propagates to connected parameter nodes
static void SetOutputParamValueInt(const Pin& pin, Node& node, App* app, int value);
static void SetOutputParamValueFloat(const Pin& pin, Node& node, App* app, float value);
static void SetOutputParamValueBool(const Pin& pin, Node& node, App* app, bool value);
static void SetOutputParamValueString(const Pin& pin, Node& node, App* app, const std::string& value);
std::vector<int> m_InputParams; // Parameter pin IDs (top)
std::vector<int> m_OutputParams; // Parameter pin IDs (bottom)
std::vector<int> m_Inputs; // Flow input IDs (left)
std::vector<int> m_Outputs; // Flow output IDs (right)
};
// Block factory function signature
using BlockFactory = std::function<Block*(int id)>;
// Block registry - like Virtools' behavior manager
class BlockRegistry
{
public:
static BlockRegistry& Instance()
{
static BlockRegistry instance;
return instance;
}
void RegisterBlock(NH_CSTRING typeName, BlockFactory factory)
{
m_Factories[typeName] = factory;
}
Block* CreateBlock(NH_CSTRING typeName, int id)
{
auto it = m_Factories.find(typeName);
if (it != m_Factories.end())
return it->second(id);
return nullptr;
}
std::vector<std::string> GetRegisteredTypes() const
{
std::vector<std::string> types;
for (const auto& pair : m_Factories)
types.push_back(pair.first);
return types;
}
private:
BlockRegistry() = default;
std::map<std::string, BlockFactory> m_Factories;
};
// Helper macro for auto-registration
#define REGISTER_BLOCK(ClassName, TypeName) \
namespace { \
struct ClassName##_Registrar { \
ClassName##_Registrar() { \
BlockRegistry::Instance().RegisterBlock(TypeName, \
[](int id) -> Block* { return new ClassName(id); }); \
} \
}; \
static ClassName##_Registrar g_##ClassName##_Registrar; \
}

View File

@ -0,0 +1,919 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include "block_edit_dialog.h"
#include "../app.h"
#include "../types.h"
#include "../blocks/parameter_node.h"
#include "../blocks/group_block.h"
#include "../blocks/parameter_operation.h"
#include <imgui.h>
#include <imgui_node_editor.h>
namespace ed = ax::NodeEditor;
//------------------------------------------------------------------------------
// Block Edit Dialog
//------------------------------------------------------------------------------
static bool s_BlockEditDialogOpen = false;
static Node* s_EditingNode = nullptr;
static App* s_EditingApp = nullptr;
void OpenBlockEditDialog(Node* node, App* app)
{
s_EditingNode = node;
s_EditingApp = app;
s_BlockEditDialogOpen = true;
// Note: OpenPopup must be called in the same frame as BeginPopupModal
// It will be handled in RenderBlockEditDialog when the context menu closes
}
void RenderBlockEditDialog()
{
if (!s_BlockEditDialogOpen || !s_EditingNode || !s_EditingApp)
return;
// Suspend node editor and disable shortcuts to prevent input conflicts
ed::Suspend();
ed::EnableShortcuts(false);
// Center modal window
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing);
// Open popup if not already open (first frame after OpenBlockEditDialog)
if (!ImGui::IsPopupOpen("Edit Block Parameters", ImGuiPopupFlags_AnyPopupId))
ImGui::OpenPopup("Edit Block Parameters");
// Use proper modal flags to prevent conflicts
bool isOpen = s_BlockEditDialogOpen;
if (!ImGui::BeginPopupModal("Edit Block Parameters", &isOpen,
ImGuiWindowFlags_AlwaysAutoResize))
{
// Popup was closed externally
if (!isOpen)
s_BlockEditDialogOpen = false;
ed::EnableShortcuts(true);
ed::Resume();
return;
}
// Update our state if popup was closed via X button
if (!isOpen)
{
s_BlockEditDialogOpen = false;
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
// Handle ESC to close (only if not typing in an input field)
if (!ImGui::IsAnyItemActive() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
{
s_BlockEditDialogOpen = false;
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
if (!s_EditingNode->IsBlockBased() || !s_EditingNode->BlockInstance)
{
ImGui::Text("Error: Not a block-based node");
if (ImGui::Button("Close"))
{
s_BlockEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
// Block name (editable if NHBEHAVIOR_SCRIPT flag is set)
bool canEditName = false;
if (s_EditingNode->IsBlockBased() && s_EditingNode->BlockInstance)
{
const NH_BEHAVIOR_FLAGS flags = s_EditingNode->BlockInstance->GetFlags();
canEditName = (flags & NHBEHAVIOR_SCRIPT) != 0;
}
if (canEditName)
{
ImGui::Text("Block Name:");
ImGui::SameLine();
ImGui::PushItemWidth(200.0f);
static std::map<int, std::string> nodeNameBuffers; // Per-node name buffers
int nodeId = ToRuntimeId(s_EditingNode->ID);
if (nodeNameBuffers.find(nodeId) == nodeNameBuffers.end())
{
nodeNameBuffers[nodeId] = s_EditingNode->Name;
}
char nameBuffer[128];
strncpy(nameBuffer, nodeNameBuffers[nodeId].c_str(), 127);
nameBuffer[127] = '\0';
if (ImGui::InputText("##block_name", nameBuffer, 128))
{
nodeNameBuffers[nodeId] = nameBuffer;
s_EditingNode->Name = nameBuffer;
}
ImGui::PopItemWidth();
}
else
{
ImGui::Text("Block: %s", s_EditingNode->Name.c_str());
}
ImGui::Separator();
ImGui::Spacing();
// Flow Inputs section (for Group blocks with variable I/O)
bool hasFlowInputs = false;
for (const auto& pin : s_EditingNode->Inputs)
{
if (pin.Type == PinType::Flow)
{
hasFlowInputs = true;
break;
}
}
if (hasFlowInputs && s_EditingNode->BlockType == "Group")
{
ImGui::Text("Flow Inputs:");
ImGui::Spacing();
size_t flowInputIndex = 0;
bool needsRebuild = false;
for (auto& pin : s_EditingNode->Inputs)
{
if (pin.Type != PinType::Flow)
continue;
ImGui::PushID(pin.ID.AsPointer());
// Editable flow input name
ImGui::PushItemWidth(150.0f);
static std::map<int, std::string> flowInputNameBuffers;
const int pinId = ToRuntimeId(pin.ID);
if (flowInputNameBuffers.find(pinId) == flowInputNameBuffers.end())
{
flowInputNameBuffers[pinId] = pin.Name;
}
char nameBuffer[64];
strncpy(nameBuffer, flowInputNameBuffers[pinId].c_str(), 63);
nameBuffer[63] = '\0';
if (ImGui::InputText("##flow_input_name", nameBuffer, 64))
{
flowInputNameBuffers[pinId] = nameBuffer;
pin.Name = nameBuffer;
// Update GroupBlock pin definition
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefName(pinId, nameBuffer);
}
}
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::TextDisabled("(Flow)");
ImGui::SameLine();
// Delete button
if (ImGui::Button("Delete"))
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->RemovePinDef(flowInputIndex, PinKind::Input);
needsRebuild = true;
}
}
ImGui::PopID();
ImGui::Spacing();
flowInputIndex++;
}
// Rebuild if any pin was deleted
if (needsRebuild)
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
ImGui::Separator();
ImGui::Spacing();
}
// Get input parameters (non-flow pins)
bool hasInputParams = false;
for (const auto& pin : s_EditingNode->Inputs)
{
if (pin.Type != PinType::Flow)
{
hasInputParams = true;
break;
}
}
if (!hasInputParams)
{
ImGui::Text("No input parameters");
}
else
{
ImGui::Text("Input Parameters:");
ImGui::Spacing();
size_t paramInputIndex = 0;
bool needsParamRebuild = false;
for (auto& pin : s_EditingNode->Inputs)
{
if (pin.Type == PinType::Flow)
continue;
ImGui::PushID(pin.ID.AsPointer());
// Editable parameter name
ImGui::PushItemWidth(150.0f);
static std::map<int, std::string> nameBuffers; // Per-pin name buffers
const int pinId = ToRuntimeId(pin.ID);
if (nameBuffers.find(pinId) == nameBuffers.end())
{
nameBuffers[pinId] = pin.Name;
}
char nameBuffer[64];
strncpy(nameBuffer, nameBuffers[pinId].c_str(), 63);
nameBuffer[63] = '\0';
if (ImGui::InputText("##param_name", nameBuffer, 64))
{
nameBuffers[pinId] = nameBuffer;
pin.Name = nameBuffer;
// Update GroupBlock pin definition if this is a Group block
if (s_EditingNode->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefName(pinId, nameBuffer);
}
}
}
ImGui::PopItemWidth();
ImGui::SameLine();
// Editable parameter type (combo)
ImGui::PushItemWidth(100.0f);
static std::map<int, int> typeIndices; // Per-pin type index buffers
const char* typeItems[] = { "Bool", "Int", "Float", "String", "Object", "Function", "Delegate" };
PinType typeValues[] = { PinType::Bool, PinType::Int, PinType::Float, PinType::String, PinType::Object, PinType::Function, PinType::Delegate };
const int typeCount = 7;
// Initialize type index if needed
if (typeIndices.find(pinId) == typeIndices.end())
{
// Find current type in the array
for (int i = 0; i < typeCount; i++)
{
if (typeValues[i] == pin.Type)
{
typeIndices[pinId] = i;
break;
}
}
}
int currentTypeIndex = typeIndices[pinId];
if (ImGui::Combo("##param_type", &currentTypeIndex, typeItems, typeCount))
{
typeIndices[pinId] = currentTypeIndex;
PinType newType = typeValues[currentTypeIndex];
// Update pin type
pin.Type = newType;
// Update GroupBlock pin definition if this is a Group block
if (s_EditingNode->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefType(pinId, newType);
// Clear and rebuild node after type change (this will disconnect incompatible links)
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
// Update ParamOp input types
else if (s_EditingNode->BlockType == "ParamOp")
{
auto* paramOpBlock = dynamic_cast<ParameterOperationBlock*>(s_EditingNode->BlockInstance);
if (paramOpBlock)
{
// Determine which input this is (A or B)
if (paramInputIndex == 0)
paramOpBlock->SetInputAType(newType);
else if (paramInputIndex == 1)
paramOpBlock->SetInputBType(newType);
// Clear operation when types change
paramOpBlock->SetOperation("");
// Rebuild node
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
paramOpBlock->Build(*s_EditingNode, s_EditingApp);
}
}
}
ImGui::PopItemWidth();
// Delete button for Group block parameters
if (s_EditingNode->BlockType == "Group")
{
ImGui::SameLine();
if (ImGui::Button("Delete"))
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->RemovePinDef(paramInputIndex, PinKind::Input);
needsParamRebuild = true;
}
}
}
ImGui::SameLine();
// Check if linked to a parameter node
auto* link = s_EditingApp->FindLinkConnectedToPin(pin.ID);
bool isLinked = (link != nullptr && link->EndPinID == pin.ID);
if (isLinked)
{
// Get the source pin (where data flows FROM)
auto* sourcePin = s_EditingApp->FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node && sourcePin->Node->Type == NodeType::Parameter)
{
// Connected to a parameter node
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 1.0f, 0.5f, 1.0f));
ImGui::Text("[External Parameter]");
ImGui::PopStyleColor();
// Show parameter node name and editable value
ImGui::Indent();
ImGui::TextDisabled("Source: %s", sourcePin->Node->Name.c_str());
ImGui::Spacing();
// Editable value field
ImGui::Text("Value:");
ImGui::SameLine();
ImGui::PushItemWidth(150.0f);
Node* paramNode = sourcePin->Node;
bool valueChanged = false;
switch (paramNode->ParameterType)
{
case PinType::Bool:
valueChanged = ImGui::Checkbox("##edit_value", &paramNode->BoolValue);
if (valueChanged && paramNode->ParameterInstance)
paramNode->ParameterInstance->SetBool(paramNode->BoolValue);
break;
case PinType::Int:
valueChanged = ImGui::InputInt("##edit_value", &paramNode->IntValue);
if (valueChanged && paramNode->ParameterInstance)
paramNode->ParameterInstance->SetInt(paramNode->IntValue);
break;
case PinType::Float:
valueChanged = ImGui::InputFloat("##edit_value", &paramNode->FloatValue, 0.01f, 1.0f, "%.3f");
if (valueChanged && paramNode->ParameterInstance)
paramNode->ParameterInstance->SetFloat(paramNode->FloatValue);
break;
case PinType::String:
{
static std::map<int, std::string> stringBuffers; // Per-pin buffers
const int pinId = ToRuntimeId(pin.ID);
if (stringBuffers.find(pinId) == stringBuffers.end())
{
stringBuffers[pinId] = paramNode->StringValue;
}
char strBuffer[256];
strncpy(strBuffer, stringBuffers[pinId].c_str(), 255);
strBuffer[255] = '\0';
if (ImGui::InputText("##edit_value", strBuffer, 256))
{
stringBuffers[pinId] = strBuffer;
paramNode->StringValue = strBuffer;
valueChanged = true;
if (paramNode->ParameterInstance)
paramNode->ParameterInstance->SetString(paramNode->StringValue);
}
}
break;
default:
break;
}
ImGui::PopItemWidth();
ImGui::Unindent();
}
else
{
ImGui::TextDisabled("[Linked]");
}
}
else
{
ImGui::TextDisabled("[Not connected]");
ImGui::Spacing();
// Editable value field for unconnected parameters
ImGui::Indent();
ImGui::Text("Default Value:");
ImGui::SameLine();
ImGui::PushItemWidth(150.0f);
// Get or create default value
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = s_EditingNode->UnconnectedParamValues;
if (paramValues.find(pinId) == paramValues.end())
{
// Initialize with default based on type
switch (pin.Type)
{
case PinType::Bool: paramValues[pinId] = "false"; break;
case PinType::Int: paramValues[pinId] = "0"; break;
case PinType::Float: paramValues[pinId] = "0.0"; break;
case PinType::String: paramValues[pinId] = ""; break;
default: paramValues[pinId] = "0"; break;
}
}
bool valueChanged = false;
std::string& valueStr = paramValues[pinId];
switch (pin.Type)
{
case PinType::Bool:
{
bool boolVal = (valueStr == "true" || valueStr == "1");
if (ImGui::Checkbox("##default_value", &boolVal))
{
valueStr = boolVal ? "true" : "false";
valueChanged = true;
}
}
break;
case PinType::Int:
{
int intVal = 0;
try { intVal = std::stoi(valueStr); } catch (...) {}
if (ImGui::InputInt("##default_value", &intVal))
{
valueStr = std::to_string(intVal);
valueChanged = true;
}
}
break;
case PinType::Float:
{
float floatVal = 0.0f;
try { floatVal = std::stof(valueStr); } catch (...) {}
if (ImGui::InputFloat("##default_value", &floatVal, 0.01f, 1.0f, "%.3f"))
{
char buf[32];
snprintf(buf, sizeof(buf), "%.6g", floatVal);
valueStr = buf;
valueChanged = true;
}
}
break;
case PinType::String:
{
static std::map<int, std::string> stringBuffers;
if (stringBuffers.find(pinId) == stringBuffers.end())
{
stringBuffers[pinId] = valueStr;
}
char strBuffer[256];
strncpy(strBuffer, stringBuffers[pinId].c_str(), 255);
strBuffer[255] = '\0';
if (ImGui::InputText("##default_value", strBuffer, 256))
{
stringBuffers[pinId] = strBuffer;
valueStr = strBuffer;
valueChanged = true;
}
}
break;
default:
break;
}
ImGui::PopItemWidth();
ImGui::Unindent();
}
ImGui::PopID();
ImGui::Spacing();
paramInputIndex++;
}
// Rebuild if any parameter was deleted
if (needsParamRebuild)
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
}
ImGui::Separator();
ImGui::Spacing();
// Flow Outputs section (for Group blocks with variable I/O)
bool hasFlowOutputs = false;
for (const auto& pin : s_EditingNode->Outputs)
{
if (pin.Type == PinType::Flow)
{
hasFlowOutputs = true;
break;
}
}
if (hasFlowOutputs && s_EditingNode->BlockType == "Group")
{
ImGui::Text("Flow Outputs:");
ImGui::Spacing();
size_t flowOutputIndex = 0;
bool needsFlowOutputRebuild = false;
for (auto& pin : s_EditingNode->Outputs)
{
if (pin.Type != PinType::Flow)
continue;
ImGui::PushID(pin.ID.AsPointer());
// Editable flow output name
ImGui::PushItemWidth(150.0f);
static std::map<int, std::string> flowOutputNameBuffers;
const int pinId = ToRuntimeId(pin.ID);
if (flowOutputNameBuffers.find(pinId) == flowOutputNameBuffers.end())
{
flowOutputNameBuffers[pinId] = pin.Name;
}
char nameBuffer[64];
strncpy(nameBuffer, flowOutputNameBuffers[pinId].c_str(), 63);
nameBuffer[63] = '\0';
if (ImGui::InputText("##flow_output_name", nameBuffer, 64))
{
flowOutputNameBuffers[pinId] = nameBuffer;
pin.Name = nameBuffer;
// Update GroupBlock pin definition
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefName(pinId, nameBuffer);
}
}
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::TextDisabled("(Flow)");
ImGui::SameLine();
// Delete button
if (ImGui::Button("Delete"))
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->RemovePinDef(flowOutputIndex, PinKind::Output);
needsFlowOutputRebuild = true;
}
}
ImGui::PopID();
ImGui::Spacing();
flowOutputIndex++;
}
// Rebuild if any pin was deleted
if (needsFlowOutputRebuild)
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
ImGui::Separator();
ImGui::Spacing();
}
// Output Parameters section
bool hasOutputParams = false;
for (const auto& pin : s_EditingNode->Outputs)
{
if (pin.Type != PinType::Flow)
{
hasOutputParams = true;
break;
}
}
if (!hasOutputParams)
{
ImGui::Text("No output parameters");
}
else
{
ImGui::Text("Output Parameters:");
ImGui::Spacing();
size_t paramOutputIndex = 0;
bool needsParamOutputRebuild = false;
for (auto& pin : s_EditingNode->Outputs)
{
if (pin.Type == PinType::Flow)
continue;
ImGui::PushID(pin.ID.AsPointer());
// Editable parameter name
ImGui::PushItemWidth(150.0f);
static std::map<int, std::string> outputNameBuffers; // Per-pin name buffers for outputs
const int pinId = ToRuntimeId(pin.ID);
if (outputNameBuffers.find(pinId) == outputNameBuffers.end())
{
outputNameBuffers[pinId] = pin.Name;
}
char nameBuffer[64];
strncpy(nameBuffer, outputNameBuffers[pinId].c_str(), 63);
nameBuffer[63] = '\0';
if (ImGui::InputText("##output_param_name", nameBuffer, 64))
{
outputNameBuffers[pinId] = nameBuffer;
pin.Name = nameBuffer;
// Update GroupBlock pin definition if this is a Group block
if (s_EditingNode->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefName(pinId, nameBuffer);
}
}
}
ImGui::PopItemWidth();
ImGui::SameLine();
// Editable parameter type (combo) for outputs
ImGui::PushItemWidth(100.0f);
static std::map<int, int> outputTypeIndices; // Per-pin type index buffers for outputs
const int outputPinId = ToRuntimeId(pin.ID);
const char* outputTypeItems[] = { "Bool", "Int", "Float", "String", "Object", "Function", "Delegate" };
PinType outputTypeValues[] = { PinType::Bool, PinType::Int, PinType::Float, PinType::String, PinType::Object, PinType::Function, PinType::Delegate };
const int outputTypeCount = 7;
// Initialize type index if needed
if (outputTypeIndices.find(outputPinId) == outputTypeIndices.end())
{
// Find current type in the array
for (int i = 0; i < outputTypeCount; i++)
{
if (outputTypeValues[i] == pin.Type)
{
outputTypeIndices[outputPinId] = i;
break;
}
}
}
int currentOutputTypeIndex = outputTypeIndices[outputPinId];
if (ImGui::Combo("##output_param_type", &currentOutputTypeIndex, outputTypeItems, outputTypeCount))
{
outputTypeIndices[outputPinId] = currentOutputTypeIndex;
PinType newType = outputTypeValues[currentOutputTypeIndex];
// Update pin type
pin.Type = newType;
// Update GroupBlock pin definition if this is a Group block
if (s_EditingNode->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->UpdatePinDefType(outputPinId, newType);
// Clear and rebuild node after type change (this will disconnect incompatible links)
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
// Update ParamOp output type
else if (s_EditingNode->BlockType == "ParamOp")
{
auto* paramOpBlock = dynamic_cast<ParameterOperationBlock*>(s_EditingNode->BlockInstance);
if (paramOpBlock)
{
// Update output type (ParamOp only has 1 output parameter)
paramOpBlock->SetOutputType(newType);
// Clear operation when types change
paramOpBlock->SetOperation("");
// Rebuild node
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
paramOpBlock->Build(*s_EditingNode, s_EditingApp);
}
}
}
ImGui::PopItemWidth();
// Delete button for Group block output parameters
if (s_EditingNode->BlockType == "Group")
{
ImGui::SameLine();
if (ImGui::Button("Delete"))
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
groupBlock->RemovePinDef(paramOutputIndex, PinKind::Output);
needsParamOutputRebuild = true;
}
}
}
ImGui::PopID();
ImGui::Spacing();
paramOutputIndex++;
}
// Rebuild if any output parameter was deleted
if (needsParamOutputRebuild)
{
auto* groupBlock = dynamic_cast<GroupBlock*>(s_EditingNode->BlockInstance);
if (groupBlock)
{
s_EditingNode->Inputs.clear();
s_EditingNode->Outputs.clear();
groupBlock->Build(*s_EditingNode, s_EditingApp);
}
}
}
ImGui::Separator();
ImGui::Spacing();
// Parameter Operation specific settings
if (s_EditingNode->BlockType == "ParamOp")
{
auto* paramOpBlock = dynamic_cast<ParameterOperationBlock*>(s_EditingNode->BlockInstance);
if (paramOpBlock)
{
ImGui::Text("Operation Selection:");
ImGui::Spacing();
// Get matching operations based on current types
auto matchingOps = ParameterOperationRegistry::Instance().GetMatchingOperations(
paramOpBlock->GetInputAType(),
paramOpBlock->GetInputBType(),
paramOpBlock->GetOutputType());
if (!matchingOps.empty())
{
// Create list of operation labels
std::vector<const char*> opLabels;
std::vector<std::string> opUUIDs;
int currentOpIndex = -1;
for (size_t i = 0; i < matchingOps.size(); ++i)
{
opLabels.push_back(matchingOps[i].Label.c_str());
opUUIDs.push_back(matchingOps[i].UUID);
if (matchingOps[i].UUID == paramOpBlock->GetOperationUUID())
currentOpIndex = (int)i;
}
ImGui::PushItemWidth(200.0f);
if (ImGui::Combo("##operation_select", &currentOpIndex, opLabels.data(), (int)opLabels.size()))
{
if (currentOpIndex >= 0 && currentOpIndex < (int)opUUIDs.size())
{
paramOpBlock->SetOperation(opUUIDs[currentOpIndex]);
// Update node name to reflect operation
s_EditingNode->Name = matchingOps[currentOpIndex].Label;
}
}
ImGui::PopItemWidth();
// Show current operation
if (currentOpIndex >= 0 && currentOpIndex < (int)matchingOps.size())
{
ImGui::SameLine();
ImGui::TextDisabled("(Selected)");
}
}
else
{
ImGui::TextDisabled("No operations available for current types");
ImGui::TextDisabled("(Change input/output types to see operations)");
}
ImGui::Separator();
ImGui::Spacing();
}
}
// Block-specific custom UI (virtual function callback)
if (s_EditingNode->IsBlockBased() && s_EditingNode->BlockInstance)
{
s_EditingNode->BlockInstance->RenderEditUI(*s_EditingNode, s_EditingApp);
}
// Buttons
bool confirmPressed = false;
if (ImGui::Button("Confirm (Enter)", ImVec2(120, 0)))
{
confirmPressed = true;
}
ImGui::SameLine();
if (ImGui::Button("Cancel (Esc)", ImVec2(120, 0)))
{
s_BlockEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
// Handle Enter key to confirm (only if not typing in an input field)
if (!ImGui::IsAnyItemActive() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)))
{
confirmPressed = true;
}
if (confirmPressed)
{
// Save graph
auto* activeContainer = s_EditingApp->GetActiveRootContainer();
if (activeContainer)
{
s_EditingApp->SaveGraph(s_EditingApp->m_GraphFilename, activeContainer);
}
s_BlockEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
// Resume node editor and re-enable shortcuts after dialog is done
ed::EnableShortcuts(true);
ed::Resume();
}
bool IsBlockEditDialogOpen()
{
return s_BlockEditDialogOpen;
}

View File

@ -0,0 +1,10 @@
#pragma once
// Forward declarations
struct Node;
class App;
void OpenBlockEditDialog(Node* node, App* app);
void RenderBlockEditDialog();
bool IsBlockEditDialogOpen();

View File

@ -0,0 +1,110 @@
// constants.h - Centralized style and layout constants for node rendering
#pragma once
#include <imgui.h>
namespace NodeConstants {
// ===== Pin Sizes =====
constexpr float FLOW_PIN_SIZE = 8.0f; // Size for flow pins (small squares)
constexpr float PARAMETER_PIN_WIDTH = 8.0f; // Width for parameter pins (small rectangles)
constexpr float PARAMETER_PIN_HEIGHT = 4.0f; // Height for parameter pins (half of squares)
// ===== Pin Edge Offsets =====
// How far pins are from the node edge
// 0.0f = exactly on edge (half inside, half outside)
// Negative = inside the node
// Positive = outside the node
constexpr float PARAMETER_PIN_EDGE_OFFSET = 0.0f; // Parameter pins on edge
constexpr float FLOW_PIN_EDGE_OFFSET = 0.0f; // Flow pins on edge
// ===== Node Style - Regular Blocks =====
namespace Blocks {
constexpr float ROUNDING = 1.0f;
constexpr float BORDER_WIDTH = 1.0f;
constexpr float BORDER_WIDTH_RUNNING = 3.0f; // Thicker when running
constexpr float PADDING = 0.0f;
// Padding as ImVec4 (left, top, right, bottom)
const ImVec4 PADDING_VEC4(8.0f, 8.0f, 8.0f, 8.0f);
// Colors
const ImColor BG_COLOR(50, 50, 60, 240);
const ImColor BORDER_COLOR(100, 100, 110, 255);
const ImColor BORDER_COLOR_RUNNING(255, 0, 0, 255); // Red when running
}
// ===== Node Style - Group Blocks =====
namespace Groups {
constexpr float ROUNDING = 1.0f;
constexpr float BORDER_WIDTH = 1.0f;
constexpr float BORDER_WIDTH_RUNNING = 3.0f;
const ImVec4 PADDING_VEC4(8.0f, 8.0f, 8.0f, 8.0f);
const ImColor BG_COLOR(50, 50, 60, 240);
const ImColor BORDER_COLOR(100, 100, 110, 255);
const ImColor BORDER_COLOR_RUNNING(255, 0, 0, 255);
// Resize grip
constexpr float RESIZE_GRIP_SIZE = 16.0f;
constexpr float RESIZE_GRIP_LINE_SPACING = 3.5f;
const ImColor RESIZE_GRIP_COLOR(200, 200, 200, 220);
}
// ===== Node Style - Parameter Nodes =====
namespace Params {
constexpr float ROUNDING = 4.0f; // More rounded for visual distinction
constexpr float BORDER_WIDTH = 1.0f;
constexpr float BORDER_WIDTH_SOURCE = 2.0f; // Thicker for source nodes
constexpr float BORDER_WIDTH_NAME_AND_VALUE = 1.5f;
constexpr float BORDER_WIDTH_SOURCE_NAME_AND_VALUE = 2.5f;
const ImVec4 PADDING_NAME_ONLY(4.0f, 2.0f, 4.0f, 2.0f);
const ImVec4 PADDING_NAME_AND_VALUE(8.0f, 4.0f, 8.0f, 4.0f);
const ImVec4 PADDING_SMALL_BOX(2.0f, 2.0f, 2.0f, 2.0f);
const ImVec4 PADDING_MINIMAL(4.0f, 4.0f, 4.0f, 4.0f);
// Colors (grayer background for distinction from blocks)
const ImColor BG_COLOR(70, 70, 80, 255); // More gray
const ImColor BORDER_COLOR(255, 255, 255, 200);
const ImColor BORDER_COLOR_SOURCE(255, 215, 0, 255); // Gold for source
const ImColor BORDER_COLOR_SHORTCUT(200, 200, 200, 180); // Dimmed for shortcuts
// Pin icon sizes for different modes
constexpr float PIN_ICON_SIZE_NORMAL = 24.0f;
constexpr float PIN_ICON_SIZE_MINIMAL = 20.0f;
// Widget widths
constexpr float INPUT_WIDTH_NAME_ONLY = 80.0f;
constexpr float INPUT_WIDTH_NAME_AND_VALUE = 100.0f;
constexpr float INPUT_WIDTH_SMALL_BOX = 50.0f;
// Dummy sizes for minimal modes
const ImVec2 MINIMAL_SIZE(27.0f, 20.0f);
const ImVec2 MINIMAL_LINKS_SIZE(40.0f, 20.0f);
}
// ===== Pin Renderer Colors =====
namespace PinColors {
// Flow pins
const ImColor FLOW_FILL(68, 201, 156, 255);
const ImColor FLOW_BORDER(68, 201, 156, 200);
const ImColor FLOW_FILL_DEACTIVATED(68, 201, 156, 200);
const ImColor FLOW_BORDER_DEACTIVATED(68, 201, 156, 150);
constexpr float FLOW_ROUNDING = 3.0f;
constexpr float FLOW_BORDER_WIDTH = 2.0f;
}
// ===== Layout Constants =====
namespace Layout {
constexpr float MIN_NODE_WIDTH = 80.0f; // Minimum node width
constexpr float MIN_NODE_HEIGHT = 40.0f; // Minimum node height
constexpr float MIN_GROUP_SIZE = 100.0f; // Minimum group size
// Pin spacing (when multiple pins on same edge)
// Uses fractional offset: 1.0 / (count + 1) spacing
}
} // namespace NodeConstants

View File

@ -0,0 +1,876 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include <crude_json.h>
#include "group_block.h"
#include "../app.h"
#include "block.h"
#include "../utilities/node_renderer_base.h"
#include "NodeEx.h"
#include "constants.h"
#include "../Logging.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_node_editor.h>
#include <algorithm>
#include <map>
namespace ed = ax::NodeEditor;
using namespace ax::NodeRendering;
using namespace NodeConstants;
void GroupBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Clear existing pin collections before rebuilding
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
// Rebuild pins from stored definitions, reusing existing pin IDs
for (auto& pinDef : m_PinDefinitions) // non-const to update PinId if needed
{
int pinId = pinDef.PinId;
// Generate new ID if this pin definition doesn't have one yet
if (pinId < 0)
{
pinId = app->GetNextId();
pinDef.PinId = pinId;
}
if (pinDef.Type == PinType::Flow)
{
// Flow pin - manually add to collections (don't call AddInput/AddOutput which generate IDs)
if (pinDef.Kind == PinKind::Input)
{
m_Inputs.push_back(pinId);
node.Inputs.emplace_back(pinId, pinDef.Name.c_str(), PinType::Flow);
}
else
{
m_Outputs.push_back(pinId);
node.Outputs.emplace_back(pinId, pinDef.Name.c_str(), PinType::Flow);
}
}
else
{
// Parameter pin - manually add to collections
if (pinDef.Kind == PinKind::Input)
{
m_InputParams.push_back(pinId);
node.Inputs.emplace_back(pinId, pinDef.Name.c_str(), pinDef.Type);
}
else
{
m_OutputParams.push_back(pinId);
node.Outputs.emplace_back(pinId, pinDef.Name.c_str(), pinDef.Type);
}
}
}
}
int GroupBlock::Run(Node& node, App* app)
{
// Groups have no flow I/O by default, so nothing to execute
return E_OK;
}
void GroupBlock::Render(Node& node, App* app, Pin* newLinkPin)
{
// Switch rendering based on display mode
switch (m_DisplayMode)
{
case GroupDisplayMode::Collapsed:
RenderCollapsed(node, app, newLinkPin);
break;
case GroupDisplayMode::Expanded:
default:
RenderExpanded(node, app, newLinkPin);
break;
}
}
void GroupBlock::RenderCollapsed(Node& node, App* app, Pin* newLinkPin)
{
// Use base class rendering like any other block
ParameterizedBlock::Render(node, app, newLinkPin);
}
void GroupBlock::RenderExpanded(Node& node, App* app, Pin* newLinkPin)
{
// Check if node is currently running (for red border visualization)
float currentTime = ImGui::GetTime();
bool isRunning = false;
auto runningIt = app->m_RunningNodes.find(node.ID);
if (runningIt != app->m_RunningNodes.end())
{
isRunning = (currentTime < runningIt->second);
}
// Get styles from StyleManager
auto& styleManager = app->GetStyleManager();
auto& groupStyle = styleManager.GroupStyle;
ImColor borderColor = isRunning ? groupStyle.BorderColorRunning : groupStyle.BorderColor;
float activeBorderWidth = isRunning ? groupStyle.BorderWidthRunning : groupStyle.BorderWidth;
// Use NodeStyleScope for group appearance
NodeStyleScope style(
groupStyle.BgColor,
borderColor,
groupStyle.Rounding, activeBorderWidth,
groupStyle.Padding,
ImVec2(0.0f, 1.0f),
ImVec2(0.0f, -1.0f)
);
ed::BeginNode(node.ID);
ImGui::PushID(node.ID.AsPointer());
ImGui::BeginVertical("collapsed_group");
// MIDDLE: Header
ImGui::BeginHorizontal("content");
ImGui::Spring(1);
ImGui::BeginVertical("header");
ImGui::Spring(1);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
ImGui::TextUnformatted(node.Name.c_str());
ImGui::PopStyleColor();
ImGui::Spring(1);
ImGui::EndVertical();
ImGui::Spring(1);
ImGui::EndHorizontal();
// Resizable spacer area
ImVec2 minSize(styleManager.MinGroupSize, styleManager.MinNodeHeight);
ImVec2 targetSize = m_CollapsedSize;
ImGui::Dummy(targetSize);
ImGui::EndVertical();
// Save cursor and get node bounds
ImVec2 contentEndPos = ImGui::GetCursorScreenPos();
ImVec2 nodePos = ed::GetNodePosition(node.ID);
ImVec2 nodeSize = ed::GetNodeSize(node.ID);
if (nodeSize.x <= 0 || nodeSize.y <= 0)
{
ImVec2 contentMin = ImGui::GetItemRectMin();
ImVec2 contentMax = ImGui::GetItemRectMax();
nodeSize = contentMax - contentMin;
}
ImRect nodeRect = ImRect(nodePos, nodePos + nodeSize);
// Collect pins by type
std::vector<Pin*> inputParams;
std::vector<Pin*> outputParams;
std::vector<Pin*> flowInputs;
std::vector<Pin*> flowOutputs;
for (auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
flowInputs.push_back(&pin);
else
inputParams.push_back(&pin);
}
for (auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
flowOutputs.push_back(&pin);
else
outputParams.push_back(&pin);
}
// Render input parameters at top edge using NodeEx
if (!inputParams.empty())
{
float spacing = 1.0f / (inputParams.size() + 1);
for (size_t i = 0; i < inputParams.size(); ++i)
{
Pin* pin = inputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Input, ed::PinEdge::Top,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state);
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Min.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render output parameters at bottom edge using NodeEx
if (!outputParams.empty())
{
float spacing = 1.0f / (outputParams.size() + 1);
for (size_t i = 0; i < outputParams.size(); ++i)
{
Pin* pin = outputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Output, ed::PinEdge::Bottom,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state);
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Max.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render flow inputs at left edge using NodeEx
if (!flowInputs.empty())
{
float spacing = 1.0f / (flowInputs.size() + 1);
for (size_t i = 0; i < flowInputs.size(); ++i)
{
Pin* pin = flowInputs[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Input, ed::PinEdge::Left,
offset, styleManager.FlowPinEdgeOffset, nodeRect, state, nullptr, ed::RenderPinBox);
pin->LastPivotPosition = ImVec2(pinRect.Min.x, pinRect.GetCenter().y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render flow outputs at right edge using NodeEx
if (!flowOutputs.empty())
{
float spacing = 1.0f / (flowOutputs.size() + 1);
for (size_t i = 0; i < flowOutputs.size(); ++i)
{
Pin* pin = flowOutputs[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Output, ed::PinEdge::Right,
offset, styleManager.FlowPinEdgeOffset, nodeRect, state, nullptr, ed::RenderPinBox);
pin->LastPivotPosition = ImVec2(pinRect.Max.x, pinRect.GetCenter().y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Restore cursor
ImGui::SetCursorScreenPos(contentEndPos);
ImGui::PopID();
ed::EndNode();
// Draw resize grip AFTER EndNode using actual node bounds
// Need to suspend editor to work in screen space
ed::Suspend();
// Reuse nodePos and nodeSize from above (already calculated for pin placement)
if (nodeSize.x > 0 && nodeSize.y > 0)
{
// Convert node bounds to screen space
ImVec2 nodeScreenMin = ed::CanvasToScreen(nodePos);
ImVec2 nodeScreenMax = ed::CanvasToScreen(nodePos + nodeSize);
// Position resize grip in bottom-right corner
ImVec2 resizeGripSize(styleManager.GroupResizeGripSize, styleManager.GroupResizeGripSize);
ImVec2 gripMin = nodeScreenMax - resizeGripSize;
ImRect gripRect(gripMin, nodeScreenMax);
// Draw resize grip visual (diagonal lines) - in screen space
auto drawList = ImGui::GetWindowDrawList();
ImU32 gripColor = styleManager.GroupResizeGripColor;
for (int i = 0; i < 3; ++i)
{
float offset = i * styleManager.GroupResizeGripLineSpacing;
ImVec2 p1 = ImVec2(gripRect.Min.x + offset, gripRect.Max.y);
ImVec2 p2 = ImVec2(gripRect.Max.x, gripRect.Min.y + offset);
drawList->AddLine(p1, p2, gripColor, 2.0f);
}
// Handle resize interaction - use ImGui button in screen space
ImGui::PushID("##resize_grip");
ImGui::SetCursorScreenPos(gripRect.Min);
ImGui::InvisibleButton("##resize", gripRect.GetSize(), ImGuiButtonFlags_None);
if (ImGui::IsItemHovered())
{
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
}
// Track resize state per node using static map keyed by node ID
static std::map<int, bool> resizeActive;
static std::map<int, ImVec2> resizeStartSize;
static std::map<int, ImVec2> resizeStartMouseCanvas;
const int nodeId = ToRuntimeId(node.ID);
if (ImGui::IsItemActive())
{
if (ImGui::IsMouseDragging(0))
{
ImVec2 currentMouseScreen = ImGui::GetMousePos();
ImVec2 currentMouseCanvas = ed::ScreenToCanvas(currentMouseScreen);
if (resizeActive.find(nodeId) == resizeActive.end() || !resizeActive[nodeId])
{
// Start of resize - store initial state
resizeActive[nodeId] = true;
resizeStartSize[nodeId] = m_CollapsedSize;
resizeStartMouseCanvas[nodeId] = currentMouseCanvas;
}
// Calculate delta from start position
ImVec2 canvasDelta = currentMouseCanvas - resizeStartMouseCanvas[nodeId];
// Apply delta to initial size
ImVec2 newSize = resizeStartSize[nodeId] + canvasDelta;
// Clamp to minimum size (in canvas coordinates)
newSize.x = ImMax(newSize.x, minSize.x);
newSize.y = ImMax(newSize.y, minSize.y);
// Update size
m_CollapsedSize = newSize;
}
}
else
{
// Not active - clear resize state for this node
resizeActive.erase(nodeId);
resizeStartSize.erase(nodeId);
resizeStartMouseCanvas.erase(nodeId);
}
ImGui::PopID();
}
ed::Resume();
}
void GroupBlock::ToggleDisplayMode()
{
if (m_DisplayMode == GroupDisplayMode::Collapsed)
m_DisplayMode = GroupDisplayMode::Expanded;
else
m_DisplayMode = GroupDisplayMode::Collapsed;
}
void GroupBlock::RebuildPins(Node& node, App* app)
{
// CRITICAL: Preserve UUIDs before clearing (pins are about to be destroyed!)
std::map<int, Uuid64> oldPinUuids; // runtime pin ID -> UUID
for (const auto& input : node.Inputs)
{
if (input.UUID.IsValid())
oldPinUuids[ToRuntimeId(input.ID)] = input.UUID;
}
for (const auto& output : node.Outputs)
{
if (output.UUID.IsValid())
oldPinUuids[ToRuntimeId(output.ID)] = output.UUID;
}
LOG_DEBUG("[GroupBlock::RebuildPins] Preserved {} pin UUIDs before rebuild", oldPinUuids.size());
// Helper method to rebuild pins and update pointers
node.Inputs.clear();
node.Outputs.clear();
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
Build(node, app);
// Restore or generate UUIDs for pins
for (auto& input : node.Inputs)
{
const int pinId = ToRuntimeId(input.ID);
// Try to restore old UUID if this pin ID existed before
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
input.UUID = it->second; // Restore old UUID
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
LOG_DEBUG("[GroupBlock::RebuildPins] Restored UUID for input pin {}: 0x{:08X}{:08X}",
pinId, input.UUID.high, input.UUID.low);
}
else
{
// New pin - generate fresh UUID
input.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
LOG_DEBUG("[GroupBlock::RebuildPins] Generated UUID for NEW input pin {}: 0x{:08X}{:08X}",
pinId, input.UUID.high, input.UUID.low);
}
input.Node = &node;
input.Kind = PinKind::Input;
}
for (auto& output : node.Outputs)
{
const int pinId = ToRuntimeId(output.ID);
// Try to restore old UUID if this pin ID existed before
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
output.UUID = it->second; // Restore old UUID
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
LOG_DEBUG("[GroupBlock::RebuildPins] Restored UUID for output pin {}: 0x{:08X}{:08X}",
pinId, output.UUID.high, output.UUID.low);
}
else
{
// New pin - generate fresh UUID
output.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
LOG_DEBUG("[GroupBlock::RebuildPins] Generated UUID for NEW output pin {}: 0x{:08X}{:08X}",
pinId, output.UUID.high, output.UUID.low);
}
output.Node = &node;
output.Kind = PinKind::Output;
}
}
void GroupBlock::AddPinDef(const std::string& name, PinType type, PinKind kind)
{
// Note: Pin ID is generated lazily in Build() method
// This allows for proper ID management when loading from file
m_PinDefinitions.push_back(GroupPinDef(name, type, kind, -1));
}
void GroupBlock::AddFlowInput(const std::string& name)
{
AddPinDef(name, PinType::Flow, PinKind::Input);
}
void GroupBlock::AddFlowOutput(const std::string& name)
{
AddPinDef(name, PinType::Flow, PinKind::Output);
}
void GroupBlock::RemovePinDef(size_t index, PinKind kind)
{
// Find and remove the pin definition by counting pins of the same kind
size_t foundIndex = 0;
for (auto it = m_PinDefinitions.begin(); it != m_PinDefinitions.end(); ++it)
{
if (it->Kind == kind)
{
if (foundIndex == index)
{
m_PinDefinitions.erase(it);
break;
}
foundIndex++;
}
}
}
void GroupBlock::UpdatePinDefName(int pinId, const std::string& newName)
{
// Match pin ID to pin definition by iterating through definitions
// and counting how many pins of each type have been created
// This matches the Build() order
size_t flowInputIndex = 0, flowOutputIndex = 0;
size_t paramInputIndex = 0, paramOutputIndex = 0;
for (auto& pinDef : m_PinDefinitions)
{
if (pinDef.Type == PinType::Flow)
{
if (pinDef.Kind == PinKind::Input)
{
// Check if this flow input matches the pin ID
if (flowInputIndex < m_Inputs.size() && m_Inputs[flowInputIndex] == pinId)
{
pinDef.Name = newName;
return;
}
flowInputIndex++;
}
else
{
// Check if this flow output matches the pin ID
if (flowOutputIndex < m_Outputs.size() && m_Outputs[flowOutputIndex] == pinId)
{
pinDef.Name = newName;
return;
}
flowOutputIndex++;
}
}
else
{
if (pinDef.Kind == PinKind::Input)
{
// Check if this param input matches the pin ID
if (paramInputIndex < m_InputParams.size() && m_InputParams[paramInputIndex] == pinId)
{
pinDef.Name = newName;
return;
}
paramInputIndex++;
}
else
{
// Check if this param output matches the pin ID
if (paramOutputIndex < m_OutputParams.size() && m_OutputParams[paramOutputIndex] == pinId)
{
pinDef.Name = newName;
return;
}
paramOutputIndex++;
}
}
}
}
void GroupBlock::UpdatePinDefType(int pinId, PinType newType)
{
// Match pin ID to pin definition (same logic as UpdatePinDefName)
size_t flowInputIndex = 0, flowOutputIndex = 0;
size_t paramInputIndex = 0, paramOutputIndex = 0;
for (auto& pinDef : m_PinDefinitions)
{
if (pinDef.Type == PinType::Flow)
{
if (pinDef.Kind == PinKind::Input)
{
if (flowInputIndex < m_Inputs.size() && m_Inputs[flowInputIndex] == pinId)
{
pinDef.Type = newType;
return;
}
flowInputIndex++;
}
else
{
if (flowOutputIndex < m_Outputs.size() && m_Outputs[flowOutputIndex] == pinId)
{
pinDef.Type = newType;
return;
}
flowOutputIndex++;
}
}
else
{
if (pinDef.Kind == PinKind::Input)
{
if (paramInputIndex < m_InputParams.size() && m_InputParams[paramInputIndex] == pinId)
{
pinDef.Type = newType;
return;
}
paramInputIndex++;
}
else
{
if (paramOutputIndex < m_OutputParams.size() && m_OutputParams[paramOutputIndex] == pinId)
{
pinDef.Type = newType;
return;
}
paramOutputIndex++;
}
}
}
}
void GroupBlock::OnMenu(Node& node, App* app)
{
// Call base class menu first
ParameterizedBlock::OnMenu(node, app);
ImGui::Separator();
ImGui::TextUnformatted("Group");
// Display mode toggle
const char* modeStr = (m_DisplayMode == GroupDisplayMode::Collapsed) ? "Collapsed" : "Expanded";
bool isCollapsed = (m_DisplayMode == GroupDisplayMode::Collapsed);
if (ImGui::MenuItem("Collapsed Mode", nullptr, isCollapsed))
{
m_DisplayMode = GroupDisplayMode::Collapsed;
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node.ID);
}
if (ImGui::MenuItem("Expanded Mode", nullptr, !isCollapsed))
{
m_DisplayMode = GroupDisplayMode::Expanded;
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node.ID);
}
ImGui::Separator();
ImGui::TextDisabled("Current: %s", modeStr);
ImGui::Separator();
ImGui::TextUnformatted("Group I/O");
// Add Flow Input
if (ImGui::MenuItem("Add Flow Input"))
{
AddPinDef("Execute", PinType::Flow, PinKind::Input);
RebuildPins(node, app);
}
// Add Flow Output
if (ImGui::MenuItem("Add Flow Output"))
{
AddPinDef("Done", PinType::Flow, PinKind::Output);
RebuildPins(node, app);
}
ImGui::Separator();
// Add Parameter Input submenu
if (ImGui::BeginMenu("Add Parameter Input"))
{
if (ImGui::MenuItem("Bool")) {
AddPinDef("Bool", PinType::Bool, PinKind::Input);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Int")) {
AddPinDef("Int", PinType::Int, PinKind::Input);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Float")) {
AddPinDef("Float", PinType::Float, PinKind::Input);
RebuildPins(node, app);
}
if (ImGui::MenuItem("String")) {
AddPinDef("String", PinType::String, PinKind::Input);
RebuildPins(node, app);
}
ImGui::EndMenu();
}
// Add Parameter Output submenu
if (ImGui::BeginMenu("Add Parameter Output"))
{
if (ImGui::MenuItem("Bool")) {
AddPinDef("Bool", PinType::Bool, PinKind::Output);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Int")) {
AddPinDef("Int", PinType::Int, PinKind::Output);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Float")) {
AddPinDef("Float", PinType::Float, PinKind::Output);
RebuildPins(node, app);
}
if (ImGui::MenuItem("String")) {
AddPinDef("String", PinType::String, PinKind::Output);
RebuildPins(node, app);
}
ImGui::EndMenu();
}
ImGui::Separator();
// Remove pins submenus
size_t inputFlowCount = 0, inputParamCount = 0;
size_t outputFlowCount = 0, outputParamCount = 0;
for (const auto& pinDef : m_PinDefinitions)
{
if (pinDef.Kind == PinKind::Input)
{
if (pinDef.Type == PinType::Flow) inputFlowCount++;
else inputParamCount++;
}
else
{
if (pinDef.Type == PinType::Flow) outputFlowCount++;
else outputParamCount++;
}
}
if (inputFlowCount > 0 || inputParamCount > 0)
{
if (ImGui::BeginMenu("Remove Input"))
{
size_t inputIndex = 0;
for (size_t i = 0; i < m_PinDefinitions.size(); ++i)
{
const auto& pinDef = m_PinDefinitions[i];
if (pinDef.Kind != PinKind::Input) continue;
std::string label = pinDef.Name + " (" +
(pinDef.Type == PinType::Flow ? "Flow" :
pinDef.Type == PinType::Bool ? "Bool" :
pinDef.Type == PinType::Int ? "Int" :
pinDef.Type == PinType::Float ? "Float" :
pinDef.Type == PinType::String ? "String" : "Unknown") + ")";
// Use a lambda to capture the current inputIndex
size_t currentIndex = inputIndex;
if (ImGui::MenuItem(label.c_str()))
{
RemovePinDef(currentIndex, PinKind::Input);
RebuildPins(node, app);
break;
}
inputIndex++;
}
ImGui::EndMenu();
}
}
if (outputFlowCount > 0 || outputParamCount > 0)
{
if (ImGui::BeginMenu("Remove Output"))
{
size_t outputIndex = 0;
for (size_t i = 0; i < m_PinDefinitions.size(); ++i)
{
const auto& pinDef = m_PinDefinitions[i];
if (pinDef.Kind != PinKind::Output) continue;
std::string label = pinDef.Name + " (" +
(pinDef.Type == PinType::Flow ? "Flow" :
pinDef.Type == PinType::Bool ? "Bool" :
pinDef.Type == PinType::Int ? "Int" :
pinDef.Type == PinType::Float ? "Float" :
pinDef.Type == PinType::String ? "String" : "Unknown") + ")";
// Use a lambda to capture the current outputIndex
size_t currentIndex = outputIndex;
if (ImGui::MenuItem(label.c_str()))
{
RemovePinDef(currentIndex, PinKind::Output);
RebuildPins(node, app);
break;
}
outputIndex++;
}
ImGui::EndMenu();
}
}
}
void GroupBlock::SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app)
{
// Call base class to save parameter values
ParameterizedBlock::SaveState(node, nodeData, container, app);
// Save display mode
nodeData["group_display_mode"] = (double)static_cast<int>(m_DisplayMode);
// Save collapsed size if in collapsed mode
if (m_DisplayMode == GroupDisplayMode::Collapsed)
{
nodeData["collapsed_size_x"] = m_CollapsedSize.x;
nodeData["collapsed_size_y"] = m_CollapsedSize.y;
}
// Save pin definitions with stable pin IDs
crude_json::value& pinDefs = nodeData["group_pin_definitions"];
for (const auto& pinDef : m_PinDefinitions)
{
crude_json::value def;
def["name"] = pinDef.Name;
def["type"] = (double)static_cast<int>(pinDef.Type);
def["kind"] = (double)static_cast<int>(pinDef.Kind);
def["pin_id"] = (double)pinDef.PinId; // Save stable pin ID
pinDefs.push_back(def);
}
}
void GroupBlock::LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app)
{
// Load display mode
if (nodeData.contains("group_display_mode"))
{
int modeValue = (int)nodeData["group_display_mode"].get<double>();
m_DisplayMode = static_cast<GroupDisplayMode>(modeValue);
}
else
{
m_DisplayMode = GroupDisplayMode::Expanded; // Default if not found
}
// Load collapsed size if available
if (nodeData.contains("collapsed_size_x") && nodeData.contains("collapsed_size_y"))
{
m_CollapsedSize.x = (float)nodeData["collapsed_size_x"].get<double>();
m_CollapsedSize.y = (float)nodeData["collapsed_size_y"].get<double>();
}
else
{
m_CollapsedSize = ImVec2(150.0f, 80.0f); // Default collapsed size
}
// Load pin definitions FIRST (before base class, so we can rebuild after)
if (nodeData.contains("group_pin_definitions"))
{
m_PinDefinitions.clear();
const auto& pinDefs = nodeData["group_pin_definitions"];
if (pinDefs.is_array())
{
for (const auto& def : pinDefs.get<crude_json::array>())
{
if (!def.is_object() || !def.contains("name") || !def.contains("type") || !def.contains("kind"))
continue;
std::string name = def["name"].get<crude_json::string>();
PinType type = static_cast<PinType>((int)def["type"].get<double>());
PinKind kind = static_cast<PinKind>((int)def["kind"].get<double>());
// Load stable pin ID (if available - for backward compatibility with old files)
int pinId = -1;
if (def.contains("pin_id"))
{
pinId = (int)def["pin_id"].get<double>();
LOG_DEBUG("[GroupBlock::LoadState] Loaded pin '{}' with stable ID {}",
name, pinId);
}
else
{
LOG_DEBUG("[GroupBlock::LoadState] Pin '{}' has no saved ID (will generate new one)",
name);
}
m_PinDefinitions.push_back(GroupPinDef(name, type, kind, pinId));
}
}
}
// Rebuild node structure with loaded pin definitions
RebuildPins(node, app);
LOG_DEBUG("[GroupBlock::LoadState] Rebuilt node {} with {} inputs, {} outputs",
ToRuntimeId(node.ID), node.Inputs.size(), node.Outputs.size());
// Call base class to load parameter values (after rebuilding structure)
ParameterizedBlock::LoadState(node, nodeData, container, app);
}
// Register block
REGISTER_BLOCK(GroupBlock, "Group");

View File

@ -0,0 +1,85 @@
#pragma once
#include "block.h"
#include <vector>
#include <string>
// Group display modes
enum class GroupDisplayMode
{
Expanded, // Full expanded view (default)
Collapsed // Compact collapsed view
};
// Pin definition for variable I/O
struct GroupPinDef
{
std::string Name;
PinType Type;
PinKind Kind; // Input or Output
int PinId; // Stable pin ID (preserved across rebuilds)
GroupPinDef() : Name(""), Type(PinType::Flow), Kind(PinKind::Input), PinId(-1) {}
GroupPinDef(const std::string& name, PinType type, PinKind kind, int pinId = -1)
: Name(name), Type(type), Kind(kind), PinId(pinId) {}
};
// Group block - inherits from ParameterizedBlock
class GroupBlock : public ParameterizedBlock
{
public:
GroupBlock(int id) : ParameterizedBlock(id, "Group")
{
SetTypeName("Group");
m_Type = NodeType::Group;
m_Color = ImColor(200, 150, 200); // Purple-ish color for groups
SetFlags(static_cast<NH_BEHAVIOR_FLAGS>(
NHBEHAVIOR_SCRIPT | NHBEHAVIOR_VARIABLEINPUTS | NHBEHAVIOR_VARIABLEOUTPUTS |
NHBEHAVIOR_VARIABLEPARAMETERINPUTS | NHBEHAVIOR_VARIABLEPARAMETEROUTPUTS));
m_DisplayMode = GroupDisplayMode::Expanded; // Default to expanded
m_CollapsedSize = ImVec2(150.0f, 80.0f); // Default collapsed size
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Group"; }
// Rendering (uses base class rendering like any other block)
void Render(Node& node, App* app, Pin* newLinkPin) override;
// Context menu for adding/removing pins
void OnMenu(Node& node, App* app) override;
// Display mode management (kept for future use)
GroupDisplayMode GetDisplayMode() const { return m_DisplayMode; }
void SetDisplayMode(GroupDisplayMode mode) { m_DisplayMode = mode; }
void ToggleDisplayMode();
// Save/load pin definitions
void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) override;
void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) override;
// Update pin name by ID (called from edit dialog)
void UpdatePinDefName(int pinId, const std::string& newName);
// Update pin type by ID (called from edit dialog)
void UpdatePinDefType(int pinId, PinType newType);
// Public methods for adding flow pins (used by keyboard shortcuts)
void AddFlowInput(const std::string& name);
void AddFlowOutput(const std::string& name);
// Public method for removing pins (used by edit dialog)
void RemovePinDef(size_t index, PinKind kind);
private:
std::vector<GroupPinDef> m_PinDefinitions; // Store pin definitions for save/load
GroupDisplayMode m_DisplayMode; // Current display mode
ImVec2 m_CollapsedSize; // Size when collapsed (resizable)
void AddPinDef(const std::string& name, PinType type, PinKind kind);
void RebuildPins(Node& node, App* app); // Helper to rebuild pins and update Node pointers
// Rendering methods for different display modes
void RenderExpanded(Node& node, App* app, Pin* newLinkPin);
void RenderCollapsed(Node& node, App* app, Pin* newLinkPin);
};

View File

@ -0,0 +1,530 @@
#include <crude_json.h>
#include "log_block.h"
#include "../app.h"
#include "../Logging.h"
#include <fstream>
#include <ctime>
#ifdef _WIN32
#include <direct.h>
#define mkdir(path, mode) _mkdir(path)
#endif
void LogBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Clear existing pin collections before rebuilding
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
// Flow input
AddInput(app, node, "Execute");
// Fixed parameter: log file path (String) - always first parameter
AddInputParameter(app, node, "FilePath", PinType::String);
// Variable parameters (built from definitions, reusing existing pin IDs)
for (auto& paramDef : m_VariableParams)
{
int pinId = paramDef.pinId;
// Generate new ID if this parameter doesn't have one yet
if (pinId < 0)
{
pinId = app->GetNextId();
paramDef.pinId = pinId; // Store for next rebuild
LOG_INFO("[LogBlock::Build] Generated new pin ID {} for '{}'", pinId, paramDef.name);
}
else
{
LOG_INFO("[LogBlock::Build] Reusing existing pin ID {} for '{}'", pinId, paramDef.name);
}
// Add to parameter collections manually (don't call AddInputParameter which generates IDs)
m_InputParams.push_back(pinId);
node.Inputs.emplace_back(pinId, paramDef.name.c_str(), paramDef.type);
}
// Flow output
AddOutput(app, node, "Done");
}
int LogBlock::Run(Node& node, App* app)
{
LogWithConfiguredLevel("[Log] Log block (ID: {}) executing", GetID());
// Get log file path from first input parameter
std::string logFilePath;
int paramIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
{
logFilePath = ParameterizedBlock::GetInputParamValueString(pin, node, app, "log_output.json");
break;
}
paramIndex++;
}
if (logFilePath.empty())
{
logFilePath = "log_output.json";
}
LogWithConfiguredLevel("[Log] Writing to file: {}", logFilePath);
// Create log entry with timestamp and all parameter values
crude_json::value logEntry;
// Add timestamp
time_t now = time(nullptr);
char timeBuffer[64];
#ifdef _WIN32
struct tm timeinfo;
localtime_s(&timeinfo, &now);
strftime(timeBuffer, sizeof(timeBuffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
#else
struct tm* timeinfo = localtime(&now);
strftime(timeBuffer, sizeof(timeBuffer), "%Y-%m-%d %H:%M:%S", timeinfo);
#endif
logEntry["timestamp"] = std::string(timeBuffer);
// Collect all input parameter values (skip FilePath and flow pins)
crude_json::value parameters;
paramIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0) // Skip FilePath parameter
{
paramIndex++;
continue;
}
// Get parameter value based on type
crude_json::value paramValue;
paramValue["name"] = pin.Name;
switch (pin.Type)
{
case PinType::Bool:
{
bool value = ParameterizedBlock::GetInputParamValueBool(pin, node, app, false);
paramValue["type"] = "bool";
paramValue["value"] = value;
break;
}
case PinType::Int:
{
int value = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
paramValue["type"] = "int";
paramValue["value"] = (double)value;
break;
}
case PinType::Float:
{
float value = ParameterizedBlock::GetInputParamValueFloat(pin, node, app, 0.0f);
paramValue["type"] = "float";
paramValue["value"] = (double)value;
break;
}
case PinType::String:
{
std::string value = ParameterizedBlock::GetInputParamValueString(pin, node, app, "");
paramValue["type"] = "string";
paramValue["value"] = value;
break;
}
default:
paramValue["type"] = "unknown";
paramValue["value"] = "?";
break;
}
parameters.push_back(paramValue);
paramIndex++;
}
logEntry["parameters"] = parameters;
// Output to console if enabled
if (m_OutputToConsole)
{
LogWithConfiguredLevel("[Log Console] Timestamp: {}", logEntry["timestamp"].get<crude_json::string>());
for (const auto& param : parameters.get<crude_json::array>())
{
std::string paramName = param["name"].get<crude_json::string>();
std::string paramType = param["type"].get<crude_json::string>();
std::string paramValueStr;
// Format value based on type
if (paramType == "bool")
{
paramValueStr = param["value"].get<bool>() ? "true" : "false";
}
else if (paramType == "int" || paramType == "float")
{
paramValueStr = spdlog::fmt_lib::format("{:.6g}", param["value"].get<double>());
}
else if (paramType == "string")
{
paramValueStr = "\"" + param["value"].get<crude_json::string>() + "\"";
}
else
{
paramValueStr = "?";
}
LogWithConfiguredLevel("[Log Console] {} ({}) = {}", paramName, paramType, paramValueStr);
}
}
// Build log array based on append mode
crude_json::value logArray;
if (m_AppendToFile)
{
// Append mode: read existing file and append new entry
std::ifstream inFile(logFilePath);
if (inFile)
{
std::string existingData((std::istreambuf_iterator<char>(inFile)), std::istreambuf_iterator<char>());
inFile.close();
if (!existingData.empty())
{
logArray = crude_json::value::parse(existingData);
if (!logArray.is_array())
{
LOG_WARN("[Log] Warning: Existing file is not a JSON array, creating new array");
logArray = crude_json::value(crude_json::type_t::array);
}
}
else
{
logArray = crude_json::value(crude_json::type_t::array);
}
}
else
{
// File doesn't exist, create new array
logArray = crude_json::value(crude_json::type_t::array);
}
// Append new entry
logArray.push_back(logEntry);
}
else
{
// Overwrite mode: create new array with just this entry
logArray = crude_json::value(crude_json::type_t::array);
logArray.push_back(logEntry);
}
// Write back to file with pretty formatting (4 space indent)
std::ofstream outFile(logFilePath);
if (outFile)
{
outFile << logArray.dump(4);
outFile.close();
LogWithConfiguredLevel("[Log] Successfully logged entry with {} parameter(s) to {} ({} mode)",
m_VariableParams.size(), logFilePath, m_AppendToFile ? "append" : "overwrite");
}
else
{
LOG_ERROR("[Log] ERROR: Failed to write to {}", logFilePath);
return 1; // Error
}
return E_OK;
}
void LogBlock::SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app)
{
// Call parent to save unconnected parameter values
ParameterizedBlock::SaveState(node, nodeData, container, app);
// Save variable parameters configuration with stable pin IDs
crude_json::value& varParams = nodeData["log_variable_params"];
for (const auto& param : m_VariableParams)
{
crude_json::value paramData;
paramData["name"] = param.name;
paramData["type"] = (double)static_cast<int>(param.type);
paramData["pin_id"] = (double)param.pinId; // Save stable pin ID
varParams.push_back(paramData);
}
// Save log settings
nodeData["log_output_to_console"] = m_OutputToConsole;
nodeData["log_append_to_file"] = m_AppendToFile;
nodeData["log_level"] = static_cast<double>(m_LogLevel);
}
void LogBlock::LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app)
{
// Load variable parameters configuration FIRST (before base class, so we can rebuild)
if (nodeData.contains("log_variable_params") && nodeData["log_variable_params"].is_array())
{
m_VariableParams.clear();
const auto& varParams = nodeData["log_variable_params"].get<crude_json::array>();
for (const auto& paramData : varParams)
{
if (paramData.is_object() && paramData.contains("name") && paramData.contains("type"))
{
std::string name = paramData["name"].get<crude_json::string>();
PinType type = static_cast<PinType>((int)paramData["type"].get<double>());
// Load stable pin ID (if available)
int pinId = -1;
if (paramData.contains("pin_id"))
{
pinId = (int)paramData["pin_id"].get<double>();
LOG_DEBUG("[LogBlock::LoadState] Loaded parameter '{}' with stable ID {}", name, pinId);
}
AddVariableParamDef(name, type, pinId);
}
}
}
// Rebuild node structure with loaded parameter definitions
RebuildPins(node, app);
LOG_DEBUG("[LogBlock::LoadState] Rebuilt node {} with {} variable parameters",
node.ID.Get(), m_VariableParams.size());
// Load log settings (with defaults if not present)
if (nodeData.contains("log_output_to_console"))
m_OutputToConsole = nodeData["log_output_to_console"].get<bool>();
if (nodeData.contains("log_append_to_file"))
m_AppendToFile = nodeData["log_append_to_file"].get<bool>();
if (nodeData.contains("log_level"))
{
int levelValue = static_cast<int>(nodeData["log_level"].get<double>());
if (levelValue >= static_cast<int>(spdlog::level::trace) &&
levelValue <= static_cast<int>(spdlog::level::n_levels) - 1)
{
m_LogLevel = static_cast<spdlog::level::level_enum>(levelValue);
}
}
// Call parent to load unconnected parameter values (after rebuilding structure)
ParameterizedBlock::LoadState(node, nodeData, container, app);
}
void LogBlock::RenderEditUI(Node& node, App* app)
{
ImGui::Separator();
ImGui::Spacing();
ImGui::TextUnformatted("Log Settings");
ImGui::Spacing();
// Output to console checkbox
ImGui::Checkbox("Output to Console", &m_OutputToConsole);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Also print log entries to console (stdout)");
// Append to file checkbox
ImGui::Checkbox("Append to File", &m_AppendToFile);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Append entries to existing file (if unchecked, file will be overwritten)");
ImGui::Spacing();
static const char* levelLabels[] = {"Trace", "Debug", "Info", "Warn", "Error", "Critical", "Off"};
static const spdlog::level::level_enum levelValues[] = {
spdlog::level::trace,
spdlog::level::debug,
spdlog::level::info,
spdlog::level::warn,
spdlog::level::err,
spdlog::level::critical,
spdlog::level::off
};
int currentLevelIndex = 0;
for (int i = 0; i < IM_ARRAYSIZE(levelValues); ++i)
{
if (levelValues[i] == m_LogLevel)
{
currentLevelIndex = i;
break;
}
}
if (ImGui::Combo("Log Level", &currentLevelIndex, levelLabels, IM_ARRAYSIZE(levelLabels)))
{
m_LogLevel = levelValues[currentLevelIndex];
}
}
void LogBlock::OnMenu(Node& node, App* app)
{
// Call parent menu
ParameterizedBlock::OnMenu(node, app);
ImGui::Separator();
ImGui::TextUnformatted("Variable Parameters");
if (ImGui::BeginMenu("Add Parameter"))
{
if (ImGui::MenuItem("Bool"))
{
std::string paramName = "Param" + std::to_string(m_VariableParams.size() + 1);
AddVariableParamDef(paramName, PinType::Bool);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Int"))
{
std::string paramName = "Param" + std::to_string(m_VariableParams.size() + 1);
AddVariableParamDef(paramName, PinType::Int);
RebuildPins(node, app);
}
if (ImGui::MenuItem("Float"))
{
std::string paramName = "Param" + std::to_string(m_VariableParams.size() + 1);
AddVariableParamDef(paramName, PinType::Float);
RebuildPins(node, app);
}
if (ImGui::MenuItem("String"))
{
std::string paramName = "Param" + std::to_string(m_VariableParams.size() + 1);
AddVariableParamDef(paramName, PinType::String);
RebuildPins(node, app);
}
ImGui::EndMenu();
}
if (!m_VariableParams.empty())
{
if (ImGui::BeginMenu("Remove Parameter"))
{
for (size_t i = 0; i < m_VariableParams.size(); ++i)
{
const auto& param = m_VariableParams[i];
std::string label = param.name + " (" +
(param.type == PinType::Bool ? "Bool" :
param.type == PinType::Int ? "Int" :
param.type == PinType::Float ? "Float" :
param.type == PinType::String ? "String" : "Unknown") + ")";
if (ImGui::MenuItem(label.c_str()))
{
RemoveVariableParamDef(i);
RebuildPins(node, app);
break;
}
}
ImGui::EndMenu();
}
}
}
void LogBlock::RebuildPins(Node& node, App* app)
{
// CRITICAL: Preserve UUIDs before clearing (pins are about to be destroyed!)
std::map<int, Uuid64> oldPinUuids; // runtime pin ID -> UUID
for (const auto& input : node.Inputs)
{
if (input.UUID.IsValid())
oldPinUuids[input.ID.Get()] = input.UUID;
}
for (const auto& output : node.Outputs)
{
if (output.UUID.IsValid())
oldPinUuids[output.ID.Get()] = output.UUID;
}
LOG_DEBUG("[LogBlock::RebuildPins] Preserved {} pin UUIDs before rebuild", oldPinUuids.size());
// Rebuild node structure
node.Inputs.clear();
node.Outputs.clear();
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
Build(node, app);
// Restore or generate UUIDs for pins
for (auto& input : node.Inputs)
{
int pinId = input.ID.Get();
// Try to restore old UUID if this pin ID existed before
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
input.UUID = it->second; // Restore old UUID
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
LOG_DEBUG("[LogBlock::RebuildPins] Restored UUID for input pin {}: 0x{:08X}{:08X}",
pinId, input.UUID.high, input.UUID.low);
}
else
{
// New pin - generate fresh UUID
input.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
LOG_DEBUG("[LogBlock::RebuildPins] Generated UUID for NEW input pin {}: 0x{:08X}{:08X}",
pinId, input.UUID.high, input.UUID.low);
}
input.Node = &node;
input.Kind = PinKind::Input;
}
for (auto& output : node.Outputs)
{
int pinId = output.ID.Get();
// Try to restore old UUID if this pin ID existed before
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
output.UUID = it->second; // Restore old UUID
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
LOG_DEBUG("[LogBlock::RebuildPins] Restored UUID for output pin {}: 0x{:08X}{:08X}",
pinId, output.UUID.high, output.UUID.low);
}
else
{
// New pin - generate fresh UUID
output.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
LOG_DEBUG("[LogBlock::RebuildPins] Generated UUID for NEW output pin {}: 0x{:08X}{:08X}",
pinId, output.UUID.high, output.UUID.low);
}
output.Node = &node;
output.Kind = PinKind::Output;
}
}
void LogBlock::AddVariableParamDef(const std::string& name, PinType type, int pinId)
{
m_VariableParams.push_back(VariableParamDef(name, type, pinId));
}
void LogBlock::RemoveVariableParamDef(size_t index)
{
if (index < m_VariableParams.size())
{
m_VariableParams.erase(m_VariableParams.begin() + index);
}
}
// Register the Log block
REGISTER_BLOCK(LogBlock, "Log");

View File

@ -0,0 +1,70 @@
#pragma once
#include "block.h"
#include "../Logging.h"
#include <vector>
#include <utility>
#include <spdlog/common.h>
// Log block - logs input parameters to a JSON file
// First parameter is the log file path (fixed)
// Additional parameters can be added dynamically
// Appends entries as JSON array with timestamps
class LogBlock : public ParameterizedBlock
{
public:
LogBlock(int id) : ParameterizedBlock(id, "Log")
{
SetTypeName("Log");
m_Type = NodeType::Blueprint;
m_Color = ImColor(255, 200, 100);
SetFlags(static_cast<NH_BEHAVIOR_FLAGS>(
NHBEHAVIOR_SCRIPT |
NHBEHAVIOR_VARIABLEINPUTS));
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Log"; }
// State save/load to persist variable parameters
void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) override;
void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) override;
// Context menu - allow adding/removing variable parameters
void OnMenu(Node& node, App* app) override;
// Edit dialog UI - custom settings for Log block
void RenderEditUI(Node& node, App* app) override;
// Pin management (like GroupBlock)
void RebuildPins(Node& node, App* app);
void AddVariableParamDef(const std::string& name, PinType type, int pinId = -1);
void RemoveVariableParamDef(size_t index);
private:
struct VariableParamDef
{
std::string name;
PinType type;
int pinId; // Stable pin ID (preserved across rebuilds)
VariableParamDef(const std::string& n, PinType t, int id = -1)
: name(n), type(t), pinId(id) {}
};
std::vector<VariableParamDef> m_VariableParams;
// Log settings
template <typename... Args>
void LogWithConfiguredLevel(NH_CSTRING fmt, Args&&... args) const
{
if (m_LogLevel == spdlog::level::off || !g_logger)
return;
g_logger->log(m_LogLevel, fmt, std::forward<Args>(args)...);
}
bool m_OutputToConsole = false; // Also output to console
bool m_AppendToFile = true; // Append to file (default) vs overwrite
spdlog::level::level_enum m_LogLevel = spdlog::level::info;
};

View File

@ -0,0 +1,421 @@
#include "logic_blocks.h"
#include "../app.h"
#include "../Logging.h"
#include <crude_json.h>
#include <imgui.h>
#include <map>
#include <string>
namespace
{
const char* ToString(LogicTestOperator op)
{
switch (op)
{
case LogicTestOperator::Equal: return "Equal";
case LogicTestOperator::NotEqual: return "Not Equal";
case LogicTestOperator::Less: return "Less";
case LogicTestOperator::LessEqual: return "Less or Equal";
case LogicTestOperator::Greater: return "Greater";
case LogicTestOperator::GreaterEqual: return "Greater or Equal";
default: return "Unknown";
}
}
}
LogicTestBlock::LogicTestBlock(int id)
: ParameterizedBlock(id, "Test")
, m_ParamType(PinType::Int)
, m_Operator(LogicTestOperator::Equal)
, m_FlowInputId(-1)
, m_FlowOutputIds({-1, -1})
, m_ValueParamIds({-1, -1})
{
SetTypeName("Logic.Test");
m_Type = NodeType::Blueprint;
m_Color = ImColor(230, 180, 95);
}
void LogicTestBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
// Flow input (Execute)
if (m_FlowInputId < 0)
{
m_FlowInputId = app->GetNextId();
}
m_Inputs.push_back(m_FlowInputId);
node.Inputs.emplace_back(m_FlowInputId, "", PinType::Flow);
// Parameter inputs (A, B)
for (int i = 0; i < 2; ++i)
{
if (m_ValueParamIds[i] < 0)
{
m_ValueParamIds[i] = app->GetNextId();
}
m_InputParams.push_back(m_ValueParamIds[i]);
const char* label = (i == 0) ? "A" : "B";
node.Inputs.emplace_back(m_ValueParamIds[i], label, m_ParamType);
}
// Flow outputs (True, False)
static const char* kFlowNames[2] = { "True", "False" };
for (int i = 0; i < 2; ++i)
{
if (m_FlowOutputIds[i] < 0)
{
m_FlowOutputIds[i] = app->GetNextId();
}
m_Outputs.push_back(m_FlowOutputIds[i]);
node.Outputs.emplace_back(m_FlowOutputIds[i], kFlowNames[i], PinType::Flow);
}
EnsureDefaultParamValues(node);
}
template <typename T>
bool LogicTestBlock::EvaluateOrdered(const T& a, const T& b) const
{
switch (m_Operator)
{
case LogicTestOperator::Equal: return a == b;
case LogicTestOperator::NotEqual: return a != b;
case LogicTestOperator::Less: return a < b;
case LogicTestOperator::LessEqual: return a <= b;
case LogicTestOperator::Greater: return a > b;
case LogicTestOperator::GreaterEqual: return a >= b;
}
return false;
}
bool LogicTestBlock::EvaluateStrings(const std::string& a, const std::string& b) const
{
switch (m_Operator)
{
case LogicTestOperator::Equal: return a == b;
case LogicTestOperator::NotEqual: return a != b;
case LogicTestOperator::Less: return a < b;
case LogicTestOperator::LessEqual: return a <= b;
case LogicTestOperator::Greater: return a > b;
case LogicTestOperator::GreaterEqual: return a >= b;
}
return false;
}
int LogicTestBlock::Run(Node& node, App* app)
{
// Gather parameter pins (skip flow pins)
const Pin* valuePins[2] = { nullptr, nullptr };
int valueIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (valueIndex < 2)
{
valuePins[valueIndex++] = &pin;
}
}
if (!valuePins[0] || !valuePins[1])
{
LOG_WARN("[Logic.Test] Missing parameter inputs on node {}", ToRuntimeId(node.ID));
ActivateOutput(0, false);
ActivateOutput(1, false);
return E_OK;
}
bool comparisonResult = false;
switch (m_ParamType)
{
case PinType::Bool:
{
bool a = GetInputParamValueBool(*valuePins[0], node, app, false);
bool b = GetInputParamValueBool(*valuePins[1], node, app, false);
// Treat booleans as integers for ordered comparisons
comparisonResult = EvaluateOrdered<int>(a ? 1 : 0, b ? 1 : 0);
break;
}
case PinType::Int:
{
int a = GetInputParamValueInt(*valuePins[0], node, app, 0);
int b = GetInputParamValueInt(*valuePins[1], node, app, 0);
comparisonResult = EvaluateOrdered<int>(a, b);
break;
}
case PinType::Float:
{
float a = GetInputParamValueFloat(*valuePins[0], node, app, 0.0f);
float b = GetInputParamValueFloat(*valuePins[1], node, app, 0.0f);
comparisonResult = EvaluateOrdered<float>(a, b);
break;
}
case PinType::String:
{
std::string a = GetInputParamValueString(*valuePins[0], node, app, "");
std::string b = GetInputParamValueString(*valuePins[1], node, app, "");
comparisonResult = EvaluateStrings(a, b);
break;
}
default:
{
LOG_WARN("[Logic.Test] Unsupported parameter type on node {}", ToRuntimeId(node.ID));
comparisonResult = false;
break;
}
}
ActivateOutput(0, comparisonResult);
ActivateOutput(1, !comparisonResult);
LOG_DEBUG("[Logic.Test] Node {} result={} (operator={})", ToRuntimeId(node.ID), comparisonResult ? "true" : "false", ToString(m_Operator));
return E_OK;
}
void LogicTestBlock::EnsureDefaultParamValues(Node& node)
{
for (int i = 0; i < 2; ++i)
{
int pinId = m_ValueParamIds[i];
if (pinId < 0)
continue;
std::string& value = node.UnconnectedParamValues[pinId];
switch (m_ParamType)
{
case PinType::Bool:
{
if (!(value == "true" || value == "false" || value == "1" || value == "0"))
value = "false";
break;
}
case PinType::Int:
{
if (value.empty())
value = "0";
break;
}
case PinType::Float:
{
if (value.empty())
value = "0.0";
break;
}
case PinType::String:
default:
{
// Nothing to do; entry already exists
break;
}
}
}
}
void LogicTestBlock::RebuildPins(Node& node, App* app)
{
std::map<int, Uuid64> oldPinUuids;
for (const auto& input : node.Inputs)
{
const int pinId = ToRuntimeId(input.ID);
if (input.UUID.IsValid())
{
oldPinUuids[pinId] = input.UUID;
}
}
for (const auto& output : node.Outputs)
{
if (output.UUID.IsValid())
{
oldPinUuids[ToRuntimeId(output.ID)] = output.UUID;
}
}
node.Inputs.clear();
node.Outputs.clear();
m_InputParams.clear();
m_OutputParams.clear();
m_Inputs.clear();
m_Outputs.clear();
Build(node, app);
for (auto& input : node.Inputs)
{
const int pinId = ToRuntimeId(input.ID);
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
input.UUID = it->second;
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
}
else
{
input.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(input.UUID, pinId);
}
input.Node = &node;
input.Kind = PinKind::Input;
}
for (auto& output : node.Outputs)
{
const int pinId = ToRuntimeId(output.ID);
auto it = oldPinUuids.find(pinId);
if (it != oldPinUuids.end())
{
output.UUID = it->second;
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
}
else
{
output.UUID = app->m_UuidIdManager.GenerateUuid();
app->m_UuidIdManager.RegisterPin(output.UUID, pinId);
}
output.Node = &node;
output.Kind = PinKind::Output;
}
}
void LogicTestBlock::SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app)
{
ParameterizedBlock::SaveState(node, nodeData, container, app);
nodeData["logic_test_param_type"] = (double)static_cast<int>(m_ParamType);
nodeData["logic_test_operator"] = (double)static_cast<int>(m_Operator);
nodeData["logic_test_flow_input_id"] = (double)m_FlowInputId;
nodeData["logic_test_flow_output_true_id"] = (double)m_FlowOutputIds[0];
nodeData["logic_test_flow_output_false_id"] = (double)m_FlowOutputIds[1];
nodeData["logic_test_param_a_id"] = (double)m_ValueParamIds[0];
nodeData["logic_test_param_b_id"] = (double)m_ValueParamIds[1];
}
void LogicTestBlock::LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app)
{
if (nodeData.contains("logic_test_param_type"))
{
m_ParamType = static_cast<PinType>((int)nodeData["logic_test_param_type"].get<double>());
}
if (nodeData.contains("logic_test_operator"))
{
m_Operator = static_cast<LogicTestOperator>((int)nodeData["logic_test_operator"].get<double>());
}
if (nodeData.contains("logic_test_flow_input_id"))
m_FlowInputId = (int)nodeData["logic_test_flow_input_id"].get<double>();
if (nodeData.contains("logic_test_flow_output_true_id"))
m_FlowOutputIds[0] = (int)nodeData["logic_test_flow_output_true_id"].get<double>();
if (nodeData.contains("logic_test_flow_output_false_id"))
m_FlowOutputIds[1] = (int)nodeData["logic_test_flow_output_false_id"].get<double>();
if (nodeData.contains("logic_test_param_a_id"))
m_ValueParamIds[0] = (int)nodeData["logic_test_param_a_id"].get<double>();
if (nodeData.contains("logic_test_param_b_id"))
m_ValueParamIds[1] = (int)nodeData["logic_test_param_b_id"].get<double>();
// If IDs were not saved (backward compatibility), capture current IDs from node
if (m_FlowInputId < 0 && !node.Inputs.empty())
m_FlowInputId = ToRuntimeId(node.Inputs.front().ID);
int paramCounter = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
int valueIndex = paramCounter;
if (valueIndex < 2 && m_ValueParamIds[valueIndex] < 0)
m_ValueParamIds[valueIndex] = ToRuntimeId(pin.ID);
++paramCounter;
}
int flowOutCounter = 0;
for (const auto& pin : node.Outputs)
{
if (pin.Type != PinType::Flow)
continue;
if (flowOutCounter < 2 && m_FlowOutputIds[flowOutCounter] < 0)
{
m_FlowOutputIds[flowOutCounter] = ToRuntimeId(pin.ID);
}
++flowOutCounter;
}
RebuildPins(node, app);
ParameterizedBlock::LoadState(node, nodeData, container, app);
EnsureDefaultParamValues(node);
}
void LogicTestBlock::OnMenu(Node& node, App* app)
{
ParameterizedBlock::OnMenu(node, app);
ImGui::Separator();
ImGui::TextUnformatted("Logic.Test");
if (ImGui::BeginMenu("Parameter Type"))
{
const struct { PinType Type; const char* Label; } options[] = {
{ PinType::Bool, "Bool" },
{ PinType::Int, "Int" },
{ PinType::Float, "Float" },
{ PinType::String, "String" }
};
for (const auto& option : options)
{
bool selected = (m_ParamType == option.Type);
if (ImGui::MenuItem(option.Label, nullptr, selected))
{
m_ParamType = option.Type;
RebuildPins(node, app);
EnsureDefaultParamValues(node);
}
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Operator"))
{
const struct { LogicTestOperator Op; const char* Label; } ops[] = {
{ LogicTestOperator::Equal, "Equal (==)" },
{ LogicTestOperator::NotEqual, "Not Equal (!=)" },
{ LogicTestOperator::Less, "Less (<)" },
{ LogicTestOperator::LessEqual, "Less or Equal (<=)" },
{ LogicTestOperator::Greater, "Greater (>)" },
{ LogicTestOperator::GreaterEqual, "Greater or Equal (>=)" }
};
for (const auto& entry : ops)
{
bool selected = (m_Operator == entry.Op);
if (ImGui::MenuItem(entry.Label, nullptr, selected))
{
m_Operator = entry.Op;
}
}
ImGui::EndMenu();
}
ImGui::Separator();
ImGui::Text("Current Operator: %s", ToString(m_Operator));
}
REGISTER_BLOCK(LogicTestBlock, "Logic.Test");

View File

@ -0,0 +1,49 @@
#pragma once
#include "block.h"
#include <array>
class Container;
namespace crude_json { struct value; }
// Operators supported by Logic.Test block
enum class LogicTestOperator
{
Equal = 0,
NotEqual,
Less,
LessEqual,
Greater,
GreaterEqual
};
class LogicTestBlock : public ParameterizedBlock
{
public:
LogicTestBlock(int id);
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Logic.Test"; }
void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) override;
void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) override;
void OnMenu(Node& node, App* app) override;
bool ShouldAutoActivateDefaultOutput() const override { return false; }
private:
void RebuildPins(Node& node, App* app);
void EnsureDefaultParamValues(Node& node);
template <typename T>
bool EvaluateOrdered(const T& a, const T& b) const;
bool EvaluateStrings(const std::string& a, const std::string& b) const;
PinType m_ParamType;
LogicTestOperator m_Operator;
int m_FlowInputId;
std::array<int, 2> m_FlowOutputIds; // 0 = True, 1 = False
std::array<int, 2> m_ValueParamIds; // 0 = A, 1 = B
};

View File

@ -0,0 +1,205 @@
#include "math_blocks.h"
#include "../app.h"
#include "block.h"
#include "../Logging.h"
void AddBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Flow control (left/right) - add FIRST so they're at indices 0
AddInput(app, node, "Execute");
// Input parameters (connections or values)
AddInputParameter(app, node, "A", PinType::Int);
AddInputParameter(app, node, "B", PinType::Int);
// Flow outputs
AddOutput(app, node, "Done");
// Output parameters
AddOutputParameter(app, node, "Result", PinType::Int);
}
void MultiplyBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Flow control
AddInput(app, node, "Execute");
// Input parameters
AddInputParameter(app, node, "A", PinType::Int);
AddInputParameter(app, node, "B", PinType::Int);
// Flow outputs
AddOutput(app, node, "Done");
// Output parameters
AddOutputParameter(app, node, "Result", PinType::Int);
}
void CompareBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Flow control
AddInput(app, node, "Execute");
// Input parameters
AddInputParameter(app, node, "A", PinType::Int);
AddInputParameter(app, node, "B", PinType::Int);
// Flow outputs
AddOutput(app, node, "Done");
// Output parameters (multiple outputs for comparison results)
AddOutputParameter(app, node, "==", PinType::Bool);
AddOutputParameter(app, node, "<", PinType::Bool);
AddOutputParameter(app, node, ">", PinType::Bool);
}
// Helper functions moved to ParameterizedBlock base class
// Use ParameterizedBlock::GetInputParamValueInt/SetOutputParamValueInt/etc.
int AddBlock::Run(Node& node, App* app)
{
// Get input parameters (skip flow pins)
int inputA = 0, inputB = 0;
int paramIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
inputA = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
else if (paramIndex == 1)
inputB = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
paramIndex++;
}
// Perform addition
int result = inputA + inputB;
// Set output parameter value
paramIndex = 0;
for (const auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
{
ParameterizedBlock::SetOutputParamValueInt(pin, node, app, result);
break;
}
paramIndex++;
}
LOG_TRACE("[Math.Add] Computed: {} + {} = {}", inputA, inputB, result);
return E_OK;
}
int MultiplyBlock::Run(Node& node, App* app)
{
LOG_INFO("[Math.Multiply] Block executing (ID: {})", GetID());
// Get input parameters (skip flow pins)
int inputA = 0, inputB = 0;
int paramIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
inputA = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
else if (paramIndex == 1)
inputB = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
paramIndex++;
}
// Perform multiplication
int result = inputA * inputB;
LOG_INFO("[Math.Multiply] Computed: {} * {} = {}", inputA, inputB, result);
// Set output parameter value
paramIndex = 0;
for (const auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
{
ParameterizedBlock::SetOutputParamValueInt(pin, node, app, result);
break;
}
paramIndex++;
}
return E_OK;
}
int CompareBlock::Run(Node& node, App* app)
{
LOG_INFO("[Math.Compare] Block executing (ID: {})", GetID());
// Get input parameters (skip flow pins)
int inputA = 0, inputB = 0;
int paramIndex = 0;
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
inputA = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
else if (paramIndex == 1)
inputB = ParameterizedBlock::GetInputParamValueInt(pin, node, app, 0);
paramIndex++;
}
// Perform comparisons and set output parameter values
paramIndex = 0;
for (const auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
continue;
bool result = false;
if (paramIndex == 0) // ==
result = (inputA == inputB);
else if (paramIndex == 1) // <
result = (inputA < inputB);
else if (paramIndex == 2) // >
result = (inputA > inputB);
LOG_DEBUG("[Math.Compare] Output {} = {}", paramIndex == 0 ? "==" : paramIndex == 1 ? "<" : ">", result);
// Set output parameter value (propagates to connected nodes)
ParameterizedBlock::SetOutputParamValueBool(pin, node, app, result);
paramIndex++;
}
return E_OK;
}
// Register blocks
REGISTER_BLOCK(AddBlock, "Math.Add");
REGISTER_BLOCK(MultiplyBlock, "Math.Multiply");
REGISTER_BLOCK(CompareBlock, "Math.Compare");

View File

@ -0,0 +1,51 @@
#pragma once
#include "block.h"
//#include "parameter.h"
// Example blocks - Math operations
class AddBlock : public ParameterizedBlock
{
public:
AddBlock(int id) : ParameterizedBlock(id, "Add")
{
SetTypeName("Math.Add");
m_Type = NodeType::Blueprint;
m_Color = ImColor(128, 195, 248);
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Math.Add"; }
};
class MultiplyBlock : public ParameterizedBlock
{
public:
MultiplyBlock(int id) : ParameterizedBlock(id, "Multiply")
{
SetTypeName("Math.Multiply");
m_Type = NodeType::Blueprint;
m_Color = ImColor(128, 195, 248);
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Math.Multiply"; }
};
class CompareBlock : public ParameterizedBlock
{
public:
CompareBlock(int id) : ParameterizedBlock(id, "Compare")
{
SetTypeName("Math.Compare");
m_Type = NodeType::Blueprint;
m_Color = ImColor(128, 195, 248);
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Math.Compare"; }
};

View File

@ -0,0 +1,355 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include "parameter_edit_dialog.h"
#include "../app.h"
#include "../types.h"
#include "parameter_node.h"
#include <imgui.h>
#include <imgui_node_editor.h>
namespace ed = ax::NodeEditor;
//------------------------------------------------------------------------------
// Parameter Edit Dialog
//------------------------------------------------------------------------------
static bool s_ParameterEditDialogOpen = false;
static Node* s_EditingParameterNode = nullptr;
static App* s_EditingParameterApp = nullptr;
void OpenParameterEditDialog(Node* node, App* app)
{
s_EditingParameterNode = node;
s_EditingParameterApp = app;
s_ParameterEditDialogOpen = true;
}
void RenderParameterEditDialog()
{
if (!s_ParameterEditDialogOpen || !s_EditingParameterNode || !s_EditingParameterApp)
return;
// Suspend node editor and disable shortcuts to prevent input conflicts
ed::Suspend();
ed::EnableShortcuts(false);
// Center modal window
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing);
// Open popup if not already open
if (!ImGui::IsPopupOpen("Edit Parameter", ImGuiPopupFlags_AnyPopupId))
ImGui::OpenPopup("Edit Parameter");
// Use proper modal flags
bool isOpen = s_ParameterEditDialogOpen;
if (!ImGui::BeginPopupModal("Edit Parameter", &isOpen, ImGuiWindowFlags_AlwaysAutoResize))
{
if (!isOpen)
s_ParameterEditDialogOpen = false;
ed::EnableShortcuts(true);
ed::Resume();
return;
}
// Update our state if popup was closed via X button
if (!isOpen)
{
s_ParameterEditDialogOpen = false;
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
// Handle ESC to close (only if not typing in an input field)
if (!ImGui::IsAnyItemActive() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
{
s_ParameterEditDialogOpen = false;
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
if (s_EditingParameterNode->Type != NodeType::Parameter || !s_EditingParameterNode->ParameterInstance)
{
ImGui::Text("Error: Not a parameter node");
if (ImGui::Button("Close"))
{
s_ParameterEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
ed::EnableShortcuts(true);
ed::Resume();
return;
}
auto* paramInstance = s_EditingParameterNode->ParameterInstance;
// Parameter Name
ImGui::Text("Parameter Name:");
ImGui::SameLine();
ImGui::PushItemWidth(200.0f);
static std::map<int, std::string> nameBuffers;
int nodeId = s_EditingParameterNode->ID.Get();
if (nameBuffers.find(nodeId) == nameBuffers.end())
{
nameBuffers[nodeId] = s_EditingParameterNode->Name;
}
char nameBuffer[128];
strncpy(nameBuffer, nameBuffers[nodeId].c_str(), 127);
nameBuffer[127] = '\0';
if (ImGui::InputText("##param_name", nameBuffer, 128))
{
nameBuffers[nodeId] = nameBuffer;
s_EditingParameterNode->Name = nameBuffer;
paramInstance->SetName(nameBuffer);
// Sync to source or shortcuts
if (paramInstance->IsSource())
paramInstance->SyncNameToAllShortcuts(*s_EditingParameterNode, s_EditingParameterApp);
else if (paramInstance->GetSourceID() > 0)
paramInstance->SyncNameToSource(s_EditingParameterApp);
}
ImGui::PopItemWidth();
ImGui::Separator();
ImGui::Spacing();
// Parameter Type (editable combo)
ImGui::Text("Type:");
ImGui::SameLine();
ImGui::PushItemWidth(150.0f);
static std::map<int, int> typeIndices; // Per-node type index
const char* typeItems[] = { "Bool", "Int", "Float", "String" };
PinType typeValues[] = { PinType::Bool, PinType::Int, PinType::Float, PinType::String };
const int typeCount = 4;
// Initialize type index if needed
if (typeIndices.find(nodeId) == typeIndices.end())
{
// Find current type in the array
for (int i = 0; i < typeCount; i++)
{
if (typeValues[i] == s_EditingParameterNode->ParameterType)
{
typeIndices[nodeId] = i;
break;
}
}
}
int currentTypeIndex = typeIndices[nodeId];
if (ImGui::Combo("##param_type_combo", &currentTypeIndex, typeItems, typeCount))
{
typeIndices[nodeId] = currentTypeIndex;
PinType newType = typeValues[currentTypeIndex];
// Update parameter type
s_EditingParameterNode->ParameterType = newType;
paramInstance->SetType(newType);
// Clear all input pins and rebuild
s_EditingParameterNode->Inputs.clear();
s_EditingParameterNode->Outputs.clear();
paramInstance->Build(*s_EditingParameterNode, s_EditingParameterApp);
// Initialize default value for the new type
switch (newType)
{
case PinType::Bool:
s_EditingParameterNode->BoolValue = false;
paramInstance->SetBool(false);
break;
case PinType::Int:
s_EditingParameterNode->IntValue = 0;
paramInstance->SetInt(0);
break;
case PinType::Float:
s_EditingParameterNode->FloatValue = 0.0f;
paramInstance->SetFloat(0.0f);
break;
case PinType::String:
s_EditingParameterNode->StringValue = "";
paramInstance->SetString("");
break;
default:
break;
}
}
ImGui::PopItemWidth();
ImGui::Separator();
ImGui::Spacing();
// Value Editor
ImGui::Text("Value:");
ImGui::PushItemWidth(200.0f);
bool valueChanged = false;
switch (s_EditingParameterNode->ParameterType)
{
case PinType::Bool:
if (ImGui::Checkbox("##value", &s_EditingParameterNode->BoolValue))
{
paramInstance->SetBool(s_EditingParameterNode->BoolValue);
valueChanged = true;
}
break;
case PinType::Int:
if (ImGui::DragInt("##value", &s_EditingParameterNode->IntValue, 1.0f))
{
paramInstance->SetInt(s_EditingParameterNode->IntValue);
valueChanged = true;
}
break;
case PinType::Float:
if (ImGui::DragFloat("##value", &s_EditingParameterNode->FloatValue, 0.01f))
{
paramInstance->SetFloat(s_EditingParameterNode->FloatValue);
valueChanged = true;
}
break;
case PinType::String:
{
static std::map<int, std::string> stringBuffers;
if (stringBuffers.find(nodeId) == stringBuffers.end())
{
stringBuffers[nodeId] = s_EditingParameterNode->StringValue;
}
char strBuffer[256];
strncpy(strBuffer, stringBuffers[nodeId].c_str(), 255);
strBuffer[255] = '\0';
if (ImGui::InputText("##value", strBuffer, 256))
{
stringBuffers[nodeId] = strBuffer;
s_EditingParameterNode->StringValue = strBuffer;
paramInstance->SetString(strBuffer);
valueChanged = true;
}
break;
}
default:
ImGui::Text("Unknown type");
break;
}
// Sync value if changed
if (valueChanged)
{
if (paramInstance->IsSource())
paramInstance->SyncValueToAllShortcuts(*s_EditingParameterNode, s_EditingParameterApp);
else if (paramInstance->GetSourceID() > 0)
paramInstance->SyncValueToSource(s_EditingParameterApp);
}
ImGui::PopItemWidth();
ImGui::Separator();
ImGui::Spacing();
// Display Mode
ImGui::Text("Display Mode:");
auto currentMode = paramInstance->GetDisplayMode();
const char* modeNames[] = {"Name Only", "Name + Value", "Small Box", "Minimal", "Minimal Links"};
int currentModeIndex = static_cast<int>(currentMode);
ImGui::PushItemWidth(200.0f);
if (ImGui::Combo("##display_mode", &currentModeIndex, modeNames, 5))
{
paramInstance->SetDisplayMode(static_cast<ParameterDisplayMode>(currentModeIndex));
// Notify editor that display mode changed
ed::NotifyBlockDisplayModeChanged(s_EditingParameterNode->ID);
}
ImGui::PopItemWidth();
ImGui::Separator();
ImGui::Spacing();
// Source/Shortcut Settings
ImGui::Text("Source/Shortcut:");
// Only allow marking as source if not already a shortcut
if (paramInstance->GetSourceID() == 0)
{
bool isSource = paramInstance->IsSource();
if (ImGui::Checkbox("Mark as Source", &isSource))
{
paramInstance->SetIsSource(isSource);
if (!isSource)
{
paramInstance->SetSourceID(0);
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Source parameters can have shortcuts created from them");
}
else
{
// This is a shortcut - show source info
ImGui::TextDisabled("This is a shortcut to source: %d", paramInstance->GetSourceID());
if (ImGui::Button("Clear Shortcut Reference"))
{
paramInstance->SetSourceID(0);
paramInstance->SetIsSource(false);
}
}
ImGui::Separator();
ImGui::Spacing();
// Buttons
bool confirmPressed = false;
if (ImGui::Button("Confirm (Enter)", ImVec2(120, 0)))
{
confirmPressed = true;
}
ImGui::SameLine();
if (ImGui::Button("Cancel (Esc)", ImVec2(120, 0)))
{
s_ParameterEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
// Handle Enter key to confirm (only if not typing in an input field)
if (!ImGui::IsAnyItemActive() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)))
{
confirmPressed = true;
}
if (confirmPressed)
{
// Save graph
auto* activeContainer = s_EditingParameterApp->GetActiveRootContainer();
if (activeContainer)
{
s_EditingParameterApp->SaveGraph(s_EditingParameterApp->m_GraphFilename, activeContainer);
}
s_ParameterEditDialogOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
// Resume node editor and re-enable shortcuts
ed::EnableShortcuts(true);
ed::Resume();
}
bool IsParameterEditDialogOpen()
{
return s_ParameterEditDialogOpen;
}

View File

@ -0,0 +1,10 @@
#pragma once
struct Node;
class App;
// Parameter Edit Dialog
void OpenParameterEditDialog(Node* node, App* app);
void RenderParameterEditDialog();
bool IsParameterEditDialogOpen();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
#pragma once
#include "../commons.h"
#include "../types.h"
#include "../utilities/node_renderer_base.h"
#include "../core/Object.h"
#include <string>
#include <vector>
// Forward declarations
class App;
class Container;
namespace crude_json { struct value; }
// Parameter display modes
enum class ParameterDisplayMode
{
NameOnly, // Just the name (compact)
NameAndValue, // Name + editable value (default)
SmallBox, // Tiny colored rectangle with value
Minimal, // Just a rectangle, no name, no pins, no links
MinimalLinks // Minimal rectangle with pins and links (but no name)
};
// Base parameter node class
class ParameterNode : public NH_Object, public ax::NodeRendering::NodeRendererBase
{
public:
ParameterNode(int id, NH_CSTRING name, PinType type)
: NH_Object(id, name)
, m_Type(type)
, m_DisplayMode(ParameterDisplayMode::NameAndValue)
, m_IsSource(false)
, m_SourceID(0)
{
InitializeDefaultValue();
}
virtual ~ParameterNode() = default;
// Core properties
int GetID() const { return NH_Object::GetID(); }
NH_CSTRING GetName() const { return NH_Object::GetName(); }
void SetName(NH_CSTRING name) { NH_Object::SetName(name); }
PinType GetType() const { return m_Type; }
void SetType(PinType type) { m_Type = type; InitializeDefaultValue(); }
// Display mode
ParameterDisplayMode GetDisplayMode() const { return m_DisplayMode; }
void SetDisplayMode(ParameterDisplayMode mode) { m_DisplayMode = mode; }
void CycleDisplayMode();
// Source/Shortcut management
bool IsSource() const { return m_IsSource; }
void SetIsSource(bool isSource) { m_IsSource = isSource; }
int GetSourceID() const { return m_SourceID; }
void SetSourceID(int sourceID) { m_SourceID = sourceID; }
// Value access
virtual bool GetBool() const { return m_BoolValue; }
virtual int GetInt() const { return m_IntValue; }
virtual float GetFloat() const { return m_FloatValue; }
virtual const std::string& GetString() const { return m_StringValue; }
virtual void SetBool(bool v) { m_BoolValue = v; }
virtual void SetInt(int v) { m_IntValue = v; }
virtual void SetFloat(float v) { m_FloatValue = v; }
virtual void SetString(const std::string& v) { m_StringValue = v; }
// String representation for display
virtual std::string GetValueString() const;
// Execution: retrieve value from connected input pin
int Run(Node& node, App* app);
// Context menu hook - add custom menu items
void OnMenu(Node& node, App* app);
// Build the node structure (add to Example's node list)
void Build(Node& node, App* app);
// Render (different modes render differently)
void Render(Node& node, App* app, Pin* newLinkPin);
// State save/load callbacks (optional - can override to save/load custom state)
// These are called from App::SaveGraph() / App::LoadGraph()
virtual void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app);
virtual void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app);
// Shortcut creation (only callable on source nodes)
Node* CreateShortcut(Node& sourceNode, App* app);
// Helper to sync value to source (called from shortcut setters and edit dialog)
void SyncValueToSource(App* app);
// Helper to sync value from source to all shortcuts
void SyncValueToAllShortcuts(Node& node, App* app);
// Helper to sync name from shortcut to source
void SyncNameToSource(App* app);
// Helper to sync name from source to all shortcuts
void SyncNameToAllShortcuts(Node& node, App* app);
protected:
// Internal run with depth limit to prevent infinite recursion
int RunInternal(Node& node, App* app, int depth);
void InitializeDefaultValue();
// Rendering for each mode
void RenderNameOnly(Node& node, App* app, Pin* newLinkPin);
void RenderNameAndValue(Node& node, App* app, Pin* newLinkPin);
void RenderSmallBox(Node& node, App* app, Pin* newLinkPin);
void RenderMinimal(Node& node, App* app, Pin* newLinkPin);
void RenderMinimalLinks(Node& node, App* app, Pin* newLinkPin);
PinType m_Type;
ParameterDisplayMode m_DisplayMode;
// Source/Shortcut state
bool m_IsSource; // true if this node is a source node
int m_SourceID; // If > 0, this is a shortcut referencing source node with this ID
// Value storage
union {
bool m_BoolValue;
int m_IntValue;
float m_FloatValue;
};
std::string m_StringValue;
};
// Parameter registry for creating parameter nodes
class ParameterRegistry
{
public:
static ParameterRegistry& Instance()
{
static ParameterRegistry instance;
return instance;
}
ParameterNode* CreateParameter(PinType type, int id, NH_CSTRING name = nullptr);
private:
ParameterRegistry() = default;
};

View File

@ -0,0 +1,573 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include "parameter_operation.h"
#include "../app.h"
#include "../utilities/node_renderer_base.h"
#include "NodeEx.h"
#include <crude_json.h>
#include <imgui.h>
#include <imgui_node_editor.h>
namespace ed = ax::NodeEditor;
using namespace ax::NodeRendering;
//------------------------------------------------------------------------------
// Parameter Operation Registry
//------------------------------------------------------------------------------
ParameterOperationRegistry::ParameterOperationRegistry()
{
// Register some default operations (stubbed for now)
// Int operations
RegisterOperation(ParameterOperationDef("int_add", "Add", PinType::Int, PinType::Int, PinType::Int));
RegisterOperation(ParameterOperationDef("int_sub", "Subtract", PinType::Int, PinType::Int, PinType::Int));
RegisterOperation(ParameterOperationDef("int_mul", "Multiply", PinType::Int, PinType::Int, PinType::Int));
RegisterOperation(ParameterOperationDef("int_div", "Divide", PinType::Int, PinType::Int, PinType::Int));
// Float operations
RegisterOperation(ParameterOperationDef("float_add", "Add", PinType::Float, PinType::Float, PinType::Float));
RegisterOperation(ParameterOperationDef("float_sub", "Subtract", PinType::Float, PinType::Float, PinType::Float));
RegisterOperation(ParameterOperationDef("float_mul", "Multiply", PinType::Float, PinType::Float, PinType::Float));
RegisterOperation(ParameterOperationDef("float_div", "Divide", PinType::Float, PinType::Float, PinType::Float));
// String operations
RegisterOperation(ParameterOperationDef("string_concat", "Concatenate", PinType::String, PinType::String, PinType::String));
// Bool operations
RegisterOperation(ParameterOperationDef("bool_and", "AND", PinType::Bool, PinType::Bool, PinType::Bool));
RegisterOperation(ParameterOperationDef("bool_or", "OR", PinType::Bool, PinType::Bool, PinType::Bool));
// Comparison operations (Int -> Bool)
RegisterOperation(ParameterOperationDef("int_eq", "Equal", PinType::Int, PinType::Int, PinType::Bool));
RegisterOperation(ParameterOperationDef("int_lt", "Less Than", PinType::Int, PinType::Int, PinType::Bool));
RegisterOperation(ParameterOperationDef("int_gt", "Greater Than", PinType::Int, PinType::Int, PinType::Bool));
// Float comparison
RegisterOperation(ParameterOperationDef("float_eq", "Equal", PinType::Float, PinType::Float, PinType::Bool));
RegisterOperation(ParameterOperationDef("float_lt", "Less Than", PinType::Float, PinType::Float, PinType::Bool));
RegisterOperation(ParameterOperationDef("float_gt", "Greater Than", PinType::Float, PinType::Float, PinType::Bool));
}
void ParameterOperationRegistry::RegisterOperation(const ParameterOperationDef& opDef)
{
m_Operations.push_back(opDef);
}
std::vector<ParameterOperationDef> ParameterOperationRegistry::GetMatchingOperations(
PinType inputA, PinType inputB, PinType output)
{
std::vector<ParameterOperationDef> matching;
for (const auto& op : m_Operations)
{
if (op.InputAType == inputA && op.InputBType == inputB && op.OutputType == output)
{
matching.push_back(op);
}
}
return matching;
}
const ParameterOperationDef* ParameterOperationRegistry::GetOperation(const std::string& uuid)
{
for (const auto& op : m_Operations)
{
if (op.UUID == uuid)
return &op;
}
return nullptr;
}
//------------------------------------------------------------------------------
// Parameter Operation Block
//------------------------------------------------------------------------------
void ParameterOperationBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// Clear existing pins
m_InputParams.clear();
m_OutputParams.clear();
// Build 2 input parameters at top
AddInputParameter(app, node, "A", m_InputAType);
AddInputParameter(app, node, "B", m_InputBType);
// Build 1 output parameter at bottom
AddOutputParameter(app, node, "Result", m_OutputType);
}
int ParameterOperationBlock::Run(Node& node, App* app)
{
// Stub for now - will execute the selected operation
if (m_OperationUUID.empty())
{
printf("[ParamOp] No operation selected\n");
return E_OK;
}
auto* opDef = ParameterOperationRegistry::Instance().GetOperation(m_OperationUUID);
if (!opDef)
{
printf("[ParamOp] Operation '%s' not found\n", m_OperationUUID.c_str());
return E_OK;
}
printf("[ParamOp] Executing operation: %s (%s)\n", opDef->Label.c_str(), opDef->UUID.c_str());
// Get input values (stub - actual implementation would read from connected parameters)
// For now, just get default values
int paramIndex = 0;
int inputAInt = 0, inputBInt = 0;
float inputAFloat = 0.0f, inputBFloat = 0.0f;
bool inputABool = false, inputBBool = false;
std::string inputAString = "", inputBString = "";
// Read input values based on type
for (const auto& pin : node.Inputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
{
// Input A
switch (m_InputAType)
{
case PinType::Int: inputAInt = GetInputParamValueInt(pin, node, app, 0); break;
case PinType::Float: inputAFloat = GetInputParamValueFloat(pin, node, app, 0.0f); break;
case PinType::Bool: inputABool = GetInputParamValueBool(pin, node, app, false); break;
case PinType::String: inputAString = GetInputParamValueString(pin, node, app, ""); break;
default: break;
}
}
else if (paramIndex == 1)
{
// Input B
switch (m_InputBType)
{
case PinType::Int: inputBInt = GetInputParamValueInt(pin, node, app, 0); break;
case PinType::Float: inputBFloat = GetInputParamValueFloat(pin, node, app, 0.0f); break;
case PinType::Bool: inputBBool = GetInputParamValueBool(pin, node, app, false); break;
case PinType::String: inputBString = GetInputParamValueString(pin, node, app, ""); break;
default: break;
}
}
paramIndex++;
}
// Execute operation (stub implementations)
bool resultBool = false;
int resultInt = 0;
float resultFloat = 0.0f;
std::string resultString = "";
if (opDef->UUID == "int_add")
resultInt = inputAInt + inputBInt;
else if (opDef->UUID == "int_sub")
resultInt = inputAInt - inputBInt;
else if (opDef->UUID == "int_mul")
resultInt = inputAInt * inputBInt;
else if (opDef->UUID == "int_div")
resultInt = inputBInt != 0 ? inputAInt / inputBInt : 0;
else if (opDef->UUID == "float_add")
resultFloat = inputAFloat + inputBFloat;
else if (opDef->UUID == "float_sub")
resultFloat = inputAFloat - inputBFloat;
else if (opDef->UUID == "float_mul")
resultFloat = inputAFloat * inputBFloat;
else if (opDef->UUID == "float_div")
resultFloat = inputBFloat != 0.0f ? inputAFloat / inputBFloat : 0.0f;
else if (opDef->UUID == "string_concat")
resultString = inputAString + inputBString;
else if (opDef->UUID == "bool_and")
resultBool = inputABool && inputBBool;
else if (opDef->UUID == "bool_or")
resultBool = inputABool || inputBBool;
else if (opDef->UUID == "int_eq")
resultBool = (inputAInt == inputBInt);
else if (opDef->UUID == "int_lt")
resultBool = (inputAInt < inputBInt);
else if (opDef->UUID == "int_gt")
resultBool = (inputAInt > inputBInt);
else if (opDef->UUID == "float_eq")
resultBool = (inputAFloat == inputBFloat);
else if (opDef->UUID == "float_lt")
resultBool = (inputAFloat < inputBFloat);
else if (opDef->UUID == "float_gt")
resultBool = (inputAFloat > inputBFloat);
// Set output value
paramIndex = 0;
for (const auto& pin : node.Outputs)
{
if (pin.Type == PinType::Flow)
continue;
if (paramIndex == 0)
{
// Output
switch (m_OutputType)
{
case PinType::Int: SetOutputParamValueInt(pin, node, app, resultInt); break;
case PinType::Float: SetOutputParamValueFloat(pin, node, app, resultFloat); break;
case PinType::Bool: SetOutputParamValueBool(pin, node, app, resultBool); break;
case PinType::String: SetOutputParamValueString(pin, node, app, resultString); break;
default: break;
}
break;
}
paramIndex++;
}
return E_OK;
}
void ParameterOperationBlock::Render(Node& node, App* app, Pin* newLinkPin)
{
// Check if node is currently running (for red border visualization)
double currentTime = (double)ImGui::GetTime();
bool isRunning = false;
auto runningIt = app->m_RunningNodes.find(node.ID);
if (runningIt != app->m_RunningNodes.end())
{
isRunning = (currentTime < runningIt->second);
}
// Get styles from StyleManager - use ParameterStyle for visual distinction
auto& styleManager = app->GetStyleManager();
auto& paramStyle = styleManager.ParameterStyle;
// Parameter Operations get a slightly blue border for distinction
ImColor borderColor = isRunning ? paramStyle.BorderColorRunning : ImColor(180, 200, 255, 255); // Light blue
float activeBorderWidth = isRunning ? paramStyle.BorderWidthRunning : paramStyle.BorderWidth;
// Use NodeStyleScope with ParameterStyle for parameter-like appearance
NodeStyleScope style(
paramStyle.BgColor, // Parameter-style background (grayer)
borderColor, // border (red if running)
paramStyle.Rounding, activeBorderWidth, // More rounded than blocks
paramStyle.Padding, // padding
ImVec2(0.0f, 1.0f), // source direction (down)
ImVec2(0.0f, -1.0f) // target direction (up)
);
ed::BeginNode(node.ID);
ImGui::PushID(node.ID.AsPointer());
ImGui::BeginVertical("node");
// Center - header (simple centered text)
ImGui::BeginHorizontal("content");
ImGui::Spring(1, 0.0f);
ImGui::BeginVertical("center");
ImGui::Dummy(ImVec2(styleManager.MinNodeWidth, 0)); // Minimum width
ImGui::Spring(1);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
ImGui::TextUnformatted(node.Name.c_str());
ImGui::PopStyleColor();
ImGui::Spring(1);
ImGui::EndVertical();
ImGui::Spring(1, 0.0f);
ImGui::EndHorizontal();
ImGui::EndVertical();
// Save cursor and get node bounds
ImVec2 contentEndPos = ImGui::GetCursorScreenPos();
ImVec2 nodePos = ed::GetNodePosition(node.ID);
ImVec2 nodeSize = ed::GetNodeSize(node.ID);
if (nodeSize.x <= 0 || nodeSize.y <= 0)
{
ImVec2 contentMin = ImGui::GetItemRectMin();
ImVec2 contentMax = ImGui::GetItemRectMax();
nodeSize = contentMax - contentMin;
}
ImRect nodeRect = ImRect(nodePos, nodePos + nodeSize);
// Collect pins by type
std::vector<Pin*> inputParams;
std::vector<Pin*> outputParams;
for (auto& pin : node.Inputs)
{
if (pin.Type != PinType::Flow)
inputParams.push_back(&pin);
}
for (auto& pin : node.Outputs)
{
if (pin.Type != PinType::Flow)
outputParams.push_back(&pin);
}
// Render input parameters at top edge using NodeEx
if (!inputParams.empty())
{
float spacing = 1.0f / (inputParams.size() + 1);
for (size_t i = 0; i < inputParams.size(); ++i)
{
Pin* pin = inputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Input, ed::PinEdge::Top,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state);
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Min.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Render output parameters at bottom edge using NodeEx
if (!outputParams.empty())
{
float spacing = 1.0f / (outputParams.size() + 1);
for (size_t i = 0; i < outputParams.size(); ++i)
{
Pin* pin = outputParams[i];
float offset = spacing * (i + 1);
float alpha = GetPinAlpha(pin, newLinkPin, app);
ed::PinState state = (alpha < 1.0f) ? ed::PinState::Deactivated : ed::PinState::Normal;
ImRect pinRect = ed::PinEx(pin->ID, ed::PinKind::Output, ed::PinEdge::Bottom,
offset, styleManager.ParameterPinEdgeOffset, nodeRect, state);
pin->LastPivotPosition = ImVec2(pinRect.GetCenter().x, pinRect.Max.y);
pin->LastRenderBounds = pinRect;
pin->HasPositionData = true;
}
}
// Restore cursor
ImGui::SetCursorScreenPos(contentEndPos);
ImGui::PopID();
ed::EndNode();
}
void ParameterOperationBlock::OnMenu(Node& node, App* app)
{
// Call base class menu first
ParameterizedBlock::OnMenu(node, app);
ImGui::Separator();
ImGui::TextUnformatted("Parameter Operation");
// Show current operation
if (!m_OperationUUID.empty())
{
auto* opDef = ParameterOperationRegistry::Instance().GetOperation(m_OperationUUID);
if (opDef)
{
ImGui::Text("Operation: %s", opDef->Label.c_str());
}
else
{
ImGui::Text("Operation: (unknown)");
}
}
else
{
ImGui::Text("Operation: (none selected)");
}
ImGui::Separator();
// Type selection submenus
if (ImGui::BeginMenu("Set Input A Type"))
{
if (ImGui::MenuItem("Bool", nullptr, m_InputAType == PinType::Bool))
{
m_InputAType = PinType::Bool;
m_OperationUUID = ""; // Clear operation when type changes
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Int", nullptr, m_InputAType == PinType::Int))
{
m_InputAType = PinType::Int;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Float", nullptr, m_InputAType == PinType::Float))
{
m_InputAType = PinType::Float;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("String", nullptr, m_InputAType == PinType::String))
{
m_InputAType = PinType::String;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Set Input B Type"))
{
if (ImGui::MenuItem("Bool", nullptr, m_InputBType == PinType::Bool))
{
m_InputBType = PinType::Bool;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Int", nullptr, m_InputBType == PinType::Int))
{
m_InputBType = PinType::Int;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Float", nullptr, m_InputBType == PinType::Float))
{
m_InputBType = PinType::Float;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("String", nullptr, m_InputBType == PinType::String))
{
m_InputBType = PinType::String;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Set Output Type"))
{
if (ImGui::MenuItem("Bool", nullptr, m_OutputType == PinType::Bool))
{
m_OutputType = PinType::Bool;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Int", nullptr, m_OutputType == PinType::Int))
{
m_OutputType = PinType::Int;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("Float", nullptr, m_OutputType == PinType::Float))
{
m_OutputType = PinType::Float;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
if (ImGui::MenuItem("String", nullptr, m_OutputType == PinType::String))
{
m_OutputType = PinType::String;
m_OperationUUID = "";
node.Inputs.clear();
node.Outputs.clear();
Build(node, app);
}
ImGui::EndMenu();
}
ImGui::Separator();
// Operation selection based on current types
auto matchingOps = ParameterOperationRegistry::Instance().GetMatchingOperations(
m_InputAType, m_InputBType, m_OutputType);
if (!matchingOps.empty())
{
if (ImGui::BeginMenu("Select Operation"))
{
for (const auto& op : matchingOps)
{
bool isSelected = (m_OperationUUID == op.UUID);
if (ImGui::MenuItem(op.Label.c_str(), nullptr, isSelected))
{
m_OperationUUID = op.UUID;
// Update node name to reflect operation
node.Name = op.Label;
}
}
ImGui::EndMenu();
}
}
else
{
ImGui::TextDisabled("No operations available for current types");
}
}
void ParameterOperationBlock::SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app)
{
// Call base class
ParameterizedBlock::SaveState(node, nodeData, container, app);
// Save operation state
nodeData["op_input_a_type"] = (double)static_cast<int>(m_InputAType);
nodeData["op_input_b_type"] = (double)static_cast<int>(m_InputBType);
nodeData["op_output_type"] = (double)static_cast<int>(m_OutputType);
nodeData["op_uuid"] = m_OperationUUID;
}
void ParameterOperationBlock::LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app)
{
// Load operation state first (before base class, so pins are correct)
if (nodeData.contains("op_input_a_type"))
m_InputAType = static_cast<PinType>((int)nodeData["op_input_a_type"].get<double>());
else
m_InputAType = PinType::Int;
if (nodeData.contains("op_input_b_type"))
m_InputBType = static_cast<PinType>((int)nodeData["op_input_b_type"].get<double>());
else
m_InputBType = PinType::Int;
if (nodeData.contains("op_output_type"))
m_OutputType = static_cast<PinType>((int)nodeData["op_output_type"].get<double>());
else
m_OutputType = PinType::Int;
if (nodeData.contains("op_uuid"))
m_OperationUUID = nodeData["op_uuid"].get<crude_json::string>();
else
m_OperationUUID = "";
// Rebuild pins with loaded types
node.Inputs.clear();
node.Outputs.clear();
m_InputParams.clear();
m_OutputParams.clear();
Build(node, app);
// Call base class to load parameter values
ParameterizedBlock::LoadState(node, nodeData, container, app);
}
// Register block
REGISTER_BLOCK(ParameterOperationBlock, "ParamOp");

View File

@ -0,0 +1,91 @@
#pragma once
#include "block.h"
#include <string>
#include <vector>
// Operation definition for parameter operations
struct ParameterOperationDef
{
std::string UUID; // Unique identifier for the operation
std::string Label; // Display label (e.g., "Multiplication", "Addition")
PinType InputAType; // Type of first input
PinType InputBType; // Type of second input
PinType OutputType; // Type of output
ParameterOperationDef(const std::string& uuid, const std::string& label,
PinType inputA, PinType inputB, PinType output)
: UUID(uuid), Label(label), InputAType(inputA), InputBType(inputB), OutputType(output) {}
};
// Registry for parameter operations
class ParameterOperationRegistry
{
public:
static ParameterOperationRegistry& Instance()
{
static ParameterOperationRegistry instance;
return instance;
}
// Register a new operation
void RegisterOperation(const ParameterOperationDef& opDef);
// Get all operations matching input/output types
std::vector<ParameterOperationDef> GetMatchingOperations(PinType inputA, PinType inputB, PinType output);
// Get operation by UUID
const ParameterOperationDef* GetOperation(const std::string& uuid);
private:
ParameterOperationRegistry();
std::vector<ParameterOperationDef> m_Operations;
};
// Parameter Operation Block
class ParameterOperationBlock : public ParameterizedBlock
{
public:
ParameterOperationBlock(int id) : ParameterizedBlock(id, "ParamOp")
{
SetTypeName("ParamOp");
m_Type = NodeType::Blueprint;
m_Color = ImColor(180, 200, 180); // Light green
SetFlags(static_cast<NH_BEHAVIOR_FLAGS>(NHBEHAVIOR_SCRIPT));
// Default types
m_InputAType = PinType::Int;
m_InputBType = PinType::Int;
m_OutputType = PinType::Int;
m_OperationUUID = ""; // No operation selected by default
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
void Render(Node& node, App* app, Pin* newLinkPin) override;
NH_CSTRING GetBlockType() const override { return "ParamOp"; }
void OnMenu(Node& node, App* app) override;
// Save/load state
void SaveState(Node& node, crude_json::value& nodeData, const Container* container, App* app) override;
void LoadState(Node& node, const crude_json::value& nodeData, Container* container, App* app) override;
// Type management
void SetInputAType(PinType type) { m_InputAType = type; }
void SetInputBType(PinType type) { m_InputBType = type; }
void SetOutputType(PinType type) { m_OutputType = type; }
PinType GetInputAType() const { return m_InputAType; }
PinType GetInputBType() const { return m_InputBType; }
PinType GetOutputType() const { return m_OutputType; }
// Operation management
void SetOperation(const std::string& uuid) { m_OperationUUID = uuid; }
std::string GetOperationUUID() const { return m_OperationUUID; }
private:
PinType m_InputAType;
PinType m_InputBType;
PinType m_OutputType;
std::string m_OperationUUID;
};

View File

@ -0,0 +1,27 @@
#include "start_block.h"
#include "../app.h"
void StartBlock::Build(Node& node, App* app)
{
node.Type = m_Type;
node.Color = m_Color;
// No flow input - this is an entry point block
// Only has flow output "Start"
AddOutput(app, node, "Start");
}
int StartBlock::Run(Node& node, App* app)
{
// Entry point block - just activate the output to trigger connected blocks
// In GUI mode: Activate this block by selecting it and pressing 'R'
// In headless mode: Would need to find all Start blocks and activate them automatically
// The output will be activated by the runtime system (ExecuteRuntimeStep)
// after Run() returns, so we just need to return success
return E_OK;
}
// Register the Start block
REGISTER_BLOCK(StartBlock, "Start");

View File

@ -0,0 +1,21 @@
#pragma once
#include "block.h"
// Start block - entry point for graph execution
// Has no flow input, only a flow output
// Anything connected to its output will execute when the graph runs
class StartBlock : public ParameterizedBlock
{
public:
StartBlock(int id) : ParameterizedBlock(id, "Start")
{
SetTypeName("Start");
m_Type = NodeType::Blueprint;
m_Color = ImColor(100, 255, 100);
}
void Build(Node& node, App* app) override;
int Run(Node& node, App* app) override;
NH_CSTRING GetBlockType() const override { return "Start"; }
};

View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <set>
#include <functional>
#include "./enums.h"
#include "./utilities/uuid_generator.h"
#ifndef NH_CSTRING
#define NH_CSTRING const char*
#endif
typedef char* NH_STRING;
typedef char NH_CHAR;
typedef unsigned short* NH_USTRING;
typedef unsigned short NH_UCHAR;
typedef int NH_BOOL;
typedef unsigned char NH_BYTE;
typedef unsigned int NH_DWORD;
typedef unsigned short NH_WORD;
typedef int NH_ERROR;
typedef int NH_PARAMETER_TYPE;
typedef int NH_OPERATION_TYPE;
typedef int NH_MESSAGE_TYPE;
typedef int NH_ATTRIBUTE_TYPE;
typedef int NH_ATTRIBUTE_CATEGORY;
typedef Uuid64 NH_GUID;
typedef unsigned int NH_ID;
typedef unsigned int NH_CLASS_ID;

View File

@ -0,0 +1,8 @@
#ifndef CONFIG_H
#define CONFIG_H
#ifndef NH_GUI
#define NH_GUI
#endif
#endif

View File

@ -0,0 +1,475 @@
#include "container.h"
#include "../app.h"
#include "../types.h"
#include <algorithm>
Container::Container(int id, const char* name)
: m_ID(id), m_Name(name ? name : ""), m_Parent(nullptr), m_Flags(0), m_NextId(1)
{
// m_NextId initialized in member initializer list
}
Container::~Container()
{
// Clean up child containers (delete them before this container is destroyed)
for (Container* child : m_Children)
{
delete child;
}
m_Children.clear();
}
// Container flags/state
bool Container::IsHidden() const
{
return (m_Flags & ContainerFlag_Hidden) != 0;
}
void Container::SetHidden(bool hidden)
{
SetFlag(ContainerFlag_Hidden, hidden);
}
bool Container::IsActive() const
{
return !IsDisabled();
}
bool Container::IsDisabled() const
{
return (m_Flags & ContainerFlag_Disabled) != 0;
}
void Container::SetActive(bool active)
{
SetDisabled(!active);
}
void Container::SetDisabled(bool disabled)
{
SetFlag(ContainerFlag_Disabled, disabled);
}
bool Container::IsRunning() const
{
return (m_Flags & ContainerFlag_Running) != 0;
}
void Container::SetRunning(bool running)
{
SetFlag(ContainerFlag_Running, running);
}
bool Container::HasError() const
{
return (m_Flags & ContainerFlag_Error) != 0;
}
void Container::SetError(bool error)
{
SetFlag(ContainerFlag_Error, error);
}
void Container::SetFlag(uint32_t flag, bool value)
{
if (value)
m_Flags |= flag;
else
m_Flags &= ~flag;
}
// Management
void Container::AddNode(Node* node)
{
if (!node || ContainsNode(node->ID))
return;
// Store ID instead of pointer to avoid invalidation when m_Nodes vector reallocates
m_NodeIds.push_back(node->ID);
// DO NOT update deprecated m_Nodes vector - it contains invalidated pointers after reallocation
// All code should use GetNodes(App*) instead, which resolves IDs to fresh pointers
// For backward compatibility, we'll try to keep m_Nodes in sync, but it's unreliable
// TODO: Remove deprecated m_Nodes/m_Links vectors entirely
}
std::vector<Node*> Container::GetNodes(App* app) const
{
std::vector<Node*> result;
// For root containers, nodes are stored directly in RootContainer::m_Nodes
// For nested containers (groups), we need to resolve via root container
if (auto* rootContainer = GetRootContainer())
{
// Root container - get nodes directly
for (auto nodeId : m_NodeIds)
{
if (auto* node = rootContainer->FindNode(nodeId))
{
result.push_back(node);
}
}
}
else if (app)
{
// Nested container - resolve via App (which will search all root containers)
for (auto nodeId : m_NodeIds)
{
if (auto* node = app->FindNode(nodeId))
{
result.push_back(node);
}
}
}
return result;
}
std::vector<Link*> Container::GetLinks(App* app) const
{
std::vector<Link*> result;
// For root containers, links are stored directly in RootContainer::m_Links
// For nested containers (groups), we need to resolve via root container
if (auto* rootContainer = GetRootContainer())
{
// Root container - get links directly
for (auto linkId : m_LinkIds)
{
if (auto* link = rootContainer->FindLink(linkId))
{
result.push_back(link);
}
}
}
else if (app)
{
// Nested container - resolve via App (which will search all root containers)
for (auto linkId : m_LinkIds)
{
if (auto* link = app->FindLink(linkId))
{
result.push_back(link);
}
}
}
return result;
}
RootContainer* Container::GetRootContainer() const
{
// Walk up the parent chain to find root container
const Container* current = this;
while (current->m_Parent)
{
current = current->m_Parent;
}
// Cast to RootContainer (should be safe if this is a root or child of root)
return dynamic_cast<RootContainer*>(const_cast<Container*>(current));
}
void Container::RemoveNode(Node* node)
{
if (!node)
{
printf("[DELETE] Container::RemoveNode: Null node pointer\n");
fflush(stdout);
return;
}
int nodeId = -1;
try {
nodeId = ToRuntimeId(node->ID);
} catch (...) {
printf("[DELETE] Container::RemoveNode: Cannot access node ID (corrupted pointer %p)\n", (void*)node);
fflush(stdout);
return; // Cannot remove without valid ID
}
printf("[DELETE] Container::RemoveNode: Removing node %d (ptr=%p) from container\n",
nodeId, (void*)node);
fflush(stdout);
// Remove from ID list only - actual node is stored in RootContainer::m_Nodes
auto idIt = std::find(m_NodeIds.begin(), m_NodeIds.end(), node->ID);
if (idIt != m_NodeIds.end())
{
printf("[DELETE] Container::RemoveNode: Node %d found in ID list, erasing\n", nodeId);
fflush(stdout);
m_NodeIds.erase(idIt);
}
else
{
printf("[DELETE] Container::RemoveNode: Node %d NOT found in container ID list\n", nodeId);
fflush(stdout);
}
}
void Container::AddLink(Link* link)
{
if (!link)
{
printf("[LINK_DRAG] Container::AddLink: Null link pointer!\n");
fflush(stdout);
return;
}
const int linkId = ToRuntimeId(link->ID);
if (ContainsLink(link->ID))
{
printf("[LINK_DRAG] Container::AddLink: Link ID %lld already exists in container, REJECTING!\n",
static_cast<long long>(linkId));
printf("[LINK_DRAG] Container::AddLink: Current link IDs in container: ");
for (auto id : m_LinkIds)
printf("%lld ", (long long)id.Get());
printf("\n");
fflush(stdout);
return;
}
printf("[LINK_DRAG] Container::AddLink: Adding link ID=%lld to container\n", static_cast<long long>(linkId));
fflush(stdout);
// Store ID - actual link is stored in RootContainer::m_Links
m_LinkIds.push_back(link->ID);
printf("[LINK_DRAG] Container::AddLink: Successfully added. Container now has %zu link IDs\n", m_LinkIds.size());
fflush(stdout);
}
void Container::RemoveLink(Link* link)
{
if (!link)
return;
// Remove from ID list only - actual link is stored in RootContainer::m_Links
auto idIt = std::find(m_LinkIds.begin(), m_LinkIds.end(), link->ID);
if (idIt != m_LinkIds.end())
m_LinkIds.erase(idIt);
}
void Container::AddChildContainer(Container* container)
{
if (!container)
return;
// Check if already a child
auto it = std::find(m_Children.begin(), m_Children.end(), container);
if (it != m_Children.end())
return;
container->m_Parent = this;
m_Children.push_back(container);
}
void Container::RemoveChildContainer(Container* container)
{
if (!container)
return;
auto it = std::find(m_Children.begin(), m_Children.end(), container);
if (it != m_Children.end())
{
(*it)->m_Parent = nullptr;
m_Children.erase(it);
}
}
// Query
Node* Container::FindNode(ed::NodeId nodeId)
{
// Check if this container owns this node ID
if (std::find(m_NodeIds.begin(), m_NodeIds.end(), nodeId) == m_NodeIds.end())
{
// Not in this container, check children
for (auto* child : m_Children)
{
if (auto* node = child->FindNode(nodeId))
return node;
}
return nullptr;
}
// This container owns the node ID - resolve via root container
if (auto* rootContainer = GetRootContainer())
{
return rootContainer->FindNode(nodeId);
}
// No root container found - caller should use App::FindNode() directly
return nullptr;
}
Link* Container::FindLink(ed::LinkId linkId)
{
// Check if this container owns this link ID
bool ownsLink = (std::find(m_LinkIds.begin(), m_LinkIds.end(), linkId) != m_LinkIds.end());
if (ownsLink)
{
// This container owns the link ID - resolve via root container
if (auto* rootContainer = GetRootContainer())
{
return rootContainer->FindLink(linkId);
}
// No root container found - caller should use App::FindLink() directly
return nullptr;
}
// Not in this container, recursively search in child containers
for (auto* child : m_Children)
{
if (auto* link = child->FindLink(linkId))
return link;
}
return nullptr;
}
Pin* Container::FindPin(ed::PinId pinId)
{
if (!pinId)
return nullptr;
// Note: Without App*, we cannot resolve node IDs to pointers
// This method is deprecated - use FindPin(pinId, App*) instead
// For now, return nullptr
// Recursively search in child containers
for (auto* child : m_Children)
{
if (auto* pin = child->FindPin(pinId))
return pin;
}
return nullptr;
}
Pin* Container::FindPin(ed::PinId pinId, App* app)
{
if (!pinId || !app)
return nullptr;
// Use GetNodes() to properly resolve IDs to pointers
auto nodes = GetNodes(app);
for (auto* node : nodes)
{
if (!node)
continue;
// Validate node pointer isn't corrupted
uintptr_t nodePtrValue = reinterpret_cast<uintptr_t>(node);
if (nodePtrValue < 0x1000 || nodePtrValue == 0xFFFFFFFFFFFFFFFFULL)
continue;
for (auto& pin : node->Inputs)
{
if (pin.ID == pinId)
{
// Ensure pin's Node pointer is correct (should be set by BuildNode after AddNode)
if (pin.Node != node)
{
pin.Node = node; // Fix to point to actual node
}
return &pin;
}
}
for (auto& pin : node->Outputs)
{
if (pin.ID == pinId)
{
// Ensure pin's Node pointer is correct (should be set by BuildNode after AddNode)
if (pin.Node != node)
{
pin.Node = node; // Fix to point to actual node
}
return &pin;
}
}
}
// Recursively search in child containers
for (auto* child : m_Children)
{
if (auto* pin = child->FindPin(pinId, app))
return pin;
}
return nullptr;
}
Container* Container::FindContainer(ed::NodeId nodeId)
{
// Check if node is in this container
if (FindNode(nodeId))
return this;
// Recursively search children
for (auto* child : m_Children)
{
if (auto* found = child->FindContainer(nodeId))
return found;
}
return nullptr;
}
bool Container::ContainsNode(ed::NodeId nodeId) const
{
// Check ID list
return std::find(m_NodeIds.begin(), m_NodeIds.end(), nodeId) != m_NodeIds.end();
}
bool Container::ContainsLink(ed::LinkId linkId) const
{
// Check ID list
return std::find(m_LinkIds.begin(), m_LinkIds.end(), linkId) != m_LinkIds.end();
}
bool Container::CanAddLink(Link* link) const
{
if (!link)
return false;
// Note: This method is const and cannot resolve node IDs to pointers without App*
// The proper implementation would need App* to resolve pins to nodes
// For now, we'll check if we can find the link by ID (basic validation)
// Actual validation should be done at a higher level with App* available
// Both pins must belong to nodes in this container
// Without App*, we can't resolve pins to nodes, so we return true
// (The caller should validate with proper context)
return true;
}
// Execution (stub - overridden in derived classes)
void Container::Run(App* app)
{
// Base implementation does nothing
// Overridden in RootContainer and BehaviorGraph
}
// Rendering (stub - overridden in derived classes)
void Container::Render(App* app, Pin* newLinkPin)
{
// Base implementation does nothing
// Overridden in RootContainer and BehaviorGraph
}
// Serialization (stub - overridden in derived classes)
void Container::Serialize(crude_json::value& json) const
{
// Base implementation does nothing
// Overridden in RootContainer and BehaviorGraph
}
void Container::Deserialize(const crude_json::value& json, App* app)
{
// Base implementation does nothing
// Overridden in RootContainer and BehaviorGraph
}

View File

@ -0,0 +1,115 @@
#pragma once
#include "../types.h"
#include <string>
#include <vector>
#include <map>
namespace ed = ax::NodeEditor;
class App;
struct Link;
struct Pin;
// Forward declaration for crude_json
namespace crude_json {
struct value;
}
class Container
{
public:
// Identification
ed::NodeId GetID() const { return m_ID; }
const std::string& GetName() const { return m_Name; }
void SetName(const std::string& name) { m_Name = name; }
// ID generation - container provides unique IDs for its nodes
// This ensures IDs are never reused even if nodes are deleted
int GetNextId() { return m_NextId++; }
int GetCurrentId() const { return m_NextId; } // For checking what next ID will be
// Container flags/state
bool IsHidden() const;
void SetHidden(bool hidden);
bool IsActive() const; // Not disabled
bool IsDisabled() const;
void SetActive(bool active);
void SetDisabled(bool disabled);
bool IsRunning() const;
void SetRunning(bool running);
bool HasError() const;
void SetError(bool error);
// Ownership
// NOTE: Store IDs instead of pointers to avoid invalidation when m_Nodes vector reallocates
std::vector<ed::NodeId> m_NodeIds; // Node IDs owned by this container (look up via App::FindNode)
std::vector<ed::LinkId> m_LinkIds; // Link IDs owned by this container (look up via App::FindLink)
// Convenience: Get actual node pointers (does lookup via App)
// Pass App* to resolve IDs to pointers
std::vector<Node*> GetNodes(App* app) const;
std::vector<Link*> GetLinks(App* app) const;
// NOTE: Nodes and Links are owned by RootContainer, not stored here
// Container only stores IDs (m_NodeIds, m_LinkIds) and resolves via GetRootContainer()
std::vector<Container*> m_Children; // Nested containers (groups)
Container* m_Parent; // Parent container (nullptr for root containers)
// Management
virtual void AddNode(Node* node);
virtual void RemoveNode(Node* node);
virtual void AddLink(Link* link);
virtual void RemoveLink(Link* link);
virtual void AddChildContainer(Container* container);
virtual void RemoveChildContainer(Container* container);
// Query
Node* FindNode(ed::NodeId nodeId);
Link* FindLink(ed::LinkId linkId); // DEPRECATED: Uses deprecated m_Links
Link* FindLink(ed::LinkId linkId, App* app); // Preferred: Uses App::FindLink() to resolve IDs
Pin* FindPin(ed::PinId pinId); // DEPRECATED: Uses deprecated m_Nodes
Pin* FindPin(ed::PinId pinId, App* app); // Preferred: Uses GetNodes() to resolve IDs
Container* FindContainer(ed::NodeId nodeId); // Recursive search (this + children)
bool ContainsNode(ed::NodeId nodeId) const;
bool ContainsLink(ed::LinkId linkId) const;
// Helper to get root container (walks up parent chain)
class RootContainer* GetRootContainer() const;
// Execution
virtual void Run(App* app); // Execute container contents (only if active)
// Rendering
virtual void Render(App* app, Pin* newLinkPin); // Only if not hidden
// Serialization
virtual void Serialize(struct crude_json::value& json) const;
virtual void Deserialize(const struct crude_json::value& json, App* app);
// Validation
virtual bool CanAddNode(Node* node) const { return true; }
virtual bool CanAddLink(Link* link) const; // Check if link is valid for this container (both pins must be in this container)
// State management
uint32_t GetFlags() const { return m_Flags; }
void SetFlag(uint32_t flag, bool value);
bool HasFlag(uint32_t flag) const { return (m_Flags & flag) != 0; }
protected:
Container(int id, const char* name);
virtual ~Container();
ed::NodeId m_ID;
std::string m_Name;
int m_NextId = 1; // Container-local ID generator (ensures no ID reuse)
// Container flags
enum ContainerFlags {
ContainerFlag_Hidden = 1 << 0,
ContainerFlag_Disabled = 1 << 1,
ContainerFlag_Running = 1 << 2,
ContainerFlag_Error = 1 << 3
};
uint32_t m_Flags = 0; // Bitmask of ContainerFlags
};

View File

@ -0,0 +1,285 @@
#include "root_container.h"
#include "../app.h"
#include "../types.h"
#include "../blocks/block.h"
#include "../blocks/parameter_node.h"
#include <imgui_node_editor.h>
namespace ed = ax::NodeEditor;
RootContainer::RootContainer(const std::string& filename, int id)
: Container(id, "Root"), m_Filename(filename), m_IsDirty(false)
{
}
RootContainer::~RootContainer()
{
// Clean up BlockInstance and ParameterInstance for all nodes BEFORE maps are destroyed
// This prevents memory leaks and potential double-free issues
// NOTE: Nodes may have already had their instances deleted via DeleteNodeAndInstances,
// so we check for nullptr and validate pointers to avoid double-free
for (auto& pair : m_Nodes)
{
Node& node = pair.second;
// Clean up BlockInstance (for block-based nodes)
// Only delete if pointer is valid and not already deleted
if (node.BlockInstance)
{
// Validate pointer is not corrupted or already freed
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(node.BlockInstance);
if (ptrValue != 0xFFFFFFFFFFFFFFFFULL && ptrValue > 0x1000) // Basic sanity check
{
try {
delete node.BlockInstance;
} catch (...) {
// Silently ignore exceptions during shutdown cleanup
}
}
node.BlockInstance = nullptr;
}
// Clean up ParameterInstance (for parameter nodes)
// Only delete if pointer is valid and not already deleted
if (node.ParameterInstance)
{
// Validate pointer is not corrupted or already freed
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(node.ParameterInstance);
if (ptrValue != 0xFFFFFFFFFFFFFFFFULL && ptrValue > 0x1000) // Basic sanity check
{
try {
delete node.ParameterInstance;
} catch (...) {
// Silently ignore exceptions during shutdown cleanup
}
}
node.ParameterInstance = nullptr;
}
}
// Clear maps explicitly (though they'll be destroyed automatically after this)
// This makes the order of destruction clear
m_Nodes.clear();
m_Links.clear();
// Base Container destructor will clean up child containers
}
Node* RootContainer::FindNode(ed::NodeId id)
{
if (!id)
return nullptr;
auto it = m_Nodes.find(id);
if (it != m_Nodes.end())
return &it->second;
return nullptr;
}
Link* RootContainer::FindLink(ed::LinkId id)
{
if (!id)
return nullptr;
auto it = m_Links.find(id);
if (it != m_Links.end())
return &it->second;
return nullptr;
}
Pin* RootContainer::FindPin(ed::PinId id, App* app)
{
if (!id)
return nullptr;
// Search all nodes in this container
for (auto& pair : m_Nodes)
{
Node& node = pair.second;
// Validate node pointer isn't corrupted before accessing pins
uintptr_t nodePtrValue = reinterpret_cast<uintptr_t>(&node);
if (nodePtrValue < 0x1000 || nodePtrValue == 0xFFFFFFFFFFFFFFFFULL)
continue;
// Check inputs
for (auto& pin : node.Inputs)
{
if (pin.ID == id)
{
// Ensure pin's Node pointer is correct (should be set by BuildNode after AddNode)
if (pin.Node != &node)
{
pin.Node = &node; // Fix to point to actual node in container
}
return &pin;
}
}
// Check outputs
for (auto& pin : node.Outputs)
{
if (pin.ID == id)
{
// Ensure pin's Node pointer is correct (should be set by BuildNode after AddNode)
if (pin.Node != &node)
{
printf("[FindPin] FIXING pin %d - Node pointer incorrect (was %p, setting to %p)\n",
id.Get(), (void*)pin.Node, (void*)&node);
fflush(stdout);
pin.Node = &node; // Fix to point to actual node in container
}
return &pin;
}
}
}
// Also search in child containers recursively
for (Container* child : m_Children)
{
if (Pin* pin = child->FindPin(id, app))
return pin;
}
return nullptr;
}
std::vector<Node*> RootContainer::GetAllNodes() const
{
std::vector<Node*> nodes;
nodes.reserve(m_Nodes.size());
for (auto& pair : m_Nodes)
{
nodes.push_back(const_cast<Node*>(&pair.second)); // Const cast needed
}
return nodes;
}
std::vector<Link*> RootContainer::GetAllLinks() const
{
std::vector<Link*> links;
links.reserve(m_Links.size());
for (auto& pair : m_Links)
{
links.push_back(const_cast<Link*>(&pair.second)); // Const cast needed
}
return links;
}
Node* RootContainer::AddNode(const Node& node)
{
auto [it, inserted] = m_Nodes.emplace(node.ID, node);
if (!inserted)
return nullptr; // Node with this ID already exists
// Also add to container's ID list
m_NodeIds.push_back(node.ID);
return &it->second;
}
bool RootContainer::RemoveNode(ed::NodeId id)
{
auto it = m_Nodes.find(id);
if (it != m_Nodes.end())
{
m_Nodes.erase(it);
// Remove from ID list
auto idIt = std::find(m_NodeIds.begin(), m_NodeIds.end(), id);
if (idIt != m_NodeIds.end())
m_NodeIds.erase(idIt);
return true;
}
return false;
}
Link* RootContainer::AddLink(const Link& link)
{
auto [it, inserted] = m_Links.emplace(link.ID, link);
if (!inserted)
return nullptr; // Link with this ID already exists
// Also add to container's ID list
m_LinkIds.push_back(link.ID);
return &it->second;
}
bool RootContainer::RemoveLink(ed::LinkId id)
{
auto it = m_Links.find(id);
if (it != m_Links.end())
{
m_Links.erase(it);
// Remove from ID list
auto idIt = std::find(m_LinkIds.begin(), m_LinkIds.end(), id);
if (idIt != m_LinkIds.end())
m_LinkIds.erase(idIt);
return true;
}
return false;
}
void RootContainer::Run(App* app)
{
// Only execute if active (not disabled)
if (IsDisabled())
return;
// Set running flag
SetRunning(true);
// Execute blocks in this container
// Note: The actual execution logic is still in App::ExecuteRuntimeStep()
// which will iterate through m_Nodes. We'll update that to use the container.
// For now, this is a placeholder.
// Execute child containers (groups) - only if active
for (auto* child : m_Children)
{
if (child->IsDisabled())
continue;
// Child containers (groups) will be executed by runtime system
// when their inputs are activated
}
// Clear running flag
SetRunning(false);
}
void RootContainer::Render(App* app, Pin* newLinkPin)
{
// Only render if not hidden
if (IsHidden())
return;
// Render this container's nodes
// Note: Actual rendering is still in App::RenderNodes()
// which will iterate through m_Nodes. We'll update that to use the container.
// For now, this is a placeholder.
// Render links between nodes in this container
// Note: Actual link rendering is still in App::RenderLinks()
// We'll update that to use the container.
// Render child containers (groups) - only if not hidden
for (auto* child : m_Children)
{
if (child->IsHidden())
continue;
// Child containers (groups) will render themselves
child->Render(app, newLinkPin);
}
}

View File

@ -0,0 +1,57 @@
#pragma once
#include "container.h"
#include "../types.h"
#include <string>
#include <map>
namespace ed = ax::NodeEditor;
class App;
struct Pin;
/**
* RootContainer - Owns actual Node and Link objects for its graph
*
* Each root container (one per file) owns all nodes and links in its graph.
* This provides proper ownership and separation between different graph files.
*/
class RootContainer : public Container
{
public:
RootContainer(const std::string& filename, int id);
virtual ~RootContainer();
// Root-specific
const std::string& GetFilename() const { return m_Filename; }
void SetFilename(const std::string& filename) { m_Filename = filename; }
bool IsDirty() const { return m_IsDirty; }
void SetDirty(bool dirty) { m_IsDirty = dirty; }
// Node/Link ownership - RootContainer owns the actual objects
std::map<ed::NodeId, Node, NodeIdLess> m_Nodes; // Node storage indexed by ID
std::map<ed::LinkId, Link, LinkIdLess> m_Links; // Link storage indexed by ID
// Node/Link lookup (search this container only)
Node* FindNode(ed::NodeId id);
Link* FindLink(ed::LinkId id);
Pin* FindPin(ed::PinId id, App* app); // App* needed for nested container lookup
// Direct access to all nodes/links (for iteration)
std::vector<Node*> GetAllNodes() const;
std::vector<Link*> GetAllLinks() const;
// Node/Link management
Node* AddNode(const Node& node);
bool RemoveNode(ed::NodeId id);
Link* AddLink(const Link& link);
bool RemoveLink(ed::LinkId id);
// Root has no interface pins
void Run(App* app) override; // Execute all blocks in root (only if active)
void Render(App* app, Pin* newLinkPin) override; // Render root + children (only if not hidden)
private:
std::string m_Filename;
bool m_IsDirty = false;
};

View File

@ -0,0 +1 @@
#include "BaseManager.h"

Some files were not shown because too many files have changed in this diff Show More