From 14fb78e8a5e3e5131a1509400ad2aebe596d49f7 Mon Sep 17 00:00:00 2001 From: Luca Heltai Date: Sun, 11 Jan 2026 02:43:12 +0100 Subject: [PATCH] GUI node editor + repo layout refactor - Split project into core/backends/gui/tools - Add coral_dump_registry CLI tool - Add Dear ImGui based node editor - Add Dealii backend plugin - Create gui/editor, gui/manipulator subprojects - Move gtests into core/tests and backend specific tests into respective backend folders --- .github/workflows/tests.yml | 24 +- .gitignore | 1 + .gitmodules | 5 +- CMakeLists.txt | 180 +- README.md | 173 + backends/dealii/CMakeLists.txt | 61 + .../dealii/include}/register_types.h | 3 + .../dealii/src/backend_main.cc | 0 backends/dealii/src/plugin_dealii.cc | 15 + backends/dealii/tests/CMakeLists.txt | 66 + .../dealii/tests}/dealii_examples.cc | 0 .../dealii/tests}/dealii_types.cc | 0 {gtests => backends/dealii/tests}/failing.cc | 0 {gtests => backends/dealii/tests}/main.cc | 0 {gtests => backends/dealii/tests}/modules.cc | 0 {gtests => backends/dealii/tests}/network.cc | 0 .../dealii/tests}/node_object_tests.cc | 0 backends/dealii/tests/plugin_smoke_test.cc | 79 + backends/dealii/tests/serialize.cc | 66 + backends/dealii/tests/trivial_types.cc | 27 + core/CMakeLists.txt | 40 + entt => core/entt | 0 {include => core/include}/CLI11/CLI11.hpp | 0 {include => core/include}/coral.h | 0 .../include}/coral_implementation.h | 0 {include => core/include}/coral_network.h | 0 .../include}/coral_network_implementation.h | 0 core/include/coral_plugin.h | 20 + .../include}/magic_enum/magic_enum.hpp | 0 .../include}/magic_enum/magic_enum_all.hpp | 0 .../magic_enum/magic_enum_containers.hpp | 0 .../include}/magic_enum/magic_enum_flags.hpp | 0 .../include}/magic_enum/magic_enum_format.hpp | 0 .../include}/magic_enum/magic_enum_fuse.hpp | 0 .../magic_enum/magic_enum_iostream.hpp | 0 .../include}/magic_enum/magic_enum_switch.hpp | 0 .../magic_enum/magic_enum_utility.hpp | 0 {include => core/include}/nlohmann/json.hpp | 0 core/include/slog.h | 3 + {include => core/include}/slog/slog.h | 0 .../include}/taskflow/algorithm/algorithm.hpp | 0 .../taskflow/algorithm/data_pipeline.hpp | 0 .../include}/taskflow/algorithm/find.hpp | 0 .../include}/taskflow/algorithm/for_each.hpp | 0 .../include}/taskflow/algorithm/module.hpp | 0 .../taskflow/algorithm/partitioner.hpp | 0 .../include}/taskflow/algorithm/pipeline.hpp | 0 .../include}/taskflow/algorithm/reduce.hpp | 0 .../include}/taskflow/algorithm/scan.hpp | 0 .../include}/taskflow/algorithm/sort.hpp | 0 .../include}/taskflow/algorithm/transform.hpp | 0 .../include}/taskflow/core/async.hpp | 0 .../include}/taskflow/core/async_task.hpp | 0 .../taskflow/core/atomic_notifier.hpp | 0 .../include}/taskflow/core/declarations.hpp | 0 .../include}/taskflow/core/environment.hpp | 0 .../include}/taskflow/core/error.hpp | 0 .../include}/taskflow/core/executor.hpp | 0 .../include}/taskflow/core/flow_builder.hpp | 0 .../include}/taskflow/core/freelist.hpp | 0 .../include}/taskflow/core/graph.hpp | 0 .../taskflow/core/nonblocking_notifier.hpp | 0 .../include}/taskflow/core/observer.hpp | 0 .../include}/taskflow/core/runtime.hpp | 0 .../include}/taskflow/core/semaphore.hpp | 0 .../include}/taskflow/core/task.hpp | 0 .../include}/taskflow/core/taskflow.hpp | 0 .../include}/taskflow/core/topology.hpp | 0 .../include}/taskflow/core/tsq.hpp | 0 .../include}/taskflow/core/worker.hpp | 0 .../include}/taskflow/cuda/algorithm/find.hpp | 0 .../taskflow/cuda/algorithm/for_each.hpp | 0 .../taskflow/cuda/algorithm/matmul.hpp | 0 .../taskflow/cuda/algorithm/merge.hpp | 0 .../taskflow/cuda/algorithm/reduce.hpp | 0 .../include}/taskflow/cuda/algorithm/scan.hpp | 0 .../taskflow/cuda/algorithm/single_task.hpp | 0 .../include}/taskflow/cuda/algorithm/sort.hpp | 0 .../taskflow/cuda/algorithm/transform.hpp | 0 .../taskflow/cuda/algorithm/transpose.hpp | 0 .../include}/taskflow/cuda/cuda_capturer.hpp | 0 .../include}/taskflow/cuda/cuda_device.hpp | 0 .../include}/taskflow/cuda/cuda_error.hpp | 0 .../taskflow/cuda/cuda_execution_policy.hpp | 0 .../include}/taskflow/cuda/cuda_graph.hpp | 0 .../taskflow/cuda/cuda_graph_exec.hpp | 0 .../include}/taskflow/cuda/cuda_memory.hpp | 0 .../include}/taskflow/cuda/cuda_meta.hpp | 0 .../include}/taskflow/cuda/cuda_optimizer.hpp | 0 .../include}/taskflow/cuda/cuda_stream.hpp | 0 .../include}/taskflow/cuda/cudaflow.hpp | 0 .../include}/taskflow/dsl/connection.hpp | 0 .../include}/taskflow/dsl/dsl.hpp | 0 .../include}/taskflow/dsl/meta_macro.hpp | 0 .../include}/taskflow/dsl/task_analyzer.hpp | 0 .../include}/taskflow/dsl/task_dsl.hpp | 0 .../include}/taskflow/dsl/task_trait.hpp | 0 .../include}/taskflow/dsl/tuple_utils.hpp | 0 .../include}/taskflow/dsl/type_list.hpp | 0 .../taskflow/sycl/algorithm/reduce.hpp | 0 .../taskflow/sycl/algorithm/sycl_for_each.hpp | 0 .../sycl/algorithm/sycl_transform.hpp | 0 .../taskflow/sycl/sycl_execution_policy.hpp | 0 .../include}/taskflow/sycl/sycl_graph.hpp | 0 .../include}/taskflow/sycl/sycl_meta.hpp | 0 .../include}/taskflow/sycl/sycl_task.hpp | 0 .../include}/taskflow/sycl/syclflow.hpp | 0 .../include}/taskflow/taskflow.hpp | 0 .../include}/taskflow/utility/iterator.hpp | 0 .../include}/taskflow/utility/latch.hpp | 0 .../include}/taskflow/utility/lazy_string.hpp | 0 .../include}/taskflow/utility/macros.hpp | 0 .../include}/taskflow/utility/math.hpp | 0 .../include}/taskflow/utility/mpmc.hpp | 0 .../include}/taskflow/utility/object_pool.hpp | 0 .../include}/taskflow/utility/os.hpp | 0 .../include}/taskflow/utility/serializer.hpp | 0 .../include}/taskflow/utility/singleton.hpp | 0 .../taskflow/utility/small_vector.hpp | 0 .../include}/taskflow/utility/stream.hpp | 0 .../include}/taskflow/utility/traits.hpp | 0 .../include}/taskflow/utility/uuid.hpp | 0 core/include/type_name.h | 122 + {include => core/include}/utils.h | 0 .../source}/coral_implementation.cc | 0 .../source}/coral_network_implementation.cc | 0 {source => core/source}/node_object.cc | 0 {source => core/source}/slog/slog.c | 0 core/tests/CMakeLists.txt | 26 + .../tests/connection_type.cc | 0 {gtests => core/tests}/serialize.cc | 77 +- {gtests => core/tests}/trivial_types.cc | 29 +- gtests/CMakeLists.txt | 43 - gui/CMakeLists.txt | 21 + gui/editor/CMakeLists.txt | 105 + gui/editor/imgui-node-editor | 1 + .../include/coral_editor/node_editor_types.h | 183 + gui/editor/source/node_editor_main.cc | 3625 +++++++++++++++++ gui/manipulator/CMakeLists.txt | 114 + .../imgui/misc/cpp/imgui_stdlib.cpp | 117 + gui/manipulator/imgui/misc/cpp/imgui_stdlib.h | 37 + .../include/coral_manipulator/manipulator.h | 41 + gui/manipulator/source/main.cpp | 135 + gui/manipulator/source/manipulator.cpp | 2457 +++++++++++ include/type_name.h | 1487 ------- test_files/mwe_imgui.json | 144 + tools/CMakeLists.txt | 17 + tools/src/coral_dump_registry.cc | 113 + 148 files changed, 7868 insertions(+), 1789 deletions(-) create mode 100644 backends/dealii/CMakeLists.txt rename {include => backends/dealii/include}/register_types.h (98%) rename source/main.cc => backends/dealii/src/backend_main.cc (100%) create mode 100644 backends/dealii/src/plugin_dealii.cc create mode 100644 backends/dealii/tests/CMakeLists.txt rename {gtests => backends/dealii/tests}/dealii_examples.cc (100%) rename {gtests => backends/dealii/tests}/dealii_types.cc (100%) rename {gtests => backends/dealii/tests}/failing.cc (100%) rename {gtests => backends/dealii/tests}/main.cc (100%) rename {gtests => backends/dealii/tests}/modules.cc (100%) rename {gtests => backends/dealii/tests}/network.cc (100%) rename {gtests => backends/dealii/tests}/node_object_tests.cc (100%) create mode 100644 backends/dealii/tests/plugin_smoke_test.cc create mode 100644 backends/dealii/tests/serialize.cc create mode 100644 backends/dealii/tests/trivial_types.cc create mode 100644 core/CMakeLists.txt rename entt => core/entt (100%) rename {include => core/include}/CLI11/CLI11.hpp (100%) rename {include => core/include}/coral.h (100%) rename {include => core/include}/coral_implementation.h (100%) rename {include => core/include}/coral_network.h (100%) rename {include => core/include}/coral_network_implementation.h (100%) create mode 100644 core/include/coral_plugin.h rename {include => core/include}/magic_enum/magic_enum.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_all.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_containers.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_flags.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_format.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_fuse.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_iostream.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_switch.hpp (100%) rename {include => core/include}/magic_enum/magic_enum_utility.hpp (100%) rename {include => core/include}/nlohmann/json.hpp (100%) create mode 100644 core/include/slog.h rename {include => core/include}/slog/slog.h (100%) rename {include => core/include}/taskflow/algorithm/algorithm.hpp (100%) rename {include => core/include}/taskflow/algorithm/data_pipeline.hpp (100%) rename {include => core/include}/taskflow/algorithm/find.hpp (100%) rename {include => core/include}/taskflow/algorithm/for_each.hpp (100%) rename {include => core/include}/taskflow/algorithm/module.hpp (100%) rename {include => core/include}/taskflow/algorithm/partitioner.hpp (100%) rename {include => core/include}/taskflow/algorithm/pipeline.hpp (100%) rename {include => core/include}/taskflow/algorithm/reduce.hpp (100%) rename {include => core/include}/taskflow/algorithm/scan.hpp (100%) rename {include => core/include}/taskflow/algorithm/sort.hpp (100%) rename {include => core/include}/taskflow/algorithm/transform.hpp (100%) rename {include => core/include}/taskflow/core/async.hpp (100%) rename {include => core/include}/taskflow/core/async_task.hpp (100%) rename {include => core/include}/taskflow/core/atomic_notifier.hpp (100%) rename {include => core/include}/taskflow/core/declarations.hpp (100%) rename {include => core/include}/taskflow/core/environment.hpp (100%) rename {include => core/include}/taskflow/core/error.hpp (100%) rename {include => core/include}/taskflow/core/executor.hpp (100%) rename {include => core/include}/taskflow/core/flow_builder.hpp (100%) rename {include => core/include}/taskflow/core/freelist.hpp (100%) rename {include => core/include}/taskflow/core/graph.hpp (100%) rename {include => core/include}/taskflow/core/nonblocking_notifier.hpp (100%) rename {include => core/include}/taskflow/core/observer.hpp (100%) rename {include => core/include}/taskflow/core/runtime.hpp (100%) rename {include => core/include}/taskflow/core/semaphore.hpp (100%) rename {include => core/include}/taskflow/core/task.hpp (100%) rename {include => core/include}/taskflow/core/taskflow.hpp (100%) rename {include => core/include}/taskflow/core/topology.hpp (100%) rename {include => core/include}/taskflow/core/tsq.hpp (100%) rename {include => core/include}/taskflow/core/worker.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/find.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/for_each.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/matmul.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/merge.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/reduce.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/scan.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/single_task.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/sort.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/transform.hpp (100%) rename {include => core/include}/taskflow/cuda/algorithm/transpose.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_capturer.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_device.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_error.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_execution_policy.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_graph.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_graph_exec.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_memory.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_meta.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_optimizer.hpp (100%) rename {include => core/include}/taskflow/cuda/cuda_stream.hpp (100%) rename {include => core/include}/taskflow/cuda/cudaflow.hpp (100%) rename {include => core/include}/taskflow/dsl/connection.hpp (100%) rename {include => core/include}/taskflow/dsl/dsl.hpp (100%) rename {include => core/include}/taskflow/dsl/meta_macro.hpp (100%) rename {include => core/include}/taskflow/dsl/task_analyzer.hpp (100%) rename {include => core/include}/taskflow/dsl/task_dsl.hpp (100%) rename {include => core/include}/taskflow/dsl/task_trait.hpp (100%) rename {include => core/include}/taskflow/dsl/tuple_utils.hpp (100%) rename {include => core/include}/taskflow/dsl/type_list.hpp (100%) rename {include => core/include}/taskflow/sycl/algorithm/reduce.hpp (100%) rename {include => core/include}/taskflow/sycl/algorithm/sycl_for_each.hpp (100%) rename {include => core/include}/taskflow/sycl/algorithm/sycl_transform.hpp (100%) rename {include => core/include}/taskflow/sycl/sycl_execution_policy.hpp (100%) rename {include => core/include}/taskflow/sycl/sycl_graph.hpp (100%) rename {include => core/include}/taskflow/sycl/sycl_meta.hpp (100%) rename {include => core/include}/taskflow/sycl/sycl_task.hpp (100%) rename {include => core/include}/taskflow/sycl/syclflow.hpp (100%) rename {include => core/include}/taskflow/taskflow.hpp (100%) rename {include => core/include}/taskflow/utility/iterator.hpp (100%) rename {include => core/include}/taskflow/utility/latch.hpp (100%) rename {include => core/include}/taskflow/utility/lazy_string.hpp (100%) rename {include => core/include}/taskflow/utility/macros.hpp (100%) rename {include => core/include}/taskflow/utility/math.hpp (100%) rename {include => core/include}/taskflow/utility/mpmc.hpp (100%) rename {include => core/include}/taskflow/utility/object_pool.hpp (100%) rename {include => core/include}/taskflow/utility/os.hpp (100%) rename {include => core/include}/taskflow/utility/serializer.hpp (100%) rename {include => core/include}/taskflow/utility/singleton.hpp (100%) rename {include => core/include}/taskflow/utility/small_vector.hpp (100%) rename {include => core/include}/taskflow/utility/stream.hpp (100%) rename {include => core/include}/taskflow/utility/traits.hpp (100%) rename {include => core/include}/taskflow/utility/uuid.hpp (100%) create mode 100644 core/include/type_name.h rename {include => core/include}/utils.h (100%) rename {source => core/source}/coral_implementation.cc (100%) rename {source => core/source}/coral_network_implementation.cc (100%) rename {source => core/source}/node_object.cc (100%) rename {source => core/source}/slog/slog.c (100%) create mode 100644 core/tests/CMakeLists.txt rename gtests/node_types.cc => core/tests/connection_type.cc (100%) rename {gtests => core/tests}/serialize.cc (56%) rename {gtests => core/tests}/trivial_types.cc (68%) delete mode 100644 gtests/CMakeLists.txt create mode 100644 gui/CMakeLists.txt create mode 100644 gui/editor/CMakeLists.txt create mode 160000 gui/editor/imgui-node-editor create mode 100644 gui/editor/include/coral_editor/node_editor_types.h create mode 100644 gui/editor/source/node_editor_main.cc create mode 100644 gui/manipulator/CMakeLists.txt create mode 100644 gui/manipulator/imgui/misc/cpp/imgui_stdlib.cpp create mode 100644 gui/manipulator/imgui/misc/cpp/imgui_stdlib.h create mode 100644 gui/manipulator/include/coral_manipulator/manipulator.h create mode 100644 gui/manipulator/source/main.cpp create mode 100644 gui/manipulator/source/manipulator.cpp delete mode 100644 include/type_name.h create mode 100644 test_files/mwe_imgui.json create mode 100644 tools/CMakeLists.txt create mode 100644 tools/src/coral_dump_registry.cc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba45d67..14c9cb0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,17 +20,21 @@ jobs: - name: Test debug run: | rm -rf build_linux_debug - mkdir build_linux_debug - cd build_linux_debug - cmake .. -GNinja -DCMAKE_BUILD_TYPE=Debug - ninja - ./gtests/gtests.g + cmake -S . -B build_linux_debug -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCORAL_BUILD_GUI=OFF + ninja -C build_linux_debug + ninja -C build_linux_debug run_coral_core_tests + ninja -C build_linux_debug run_dealii_backend_tests + ninja -C build_linux_debug run_dealii_plugin_tests - name: Test release run: | rm -rf build_linux_release - mkdir build_linux_release - cd build_linux_release - cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release - ninja - ./gtests/gtests + cmake -S . -B build_linux_release -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCORAL_BUILD_GUI=OFF + ninja -C build_linux_release + ninja -C build_linux_release run_coral_core_tests + ninja -C build_linux_release run_dealii_backend_tests + ninja -C build_linux_release run_dealii_plugin_tests diff --git a/.gitignore b/.gitignore index f6c99b7..01b00e4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ doc/html doc/latex .aider* +.vscode .vscode/ diff --git a/.gitmodules b/.gitmodules index cb7be75..baa1cc5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "entt"] - path = entt + path = core/entt url = https://github.com/skypjack/entt.git +[submodule "imgui-node-editor"] + path = gui/editor/imgui-node-editor + url = https://github.com/thedmd/imgui-node-editor.git diff --git a/CMakeLists.txt b/CMakeLists.txt index bcd43ec..e78dfe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,157 +1,43 @@ -## --------------------------------------------------------------------- -## -## Copyright (C) 2000 2020 by the deal.II authors -## -## This file is part of the deal.II library. -## -## The deal.II library is free software; you can use it, redistribute -## it, and/or modify it under the terms of the GNU Lesser General -## Public License as published by the Free Software Foundation; either -## version 2.1 of the License, or (at your option) any later version. -## The full text of the license can be found in the file LICENSE.md at -## the top level directory of deal.II. -## -## --------------------------------------------------------------------- +cmake_minimum_required(VERSION 3.16) -## -# CMake script for large project, with one executable -# and a library of objects -# -# The structure of the directory is assumed to be: -# ./source: containing implementations and one file with "int main()" -# ./include: containing all class declarations -# ./gtests: containing gtest unit tests -## +project(coral LANGUAGES C CXX) -# Set the name of the project and target -# If your application follows the structure above, you don't need to -# specify anything else. -SET(_main_target dealii_backend) -SET(_main source/main.cc) +option(CORAL_BUILD_TOOLS "Build small CLI tools." ON) +option(CORAL_BUILD_GUI "Build GUI applications." ON) +option(CORAL_BUILD_BACKEND_DEALII "Build the deal.II backend (plugin + CLI)." ON) +option(CORAL_BUILD_TESTS "Build tests (backend-specific where applicable)." ON) +option(CORAL_BUILD_SHARED_CORE "Build coral_core as a shared library (recommended for plugins)." ON) -# Set the _main variable to empty if you don't want an executable -# but only a library -#SET(_main "") +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -############################################################ -# Normally you shouldn't need to change anything below. -############################################################ -# Declare all source files the target consists of: +if(CORAL_BUILD_BACKEND_DEALII AND NOT CORAL_BUILD_SHARED_CORE) + message(WARNING "CORAL_BUILD_BACKEND_DEALII=ON with CORAL_BUILD_SHARED_CORE=OFF may prevent plugins from sharing the type registry with the host.") +endif() -# INCLUDE_DIRECTORIES(include ./) +if(CORAL_BUILD_TESTS) + # We intentionally run GoogleTest executables directly (faster than ctest). + # Do not call enable_testing() so CMake doesn't generate its own `test` target. + add_custom_target(test) +endif() -# Disable specific warnings related to the nlohmann/json library -IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - ADD_COMPILE_OPTIONS(-Wno-array-bounds -Wno-maybe-uninitialized -Wno-class-memaccess) -ELSEIF(CMAKE_CXX_COMPILER_ID MATCHES "Clang") - ADD_COMPILE_OPTIONS(-Wno-array-bounds -Wno-uninitialized -Wno-self-assign-overloaded) -ENDIF() +add_subdirectory(core) -CMAKE_MINIMUM_REQUIRED(VERSION 3.10) +if(CORAL_BUILD_TOOLS) + add_subdirectory(tools) +endif() -FIND_PACKAGE(deal.II 9.5 REQUIRED -HINTS ${deal.II_DIR} ${DEAL_II_DIR} ../ ../../ $ENV{DEAL_II_DIR}) -DEAL_II_INITIALIZE_CACHED_VARIABLES() +if(CORAL_BUILD_BACKEND_DEALII) + add_subdirectory(backends/dealii) +endif() +if(CORAL_BUILD_GUI) + add_subdirectory(gui) +endif() -PROJECT(${_main_target}) -DEAL_II_INITIALIZE_CACHED_VARIABLES() -OPTION(ENABLE_GOOGLE_TESTING -"Enable googletest style tests in this application." ON) -OPTION(CORAL_HEADER_ONLY "Build CORAL as a header-only library." OFF) - -# We add one library and one target for each type of deal.II library -# we found. If you compiled deal.II with both Release and Debug -# mode, this will generate both Release and Debug programs for you. -# The debug library and program are postfixed with ".g" -SET(_d2_build_types "Release;Debug") -SET(Release_postfix "") -SET(Debug_postfix ".g") - -FOREACH(_build_type ${_d2_build_types}) - # Postfix to use everywhere - SET(_p "${${_build_type}_postfix}") - # Only build this type, if deal.II was compiled with it. - IF(CMAKE_BUILD_TYPE MATCHES "${_build_type}" AND - DEAL_II_BUILD_TYPE MATCHES "${_build_type}") - - MESSAGE("-- Found ${_build_type} version of deal.II.") - - STRING(TOUPPER "${_build_type}" _BUILD_TYPE) - - # Setup slog (C library) - SET(_slog_lib "slog${_p}") - if (NOT TARGET ${_slog_lib}) - ADD_LIBRARY(${_slog_lib} STATIC source/slog/slog.c) - TARGET_INCLUDE_DIRECTORIES( - ${_slog_lib} PUBLIC ./include ./include/slog) - # libdealii_backend_lib is a shared library; ensure slog can be linked into it. - set_target_properties(${_slog_lib} PROPERTIES POSITION_INDEPENDENT_CODE ON) - endif() - - # Setup library - SET(_lib "${_main_target}_lib${_p}") - MESSAGE("-- Configuring library ${_lib}") - - if (CORAL_HEADER_ONLY) - ADD_LIBRARY(${_lib} INTERFACE) - TARGET_INCLUDE_DIRECTORIES( - ${_lib} INTERFACE ./include ./include/slog ${CMAKE_CURRENT_SOURCE_DIR}/entt/single_include) - TARGET_LINK_LIBRARIES(${_lib} INTERFACE ${_slog_lib}) - TARGET_COMPILE_DEFINITIONS( - ${_lib} INTERFACE SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" - CORAL_HEADER_ONLY=1 - JSON_DIAGNOSTICS=1) - else() - ADD_LIBRARY(${_lib} SHARED - source/coral_implementation.cc - source/coral_network_implementation.cc) - TARGET_INCLUDE_DIRECTORIES( - ${_lib} PUBLIC ./include ./include/slog ${CMAKE_CURRENT_SOURCE_DIR}/entt/single_include) - TARGET_LINK_LIBRARIES(${_lib} ${_slog_lib}) - TARGET_COMPILE_DEFINITIONS( - ${_lib} PUBLIC SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" - CORAL_HEADER_ONLY=0 - JSON_DIAGNOSTICS=1) - DEAL_II_SETUP_TARGET(${_lib} ${_BUILD_TYPE}) - endif() - SET(TEST_LIBRARIES_${_BUILD_TYPE} ${_lib}) - - # Setup one executable per dimension/per build type - # FOREACH(_dim 1 2 3) - # SET(_exe "${_main_target}_${_dim}d${${_build_type}_postfix}") - # MESSAGE("-- Configuring executable ${_exe}") - # ADD_EXECUTABLE(${_exe} ${_main}) - # TARGET_LINK_LIBRARIES(${_exe} ${_lib}) - # TARGET_COMPILE_DEFINITIONS(${_exe} PUBLIC DEAL_DIMENSION=${_dim}) - # DEAL_II_SETUP_TARGET(${_exe} ${_BUILD_TYPE}) - # ENDFOREACH() - - # Setup one executable per build type - SET(_exe "${_main_target}${${_build_type}_postfix}") - MESSAGE("-- Configuring executable ${_exe}") - ADD_EXECUTABLE(${_exe} ${_main}) - TARGET_LINK_LIBRARIES(${_exe} ${_lib}) - DEAL_II_SETUP_TARGET(${_exe} ${_BUILD_TYPE}) - ENDIF() -ENDFOREACH() - -# Google test style testing -IF(ENABLE_GOOGLE_TESTING) - find_package(GTest) - if (GTest_FOUND) - ADD_SUBDIRECTORY(gtests) - else () - MESSAGE(WARNING "GTest library not found. Tests will not be compiled!") - endif () -ENDIF() - -find_package(Doxygen) - -if(DOXYGEN_FOUND) - add_custom_target(doc - ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating API documentation with Doxygen" VERBATIM) - MESSAGE("-- Configured Doxygen") -endif(DOXYGEN_FOUND) +if(CORAL_BUILD_TESTS) + add_subdirectory(core/tests) + if(TARGET test AND TARGET run_coral_core_tests) + add_dependencies(test run_coral_core_tests) + endif() +endif() diff --git a/README.md b/README.md index 13f2510..f0f1f13 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,179 @@ include: - **register_method**: For member functions (void/non-void, const/non-const) - **register_function**: For free functions +## Repository Layout + +The codebase is intentionally split so the **core graph library** can be built +without any particular “backend” (e.g. deal.II), and UIs can be built without +linking backend types. Backends provide types at runtime via a small plugin ABI +and/or by dumping a `registry.json` for UI authoring. + +### Top-level directories + +- `core/` + - `core/include/`: CORAL public headers (NodeObject, Network, JSON, etc.) + - `core/src/`: CORAL implementation + - `core/include/coral_plugin.h`: minimal C ABI that backend plugins export +- `backends/` + - `backends/dealii/`: one backend implementation (deal.II) + - `backends/dealii/src/backend_main.cc`: backend CLI (`dealii_backend`) + - `backends/dealii/src/plugin_dealii.cc`: backend plugin (`coral_backend_dealii`) + - `backends/dealii/include/register_types.h`: deal.II type registration + - `backends/dealii/tests/`: backend-specific gtests +- `gui/` + - `gui/editor/`: node editor application (registry-driven, backend-agnostic) + - `gui/imgui-node-editor/`: `imgui-node-editor` submodule lives under `gui/` +- `tools/` + - `tools/src/coral_dump_registry.cc`: helper to dump a registry from a plugin + +## Build System (CMake) + +The top-level `CMakeLists.txt` composes independent subprojects: + +- `coral_core` (always): the core library under `core/` +- `coral_node_editor`: the editor UI under `gui/editor/` +- `coral_backend_dealii` + `dealii_backend`: deal.II backend plugin + CLI under `backends/dealii/` +- `coral_dump_registry`: tool under `tools/` + +### Options + +These options are enabled by default: + +- `CORAL_BUILD_GUI=ON` +- `CORAL_BUILD_BACKEND_DEALII=ON` (auto-skips if `deal.II` is not found) +- `CORAL_BUILD_TESTS=ON` +- `CORAL_BUILD_TOOLS=ON` + +Example: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j 8 +``` + +### Warning-free builds + +External libraries (and translation units that include a lot of external +headers, such as deal.II-heavy code) are built with warnings disabled +to keep output clean by default. + +## Backend Plugins and Registries + +CORAL “registration” is backend-specific: a backend is responsible for calling +`coral::NodeObject::register_*` for its own node types. + +Two workflows are supported: + +### 1) Editor workflow (backend-agnostic) + +The node editor does **not** link to a backend. It consumes a `registry.json` +and uses it to validate/author networks. + +Backend authors generate the registry by registering types in their backend and +dumping the registry to JSON. + +### 2) Runtime workflow (backend plugin) + +Backends can also be loaded dynamically at runtime via a minimal plugin ABI +defined in `core/include/coral_plugin.h`: + +- `coral_backend_register_types()` registers backend node types into CORAL +- `coral_backend_name()` returns a backend identifier + +The deal.II backend implements this in `backends/dealii/src/plugin_dealii.cc`. + +Any host application can load the shared library (platform `dlopen`/`LoadLibrary`) +and call `coral_backend_register_types()` before constructing/executing networks. +The `coral_dump_registry` tool is a minimal example of a plugin host. + +### Creating a new plugin + +To add a new backend plugin (e.g. `my_backend`), create a new subdirectory and +provide two pieces: (1) a normal C++ registration function that calls +`coral::NodeObject::register_*`, and (2) a shared library that exports the CORAL +plugin ABI from `core/include/coral_plugin.h`. + +Suggested layout: + +- `backends/my_backend/include/register_types.h` (declares `void register_types();`) +- `backends/my_backend/src/register_types.cc` (implements `register_types()`) +- `backends/my_backend/src/plugin_my_backend.cc` (exports the plugin ABI) +- `backends/my_backend/CMakeLists.txt` (builds the shared library target) + +Minimal plugin entry points (`backends/my_backend/src/plugin_my_backend.cc`): + +```cpp +#include "coral_plugin.h" +#include "register_types.h" + +CORAL_PLUGIN_EXPORT void +coral_backend_register_types() +{ + register_types(); +} + +CORAL_PLUGIN_EXPORT const char * +coral_backend_name() +{ + return "my_backend"; +} +``` + +CMake should build a shared library and link it against `coral_core` plus any +backend dependencies: + +- `add_library(coral_backend_my_backend SHARED ...)` +- `target_link_libraries(coral_backend_my_backend PRIVATE coral_core ...)` + +### Dump a registry from a plugin + +Use `coral_dump_registry` (built under `tools/`) to load a plugin and write the +registry JSON: + +```bash +./build/tools/coral_dump_registry ./build/backends/dealii/libcoral_backend_dealii.(dylib|so|dll) registry.json +``` + +### Using a registry in the editor + +The editor consumes a registry JSON file at runtime. By default, +`coral_node_editor` looks for `node_types.json` in the current working +directory (you can also change the path in the UI). + +Example: + +```bash +./build/tools/coral_dump_registry ./build/backends/dealii/libcoral_backend_dealii.(dylib|so|dll) node_types.json +./build/gui/editor/coral_node_editor +``` + +## Tests + +Core tests live in `core/tests/`. Backend-specific tests live next to the backend +(`backends/dealii/tests/`). +Instead of using `ctest`, the build provides a fast path to run the gtest +executable directly: + +```bash +cmake --build build --target run_dealii_backend_tests +``` + +Run core tests: + +```bash +cmake --build build --target run_coral_core_tests +``` + +Or run the binary yourself: + +```bash +./build/backends/dealii/tests/dealii_backend_tests +``` + +```bash +./build/core/tests/coral_core_tests +``` + ## Execution Model The execution model is based on the Taskflow library: diff --git a/backends/dealii/CMakeLists.txt b/backends/dealii/CMakeLists.txt new file mode 100644 index 0000000..f21d57b --- /dev/null +++ b/backends/dealii/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(deal.II 9.5 QUIET + HINTS ${deal.II_DIR} ${DEAL_II_DIR} ../../ $ENV{DEAL_II_DIR}) + +if(NOT deal.II_FOUND AND NOT DEAL_II_FOUND) + message(WARNING "deal.II not found; skipping backends/dealii (set CORAL_BUILD_BACKEND_DEALII=OFF to silence)") + return() +endif() + +deal_ii_initialize_cached_variables() + +add_library(coral_backend_dealii SHARED + src/plugin_dealii.cc) + +target_compile_features(coral_backend_dealii PRIVATE cxx_std_17) + +target_include_directories(coral_backend_dealii + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/include) + +target_link_libraries(coral_backend_dealii + coral_core) + +deal_ii_setup_target(coral_backend_dealii) + +if(MSVC) + target_compile_options(coral_backend_dealii PRIVATE /W0) +else() + target_compile_options(coral_backend_dealii PRIVATE -w) +endif() + +add_executable(dealii_backend + src/backend_main.cc) + +target_compile_features(dealii_backend PRIVATE cxx_std_17) + +target_include_directories(dealii_backend + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/include) + +target_link_libraries(dealii_backend + coral_core) + +deal_ii_setup_target(dealii_backend) + +if(MSVC) + target_compile_options(dealii_backend PRIVATE /W0) +else() + target_compile_options(dealii_backend PRIVATE -w) +endif() + +if(CORAL_BUILD_TESTS) + add_subdirectory(tests) + if(TARGET test AND TARGET run_dealii_backend_tests) + add_dependencies(test run_dealii_backend_tests) + endif() + if(TARGET test AND TARGET run_dealii_plugin_tests) + add_dependencies(test run_dealii_plugin_tests) + endif() +endif() diff --git a/include/register_types.h b/backends/dealii/include/register_types.h similarity index 98% rename from include/register_types.h rename to backends/dealii/include/register_types.h index b80732e..9cda2cb 100644 --- a/include/register_types.h +++ b/backends/dealii/include/register_types.h @@ -13,6 +13,9 @@ #include +#include +#include + #include "coral.h" #include "coral_network.h" diff --git a/source/main.cc b/backends/dealii/src/backend_main.cc similarity index 100% rename from source/main.cc rename to backends/dealii/src/backend_main.cc diff --git a/backends/dealii/src/plugin_dealii.cc b/backends/dealii/src/plugin_dealii.cc new file mode 100644 index 0000000..e2ef471 --- /dev/null +++ b/backends/dealii/src/plugin_dealii.cc @@ -0,0 +1,15 @@ +#include "coral_plugin.h" + +#include "register_types.h" + +CORAL_PLUGIN_EXPORT void +coral_backend_register_types() +{ + coral::register_all_types(); +} + +CORAL_PLUGIN_EXPORT const char * +coral_backend_name() +{ + return "dealii"; +} diff --git a/backends/dealii/tests/CMakeLists.txt b/backends/dealii/tests/CMakeLists.txt new file mode 100644 index 0000000..cdafd84 --- /dev/null +++ b/backends/dealii/tests/CMakeLists.txt @@ -0,0 +1,66 @@ +find_package(GTest) +if(NOT GTest_FOUND) + message(WARNING "GTest not found; skipping deal.II backend tests") + return() +endif() + +file(GLOB _test_files CONFIGURE_DEPENDS *.cc) + +include(GoogleTest) + +# Keep plugin smoke test in its own executable so it doesn't get "help" from +# backend tests that register types directly. +set(_plugin_test_file ${CMAKE_CURRENT_LIST_DIR}/plugin_smoke_test.cc) +list(REMOVE_ITEM _test_files ${_plugin_test_file}) + +add_executable(dealii_backend_tests ${_test_files}) + +target_include_directories(dealii_backend_tests + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../include) + +target_link_libraries(dealii_backend_tests + coral_core + ${GTEST_LIBRARIES} + ${GTEST_MAIN_LIBRARIES}) + +if(GTEST_INCLUDE_DIRS) + target_include_directories(dealii_backend_tests SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) +endif() + +deal_ii_setup_target(dealii_backend_tests) + +if(MSVC) + target_compile_options(dealii_backend_tests PRIVATE /W0) +else() + target_compile_options(dealii_backend_tests PRIVATE -w) +endif() + +# Prefer running the gtest binary directly (faster iteration than ctest). +add_custom_target(run_dealii_backend_tests + COMMAND $ + DEPENDS dealii_backend_tests + USES_TERMINAL) + +add_executable(dealii_plugin_tests ${_plugin_test_file}) +target_link_libraries(dealii_plugin_tests coral_core ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES}) +if(GTEST_INCLUDE_DIRS) + target_include_directories(dealii_plugin_tests SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) +endif() + +target_compile_definitions(dealii_plugin_tests PRIVATE + CORAL_TEST_PLUGIN_PATH="$") + +# Ensure the plugin exists before we try to load it. +add_dependencies(dealii_plugin_tests coral_backend_dealii) + +if(MSVC) + target_compile_options(dealii_plugin_tests PRIVATE /W0) +else() + target_compile_options(dealii_plugin_tests PRIVATE -w) +endif() + +add_custom_target(run_dealii_plugin_tests + COMMAND $ + DEPENDS dealii_plugin_tests coral_backend_dealii + USES_TERMINAL) diff --git a/gtests/dealii_examples.cc b/backends/dealii/tests/dealii_examples.cc similarity index 100% rename from gtests/dealii_examples.cc rename to backends/dealii/tests/dealii_examples.cc diff --git a/gtests/dealii_types.cc b/backends/dealii/tests/dealii_types.cc similarity index 100% rename from gtests/dealii_types.cc rename to backends/dealii/tests/dealii_types.cc diff --git a/gtests/failing.cc b/backends/dealii/tests/failing.cc similarity index 100% rename from gtests/failing.cc rename to backends/dealii/tests/failing.cc diff --git a/gtests/main.cc b/backends/dealii/tests/main.cc similarity index 100% rename from gtests/main.cc rename to backends/dealii/tests/main.cc diff --git a/gtests/modules.cc b/backends/dealii/tests/modules.cc similarity index 100% rename from gtests/modules.cc rename to backends/dealii/tests/modules.cc diff --git a/gtests/network.cc b/backends/dealii/tests/network.cc similarity index 100% rename from gtests/network.cc rename to backends/dealii/tests/network.cc diff --git a/gtests/node_object_tests.cc b/backends/dealii/tests/node_object_tests.cc similarity index 100% rename from gtests/node_object_tests.cc rename to backends/dealii/tests/node_object_tests.cc diff --git a/backends/dealii/tests/plugin_smoke_test.cc b/backends/dealii/tests/plugin_smoke_test.cc new file mode 100644 index 0000000..21f5a2d --- /dev/null +++ b/backends/dealii/tests/plugin_smoke_test.cc @@ -0,0 +1,79 @@ +#include + +#include "coral.h" +#include "coral_plugin.h" + +#if defined(_WIN32) +# include +#else +# include +#endif + +namespace +{ + using RegisterFn = void (*)(); + using NameFn = const char *(*)(); + + struct PluginHandle + { +#if defined(_WIN32) + HMODULE handle = nullptr; +#else + void *handle = nullptr; +#endif + }; + + auto load_plugin(const char *path) -> PluginHandle + { + PluginHandle h; +#if defined(_WIN32) + h.handle = LoadLibraryA(path); + if (!h.handle) + throw std::runtime_error("LoadLibrary failed."); +#else + h.handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!h.handle) + throw std::runtime_error(std::string("dlopen failed: ") + dlerror()); +#endif + return h; + } + + template + auto load_symbol(const PluginHandle &h, const char *name) -> Fn + { +#if defined(_WIN32) + auto *sym = GetProcAddress(h.handle, name); + if (!sym) + throw std::runtime_error(std::string("GetProcAddress failed for: ") + name); + return reinterpret_cast(sym); +#else + dlerror(); + void *sym = dlsym(h.handle, name); + const char *err = dlerror(); + if (err) + throw std::runtime_error(std::string("dlsym failed: ") + err); + return reinterpret_cast(sym); +#endif + } +} // namespace + +TEST(Plugin, DealiiRegistersTypes) +{ +#ifndef CORAL_TEST_PLUGIN_PATH + GTEST_SKIP() << "CORAL_TEST_PLUGIN_PATH not provided by build."; +#else + const auto before = coral::NodeObject::get_registry().size(); + + PluginHandle plugin = load_plugin(CORAL_TEST_PLUGIN_PATH); + auto reg = load_symbol(plugin, "coral_backend_register_types"); + auto name = load_symbol(plugin, "coral_backend_name"); + + ASSERT_STREQ(name(), "dealii"); + reg(); + + const auto after = coral::NodeObject::get_registry().size(); + EXPECT_GT(after, before); + EXPECT_GT(after, 10u); +#endif +} + diff --git a/backends/dealii/tests/serialize.cc b/backends/dealii/tests/serialize.cc new file mode 100644 index 0000000..519cd84 --- /dev/null +++ b/backends/dealii/tests/serialize.cc @@ -0,0 +1,66 @@ +#include + +#include + +#include + +#include "coral.h" +#include "register_types.h" + +using namespace dealii; +using namespace coral; + +TEST(Serialize, Point) +{ + using type = Point<2>; + NodeObject::register_elementary_type(); + NodeObjectPtr obj = make_node(type(0.0, 1.0)); + ASSERT_TRUE(obj->ready()); + ASSERT_EQ(type(0.0, 1.0), obj->get()); + ASSERT_EQ("[0.0,1.0]", obj->to_string()); + + // Serialize to json + json j = obj; + ASSERT_TRUE(j["node_type"] == "elementary_constructor"); + ASSERT_TRUE(j["value"] == "[0.0,1.0]"); + + // Deserialize from json + auto obj2 = j.get(); + + ASSERT_EQ(obj->hash(), obj2->hash()); + ASSERT_TRUE(obj2->ready()); + ASSERT_EQ(obj->get(), obj2->get()); + + // Make sure that after executing the object, it still holds the same value + (*obj2)(); + ASSERT_EQ("[0.0,1.0]", obj2->to_string()); +} + + +TEST(Serialize, Triangulation) +{ + using type = Triangulation<2>; + NodeObject::register_type(); + NodeObjectPtr obj = make_node(); + (*obj)(); + + ASSERT_TRUE(obj->ready()); + + // Serialize to json + json j = obj; + ASSERT_TRUE(j["node_type"] == "empty_constructor"); + + // Deserialize from json. This will call the constructor. + auto obj2 = j.get(); + + ASSERT_EQ(obj->hash(), obj2->hash()); + ASSERT_TRUE(obj2->ready()); + + // The two objects are actually different objects in memory. + ASSERT_NE(&obj->get(), &obj2->get()); + + // Dump the json to a file + std::ofstream ofs("triangulation.json"); + ofs << j.dump(2); + ofs.close(); +} diff --git a/backends/dealii/tests/trivial_types.cc b/backends/dealii/tests/trivial_types.cc new file mode 100644 index 0000000..dec15d4 --- /dev/null +++ b/backends/dealii/tests/trivial_types.cc @@ -0,0 +1,27 @@ +#include + +#include "coral.h" +#include "register_types.h" + +using namespace dealii; +using namespace coral; + +TEST(TrivialTypes, Point) +{ + using type = Point<2>; + // Now we can use register_elementary_type since we have JSON serialization + // for Point + NodeObject::register_elementary_type(); + NodeObject obj = type(1.0, 2.0); + ASSERT_EQ(type(1.0, 2.0), obj.get()); + const auto &j = obj.get_info(); + std::string s = j.at("type"); + ASSERT_EQ(s, coral::detail::hash()); + + // Verify JSON serialization is working correctly + ASSERT_TRUE(j.contains("value")); + auto json_value = json::parse(j.at("value").get()); + ASSERT_EQ(json_value.size(), 2); + ASSERT_EQ(json_value[0], 1.0); + ASSERT_EQ(json_value[1], 2.0); +} diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..f284290 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,40 @@ +if(CORAL_BUILD_SHARED_CORE) + set(_coral_core_library_type SHARED) +else() + set(_coral_core_library_type STATIC) +endif() + +add_library(coral_core ${_coral_core_library_type} + source/coral_implementation.cc + source/coral_network_implementation.cc + source/node_object.cc + source/slog/slog.c) + +set_target_properties(coral_core PROPERTIES POSITION_INDEPENDENT_CODE ON) + +target_compile_features(coral_core PUBLIC cxx_std_17) + +target_include_directories(coral_core + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include + ${CMAKE_CURRENT_LIST_DIR}/entt/single_include) + +target_compile_definitions(coral_core + PUBLIC + SOURCE_DIR="${PROJECT_SOURCE_DIR}" + JSON_DIAGNOSTICS=1) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(coral_core PRIVATE + -Wno-array-bounds -Wno-maybe-uninitialized -Wno-class-memaccess) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(coral_core PRIVATE + -Wno-array-bounds -Wno-uninitialized -Wno-self-assign-overloaded) +endif() + +# Keep build output warning-free by default. +if(MSVC) + target_compile_options(coral_core PRIVATE /W0) +else() + target_compile_options(coral_core PRIVATE -w) +endif() diff --git a/entt b/core/entt similarity index 100% rename from entt rename to core/entt diff --git a/include/CLI11/CLI11.hpp b/core/include/CLI11/CLI11.hpp similarity index 100% rename from include/CLI11/CLI11.hpp rename to core/include/CLI11/CLI11.hpp diff --git a/include/coral.h b/core/include/coral.h similarity index 100% rename from include/coral.h rename to core/include/coral.h diff --git a/include/coral_implementation.h b/core/include/coral_implementation.h similarity index 100% rename from include/coral_implementation.h rename to core/include/coral_implementation.h diff --git a/include/coral_network.h b/core/include/coral_network.h similarity index 100% rename from include/coral_network.h rename to core/include/coral_network.h diff --git a/include/coral_network_implementation.h b/core/include/coral_network_implementation.h similarity index 100% rename from include/coral_network_implementation.h rename to core/include/coral_network_implementation.h diff --git a/core/include/coral_plugin.h b/core/include/coral_plugin.h new file mode 100644 index 0000000..c9e2e00 --- /dev/null +++ b/core/include/coral_plugin.h @@ -0,0 +1,20 @@ +#pragma once + +// Minimal C ABI for backend plugins. +// +// Backends should build a shared library exporting coral_backend_register_types(). +// A host application (runner/manipulator/tools) loads the plugin and calls this +// function to populate coral::NodeObject's registry. + +#if defined(_WIN32) +# define CORAL_PLUGIN_EXPORT extern "C" __declspec(dllexport) +#else +# define CORAL_PLUGIN_EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +CORAL_PLUGIN_EXPORT void +coral_backend_register_types(); + +CORAL_PLUGIN_EXPORT const char * +coral_backend_name(); + diff --git a/include/magic_enum/magic_enum.hpp b/core/include/magic_enum/magic_enum.hpp similarity index 100% rename from include/magic_enum/magic_enum.hpp rename to core/include/magic_enum/magic_enum.hpp diff --git a/include/magic_enum/magic_enum_all.hpp b/core/include/magic_enum/magic_enum_all.hpp similarity index 100% rename from include/magic_enum/magic_enum_all.hpp rename to core/include/magic_enum/magic_enum_all.hpp diff --git a/include/magic_enum/magic_enum_containers.hpp b/core/include/magic_enum/magic_enum_containers.hpp similarity index 100% rename from include/magic_enum/magic_enum_containers.hpp rename to core/include/magic_enum/magic_enum_containers.hpp diff --git a/include/magic_enum/magic_enum_flags.hpp b/core/include/magic_enum/magic_enum_flags.hpp similarity index 100% rename from include/magic_enum/magic_enum_flags.hpp rename to core/include/magic_enum/magic_enum_flags.hpp diff --git a/include/magic_enum/magic_enum_format.hpp b/core/include/magic_enum/magic_enum_format.hpp similarity index 100% rename from include/magic_enum/magic_enum_format.hpp rename to core/include/magic_enum/magic_enum_format.hpp diff --git a/include/magic_enum/magic_enum_fuse.hpp b/core/include/magic_enum/magic_enum_fuse.hpp similarity index 100% rename from include/magic_enum/magic_enum_fuse.hpp rename to core/include/magic_enum/magic_enum_fuse.hpp diff --git a/include/magic_enum/magic_enum_iostream.hpp b/core/include/magic_enum/magic_enum_iostream.hpp similarity index 100% rename from include/magic_enum/magic_enum_iostream.hpp rename to core/include/magic_enum/magic_enum_iostream.hpp diff --git a/include/magic_enum/magic_enum_switch.hpp b/core/include/magic_enum/magic_enum_switch.hpp similarity index 100% rename from include/magic_enum/magic_enum_switch.hpp rename to core/include/magic_enum/magic_enum_switch.hpp diff --git a/include/magic_enum/magic_enum_utility.hpp b/core/include/magic_enum/magic_enum_utility.hpp similarity index 100% rename from include/magic_enum/magic_enum_utility.hpp rename to core/include/magic_enum/magic_enum_utility.hpp diff --git a/include/nlohmann/json.hpp b/core/include/nlohmann/json.hpp similarity index 100% rename from include/nlohmann/json.hpp rename to core/include/nlohmann/json.hpp diff --git a/core/include/slog.h b/core/include/slog.h new file mode 100644 index 0000000..35cd579 --- /dev/null +++ b/core/include/slog.h @@ -0,0 +1,3 @@ +#pragma once + +#include "slog/slog.h" diff --git a/include/slog/slog.h b/core/include/slog/slog.h similarity index 100% rename from include/slog/slog.h rename to core/include/slog/slog.h diff --git a/include/taskflow/algorithm/algorithm.hpp b/core/include/taskflow/algorithm/algorithm.hpp similarity index 100% rename from include/taskflow/algorithm/algorithm.hpp rename to core/include/taskflow/algorithm/algorithm.hpp diff --git a/include/taskflow/algorithm/data_pipeline.hpp b/core/include/taskflow/algorithm/data_pipeline.hpp similarity index 100% rename from include/taskflow/algorithm/data_pipeline.hpp rename to core/include/taskflow/algorithm/data_pipeline.hpp diff --git a/include/taskflow/algorithm/find.hpp b/core/include/taskflow/algorithm/find.hpp similarity index 100% rename from include/taskflow/algorithm/find.hpp rename to core/include/taskflow/algorithm/find.hpp diff --git a/include/taskflow/algorithm/for_each.hpp b/core/include/taskflow/algorithm/for_each.hpp similarity index 100% rename from include/taskflow/algorithm/for_each.hpp rename to core/include/taskflow/algorithm/for_each.hpp diff --git a/include/taskflow/algorithm/module.hpp b/core/include/taskflow/algorithm/module.hpp similarity index 100% rename from include/taskflow/algorithm/module.hpp rename to core/include/taskflow/algorithm/module.hpp diff --git a/include/taskflow/algorithm/partitioner.hpp b/core/include/taskflow/algorithm/partitioner.hpp similarity index 100% rename from include/taskflow/algorithm/partitioner.hpp rename to core/include/taskflow/algorithm/partitioner.hpp diff --git a/include/taskflow/algorithm/pipeline.hpp b/core/include/taskflow/algorithm/pipeline.hpp similarity index 100% rename from include/taskflow/algorithm/pipeline.hpp rename to core/include/taskflow/algorithm/pipeline.hpp diff --git a/include/taskflow/algorithm/reduce.hpp b/core/include/taskflow/algorithm/reduce.hpp similarity index 100% rename from include/taskflow/algorithm/reduce.hpp rename to core/include/taskflow/algorithm/reduce.hpp diff --git a/include/taskflow/algorithm/scan.hpp b/core/include/taskflow/algorithm/scan.hpp similarity index 100% rename from include/taskflow/algorithm/scan.hpp rename to core/include/taskflow/algorithm/scan.hpp diff --git a/include/taskflow/algorithm/sort.hpp b/core/include/taskflow/algorithm/sort.hpp similarity index 100% rename from include/taskflow/algorithm/sort.hpp rename to core/include/taskflow/algorithm/sort.hpp diff --git a/include/taskflow/algorithm/transform.hpp b/core/include/taskflow/algorithm/transform.hpp similarity index 100% rename from include/taskflow/algorithm/transform.hpp rename to core/include/taskflow/algorithm/transform.hpp diff --git a/include/taskflow/core/async.hpp b/core/include/taskflow/core/async.hpp similarity index 100% rename from include/taskflow/core/async.hpp rename to core/include/taskflow/core/async.hpp diff --git a/include/taskflow/core/async_task.hpp b/core/include/taskflow/core/async_task.hpp similarity index 100% rename from include/taskflow/core/async_task.hpp rename to core/include/taskflow/core/async_task.hpp diff --git a/include/taskflow/core/atomic_notifier.hpp b/core/include/taskflow/core/atomic_notifier.hpp similarity index 100% rename from include/taskflow/core/atomic_notifier.hpp rename to core/include/taskflow/core/atomic_notifier.hpp diff --git a/include/taskflow/core/declarations.hpp b/core/include/taskflow/core/declarations.hpp similarity index 100% rename from include/taskflow/core/declarations.hpp rename to core/include/taskflow/core/declarations.hpp diff --git a/include/taskflow/core/environment.hpp b/core/include/taskflow/core/environment.hpp similarity index 100% rename from include/taskflow/core/environment.hpp rename to core/include/taskflow/core/environment.hpp diff --git a/include/taskflow/core/error.hpp b/core/include/taskflow/core/error.hpp similarity index 100% rename from include/taskflow/core/error.hpp rename to core/include/taskflow/core/error.hpp diff --git a/include/taskflow/core/executor.hpp b/core/include/taskflow/core/executor.hpp similarity index 100% rename from include/taskflow/core/executor.hpp rename to core/include/taskflow/core/executor.hpp diff --git a/include/taskflow/core/flow_builder.hpp b/core/include/taskflow/core/flow_builder.hpp similarity index 100% rename from include/taskflow/core/flow_builder.hpp rename to core/include/taskflow/core/flow_builder.hpp diff --git a/include/taskflow/core/freelist.hpp b/core/include/taskflow/core/freelist.hpp similarity index 100% rename from include/taskflow/core/freelist.hpp rename to core/include/taskflow/core/freelist.hpp diff --git a/include/taskflow/core/graph.hpp b/core/include/taskflow/core/graph.hpp similarity index 100% rename from include/taskflow/core/graph.hpp rename to core/include/taskflow/core/graph.hpp diff --git a/include/taskflow/core/nonblocking_notifier.hpp b/core/include/taskflow/core/nonblocking_notifier.hpp similarity index 100% rename from include/taskflow/core/nonblocking_notifier.hpp rename to core/include/taskflow/core/nonblocking_notifier.hpp diff --git a/include/taskflow/core/observer.hpp b/core/include/taskflow/core/observer.hpp similarity index 100% rename from include/taskflow/core/observer.hpp rename to core/include/taskflow/core/observer.hpp diff --git a/include/taskflow/core/runtime.hpp b/core/include/taskflow/core/runtime.hpp similarity index 100% rename from include/taskflow/core/runtime.hpp rename to core/include/taskflow/core/runtime.hpp diff --git a/include/taskflow/core/semaphore.hpp b/core/include/taskflow/core/semaphore.hpp similarity index 100% rename from include/taskflow/core/semaphore.hpp rename to core/include/taskflow/core/semaphore.hpp diff --git a/include/taskflow/core/task.hpp b/core/include/taskflow/core/task.hpp similarity index 100% rename from include/taskflow/core/task.hpp rename to core/include/taskflow/core/task.hpp diff --git a/include/taskflow/core/taskflow.hpp b/core/include/taskflow/core/taskflow.hpp similarity index 100% rename from include/taskflow/core/taskflow.hpp rename to core/include/taskflow/core/taskflow.hpp diff --git a/include/taskflow/core/topology.hpp b/core/include/taskflow/core/topology.hpp similarity index 100% rename from include/taskflow/core/topology.hpp rename to core/include/taskflow/core/topology.hpp diff --git a/include/taskflow/core/tsq.hpp b/core/include/taskflow/core/tsq.hpp similarity index 100% rename from include/taskflow/core/tsq.hpp rename to core/include/taskflow/core/tsq.hpp diff --git a/include/taskflow/core/worker.hpp b/core/include/taskflow/core/worker.hpp similarity index 100% rename from include/taskflow/core/worker.hpp rename to core/include/taskflow/core/worker.hpp diff --git a/include/taskflow/cuda/algorithm/find.hpp b/core/include/taskflow/cuda/algorithm/find.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/find.hpp rename to core/include/taskflow/cuda/algorithm/find.hpp diff --git a/include/taskflow/cuda/algorithm/for_each.hpp b/core/include/taskflow/cuda/algorithm/for_each.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/for_each.hpp rename to core/include/taskflow/cuda/algorithm/for_each.hpp diff --git a/include/taskflow/cuda/algorithm/matmul.hpp b/core/include/taskflow/cuda/algorithm/matmul.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/matmul.hpp rename to core/include/taskflow/cuda/algorithm/matmul.hpp diff --git a/include/taskflow/cuda/algorithm/merge.hpp b/core/include/taskflow/cuda/algorithm/merge.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/merge.hpp rename to core/include/taskflow/cuda/algorithm/merge.hpp diff --git a/include/taskflow/cuda/algorithm/reduce.hpp b/core/include/taskflow/cuda/algorithm/reduce.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/reduce.hpp rename to core/include/taskflow/cuda/algorithm/reduce.hpp diff --git a/include/taskflow/cuda/algorithm/scan.hpp b/core/include/taskflow/cuda/algorithm/scan.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/scan.hpp rename to core/include/taskflow/cuda/algorithm/scan.hpp diff --git a/include/taskflow/cuda/algorithm/single_task.hpp b/core/include/taskflow/cuda/algorithm/single_task.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/single_task.hpp rename to core/include/taskflow/cuda/algorithm/single_task.hpp diff --git a/include/taskflow/cuda/algorithm/sort.hpp b/core/include/taskflow/cuda/algorithm/sort.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/sort.hpp rename to core/include/taskflow/cuda/algorithm/sort.hpp diff --git a/include/taskflow/cuda/algorithm/transform.hpp b/core/include/taskflow/cuda/algorithm/transform.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/transform.hpp rename to core/include/taskflow/cuda/algorithm/transform.hpp diff --git a/include/taskflow/cuda/algorithm/transpose.hpp b/core/include/taskflow/cuda/algorithm/transpose.hpp similarity index 100% rename from include/taskflow/cuda/algorithm/transpose.hpp rename to core/include/taskflow/cuda/algorithm/transpose.hpp diff --git a/include/taskflow/cuda/cuda_capturer.hpp b/core/include/taskflow/cuda/cuda_capturer.hpp similarity index 100% rename from include/taskflow/cuda/cuda_capturer.hpp rename to core/include/taskflow/cuda/cuda_capturer.hpp diff --git a/include/taskflow/cuda/cuda_device.hpp b/core/include/taskflow/cuda/cuda_device.hpp similarity index 100% rename from include/taskflow/cuda/cuda_device.hpp rename to core/include/taskflow/cuda/cuda_device.hpp diff --git a/include/taskflow/cuda/cuda_error.hpp b/core/include/taskflow/cuda/cuda_error.hpp similarity index 100% rename from include/taskflow/cuda/cuda_error.hpp rename to core/include/taskflow/cuda/cuda_error.hpp diff --git a/include/taskflow/cuda/cuda_execution_policy.hpp b/core/include/taskflow/cuda/cuda_execution_policy.hpp similarity index 100% rename from include/taskflow/cuda/cuda_execution_policy.hpp rename to core/include/taskflow/cuda/cuda_execution_policy.hpp diff --git a/include/taskflow/cuda/cuda_graph.hpp b/core/include/taskflow/cuda/cuda_graph.hpp similarity index 100% rename from include/taskflow/cuda/cuda_graph.hpp rename to core/include/taskflow/cuda/cuda_graph.hpp diff --git a/include/taskflow/cuda/cuda_graph_exec.hpp b/core/include/taskflow/cuda/cuda_graph_exec.hpp similarity index 100% rename from include/taskflow/cuda/cuda_graph_exec.hpp rename to core/include/taskflow/cuda/cuda_graph_exec.hpp diff --git a/include/taskflow/cuda/cuda_memory.hpp b/core/include/taskflow/cuda/cuda_memory.hpp similarity index 100% rename from include/taskflow/cuda/cuda_memory.hpp rename to core/include/taskflow/cuda/cuda_memory.hpp diff --git a/include/taskflow/cuda/cuda_meta.hpp b/core/include/taskflow/cuda/cuda_meta.hpp similarity index 100% rename from include/taskflow/cuda/cuda_meta.hpp rename to core/include/taskflow/cuda/cuda_meta.hpp diff --git a/include/taskflow/cuda/cuda_optimizer.hpp b/core/include/taskflow/cuda/cuda_optimizer.hpp similarity index 100% rename from include/taskflow/cuda/cuda_optimizer.hpp rename to core/include/taskflow/cuda/cuda_optimizer.hpp diff --git a/include/taskflow/cuda/cuda_stream.hpp b/core/include/taskflow/cuda/cuda_stream.hpp similarity index 100% rename from include/taskflow/cuda/cuda_stream.hpp rename to core/include/taskflow/cuda/cuda_stream.hpp diff --git a/include/taskflow/cuda/cudaflow.hpp b/core/include/taskflow/cuda/cudaflow.hpp similarity index 100% rename from include/taskflow/cuda/cudaflow.hpp rename to core/include/taskflow/cuda/cudaflow.hpp diff --git a/include/taskflow/dsl/connection.hpp b/core/include/taskflow/dsl/connection.hpp similarity index 100% rename from include/taskflow/dsl/connection.hpp rename to core/include/taskflow/dsl/connection.hpp diff --git a/include/taskflow/dsl/dsl.hpp b/core/include/taskflow/dsl/dsl.hpp similarity index 100% rename from include/taskflow/dsl/dsl.hpp rename to core/include/taskflow/dsl/dsl.hpp diff --git a/include/taskflow/dsl/meta_macro.hpp b/core/include/taskflow/dsl/meta_macro.hpp similarity index 100% rename from include/taskflow/dsl/meta_macro.hpp rename to core/include/taskflow/dsl/meta_macro.hpp diff --git a/include/taskflow/dsl/task_analyzer.hpp b/core/include/taskflow/dsl/task_analyzer.hpp similarity index 100% rename from include/taskflow/dsl/task_analyzer.hpp rename to core/include/taskflow/dsl/task_analyzer.hpp diff --git a/include/taskflow/dsl/task_dsl.hpp b/core/include/taskflow/dsl/task_dsl.hpp similarity index 100% rename from include/taskflow/dsl/task_dsl.hpp rename to core/include/taskflow/dsl/task_dsl.hpp diff --git a/include/taskflow/dsl/task_trait.hpp b/core/include/taskflow/dsl/task_trait.hpp similarity index 100% rename from include/taskflow/dsl/task_trait.hpp rename to core/include/taskflow/dsl/task_trait.hpp diff --git a/include/taskflow/dsl/tuple_utils.hpp b/core/include/taskflow/dsl/tuple_utils.hpp similarity index 100% rename from include/taskflow/dsl/tuple_utils.hpp rename to core/include/taskflow/dsl/tuple_utils.hpp diff --git a/include/taskflow/dsl/type_list.hpp b/core/include/taskflow/dsl/type_list.hpp similarity index 100% rename from include/taskflow/dsl/type_list.hpp rename to core/include/taskflow/dsl/type_list.hpp diff --git a/include/taskflow/sycl/algorithm/reduce.hpp b/core/include/taskflow/sycl/algorithm/reduce.hpp similarity index 100% rename from include/taskflow/sycl/algorithm/reduce.hpp rename to core/include/taskflow/sycl/algorithm/reduce.hpp diff --git a/include/taskflow/sycl/algorithm/sycl_for_each.hpp b/core/include/taskflow/sycl/algorithm/sycl_for_each.hpp similarity index 100% rename from include/taskflow/sycl/algorithm/sycl_for_each.hpp rename to core/include/taskflow/sycl/algorithm/sycl_for_each.hpp diff --git a/include/taskflow/sycl/algorithm/sycl_transform.hpp b/core/include/taskflow/sycl/algorithm/sycl_transform.hpp similarity index 100% rename from include/taskflow/sycl/algorithm/sycl_transform.hpp rename to core/include/taskflow/sycl/algorithm/sycl_transform.hpp diff --git a/include/taskflow/sycl/sycl_execution_policy.hpp b/core/include/taskflow/sycl/sycl_execution_policy.hpp similarity index 100% rename from include/taskflow/sycl/sycl_execution_policy.hpp rename to core/include/taskflow/sycl/sycl_execution_policy.hpp diff --git a/include/taskflow/sycl/sycl_graph.hpp b/core/include/taskflow/sycl/sycl_graph.hpp similarity index 100% rename from include/taskflow/sycl/sycl_graph.hpp rename to core/include/taskflow/sycl/sycl_graph.hpp diff --git a/include/taskflow/sycl/sycl_meta.hpp b/core/include/taskflow/sycl/sycl_meta.hpp similarity index 100% rename from include/taskflow/sycl/sycl_meta.hpp rename to core/include/taskflow/sycl/sycl_meta.hpp diff --git a/include/taskflow/sycl/sycl_task.hpp b/core/include/taskflow/sycl/sycl_task.hpp similarity index 100% rename from include/taskflow/sycl/sycl_task.hpp rename to core/include/taskflow/sycl/sycl_task.hpp diff --git a/include/taskflow/sycl/syclflow.hpp b/core/include/taskflow/sycl/syclflow.hpp similarity index 100% rename from include/taskflow/sycl/syclflow.hpp rename to core/include/taskflow/sycl/syclflow.hpp diff --git a/include/taskflow/taskflow.hpp b/core/include/taskflow/taskflow.hpp similarity index 100% rename from include/taskflow/taskflow.hpp rename to core/include/taskflow/taskflow.hpp diff --git a/include/taskflow/utility/iterator.hpp b/core/include/taskflow/utility/iterator.hpp similarity index 100% rename from include/taskflow/utility/iterator.hpp rename to core/include/taskflow/utility/iterator.hpp diff --git a/include/taskflow/utility/latch.hpp b/core/include/taskflow/utility/latch.hpp similarity index 100% rename from include/taskflow/utility/latch.hpp rename to core/include/taskflow/utility/latch.hpp diff --git a/include/taskflow/utility/lazy_string.hpp b/core/include/taskflow/utility/lazy_string.hpp similarity index 100% rename from include/taskflow/utility/lazy_string.hpp rename to core/include/taskflow/utility/lazy_string.hpp diff --git a/include/taskflow/utility/macros.hpp b/core/include/taskflow/utility/macros.hpp similarity index 100% rename from include/taskflow/utility/macros.hpp rename to core/include/taskflow/utility/macros.hpp diff --git a/include/taskflow/utility/math.hpp b/core/include/taskflow/utility/math.hpp similarity index 100% rename from include/taskflow/utility/math.hpp rename to core/include/taskflow/utility/math.hpp diff --git a/include/taskflow/utility/mpmc.hpp b/core/include/taskflow/utility/mpmc.hpp similarity index 100% rename from include/taskflow/utility/mpmc.hpp rename to core/include/taskflow/utility/mpmc.hpp diff --git a/include/taskflow/utility/object_pool.hpp b/core/include/taskflow/utility/object_pool.hpp similarity index 100% rename from include/taskflow/utility/object_pool.hpp rename to core/include/taskflow/utility/object_pool.hpp diff --git a/include/taskflow/utility/os.hpp b/core/include/taskflow/utility/os.hpp similarity index 100% rename from include/taskflow/utility/os.hpp rename to core/include/taskflow/utility/os.hpp diff --git a/include/taskflow/utility/serializer.hpp b/core/include/taskflow/utility/serializer.hpp similarity index 100% rename from include/taskflow/utility/serializer.hpp rename to core/include/taskflow/utility/serializer.hpp diff --git a/include/taskflow/utility/singleton.hpp b/core/include/taskflow/utility/singleton.hpp similarity index 100% rename from include/taskflow/utility/singleton.hpp rename to core/include/taskflow/utility/singleton.hpp diff --git a/include/taskflow/utility/small_vector.hpp b/core/include/taskflow/utility/small_vector.hpp similarity index 100% rename from include/taskflow/utility/small_vector.hpp rename to core/include/taskflow/utility/small_vector.hpp diff --git a/include/taskflow/utility/stream.hpp b/core/include/taskflow/utility/stream.hpp similarity index 100% rename from include/taskflow/utility/stream.hpp rename to core/include/taskflow/utility/stream.hpp diff --git a/include/taskflow/utility/traits.hpp b/core/include/taskflow/utility/traits.hpp similarity index 100% rename from include/taskflow/utility/traits.hpp rename to core/include/taskflow/utility/traits.hpp diff --git a/include/taskflow/utility/uuid.hpp b/core/include/taskflow/utility/uuid.hpp similarity index 100% rename from include/taskflow/utility/uuid.hpp rename to core/include/taskflow/utility/uuid.hpp diff --git a/core/include/type_name.h b/core/include/type_name.h new file mode 100644 index 0000000..d2c1b2f --- /dev/null +++ b/core/include/type_name.h @@ -0,0 +1,122 @@ +#ifndef CORAL_TYPE_NAME_H +#define CORAL_TYPE_NAME_H + +#include +#include + +#if defined(__has_include) +# if __has_include() +# include +# endif +#endif + +#if defined(__clang__) || defined(__GNUG__) +# include +# include +#endif + +namespace boost +{ + namespace core + { + namespace coral_detail + { + inline void + replace_all(std::string &s, const char *from, const char *to) + { + if (!from || !to) + return; + const std::string needle(from); + if (needle.empty()) + return; + const std::string repl(to); + + std::size_t pos = 0; + while ((pos = s.find(needle, pos)) != std::string::npos) + { + s.replace(pos, needle.size(), repl); + pos += repl.size(); + } + } + + inline void + normalize_type_string(std::string &s) + { + // Normalize common standard library inline namespaces. + replace_all(s, "std::__1::", "std::"); // libc++ + replace_all(s, "std::__cxx11::", "std::"); // libstdc++ + + // Make function types match a more common spelling. + replace_all(s, " (", "("); + + // Common aliases used in tests and logs. + if (s == "unsigned int") + s = "unsigned"; + + // std::string + const bool is_basic_string_char = + s.find("std::basic_string") != std::string::npos && + s.find("std::allocator") != std::string::npos; + if (is_basic_string_char) + s = "std::string"; + + // std::basic_ofstream + const bool is_basic_ofstream_char = + s.find("std::basic_ofstream") != std::string::npos; + if (is_basic_ofstream_char) + s = "std::basic_ofstream"; + + // std::ostream + replace_all(s, + "std::basic_ostream>", + "std::ostream"); + } + + inline std::string + demangle_type_name(const char *name) + { + if (!name) + return {}; + +#if defined(__clang__) || defined(__GNUG__) + int status = 0; + char *demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status); + std::string result = + (status == 0 && demangled) ? std::string(demangled) : + std::string(name); + std::free(demangled); + normalize_type_string(result); + return result; +#else + std::string result(name); + normalize_type_string(result); + return result; +#endif + } + } // namespace coral_detail + +#if !defined(BOOST_CORE_DEMANGLE_HPP_INCLUDED) + inline std::string + demangle(const char *name) + { + return coral_detail::demangle_type_name(name); + } +# define BOOST_CORE_DEMANGLE_HPP_INCLUDED 1 +#endif + +#if !defined(BOOST_CORE_TYPE_NAME_HPP_INCLUDED) + template + inline std::string + type_name() + { + return coral_detail::demangle_type_name(typeid(T).name()); + } +# define BOOST_CORE_TYPE_NAME_HPP_INCLUDED 1 +#endif + + } // namespace core +} // namespace boost + +#endif // CORAL_TYPE_NAME_H diff --git a/include/utils.h b/core/include/utils.h similarity index 100% rename from include/utils.h rename to core/include/utils.h diff --git a/source/coral_implementation.cc b/core/source/coral_implementation.cc similarity index 100% rename from source/coral_implementation.cc rename to core/source/coral_implementation.cc diff --git a/source/coral_network_implementation.cc b/core/source/coral_network_implementation.cc similarity index 100% rename from source/coral_network_implementation.cc rename to core/source/coral_network_implementation.cc diff --git a/source/node_object.cc b/core/source/node_object.cc similarity index 100% rename from source/node_object.cc rename to core/source/node_object.cc diff --git a/source/slog/slog.c b/core/source/slog/slog.c similarity index 100% rename from source/slog/slog.c rename to core/source/slog/slog.c diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt new file mode 100644 index 0000000..3b7648f --- /dev/null +++ b/core/tests/CMakeLists.txt @@ -0,0 +1,26 @@ +find_package(GTest) +if(NOT GTest_FOUND) + message(WARNING "GTest not found; skipping core tests") + return() +endif() + +file(GLOB _test_files CONFIGURE_DEPENDS *.cc) + +add_executable(coral_core_tests ${_test_files}) +target_link_libraries(coral_core_tests coral_core ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES}) + +if(GTEST_INCLUDE_DIRS) + target_include_directories(coral_core_tests SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) +endif() + +if(MSVC) + target_compile_options(coral_core_tests PRIVATE /W0) +else() + target_compile_options(coral_core_tests PRIVATE -w) +endif() + +add_custom_target(run_coral_core_tests + COMMAND $ + DEPENDS coral_core_tests + USES_TERMINAL) + diff --git a/gtests/node_types.cc b/core/tests/connection_type.cc similarity index 100% rename from gtests/node_types.cc rename to core/tests/connection_type.cc diff --git a/gtests/serialize.cc b/core/tests/serialize.cc similarity index 56% rename from gtests/serialize.cc rename to core/tests/serialize.cc index 4cb496e..ac83ff1 100644 --- a/gtests/serialize.cc +++ b/core/tests/serialize.cc @@ -1,13 +1,7 @@ -#include - -#include - #include #include "coral.h" -#include "register_types.h" -using namespace dealii; using namespace coral; TEST(Serialize, Int) @@ -35,71 +29,11 @@ TEST(Serialize, Int) ASSERT_EQ(42, obj2->get()); ASSERT_EQ("42", obj2->to_string()); - // auto j2 = obj2->get_info(); - // ASSERT_TRUE(j2["value"] == "42"); - // Make sure that after executing the object, it still holds the same value (*obj2)(); ASSERT_EQ("42", obj2->to_string()); } -TEST(Serialize, Point) -{ - using type = Point<2>; - NodeObject::register_elementary_type(); - NodeObjectPtr obj = make_node(type(0.0, 1.0)); - ASSERT_TRUE(obj->ready()); - ASSERT_EQ(type(0.0, 1.0), obj->get()); - ASSERT_EQ("[0.0,1.0]", obj->to_string()); - - // Serialize to json - json j = obj; - ASSERT_TRUE(j["node_type"] == "elementary_constructor"); - ASSERT_TRUE(j["value"] == "[0.0,1.0]"); - - // Deserialize from json - auto obj2 = j.get(); - - ASSERT_EQ(obj->hash(), obj2->hash()); - ASSERT_TRUE(obj2->ready()); - ASSERT_EQ(obj->get(), obj2->get()); - - // Make sure that after executing the object, it still holds the same value - (*obj2)(); - ASSERT_EQ("[0.0,1.0]", obj2->to_string()); -} - - -TEST(Serialize, Triangulation) -{ - using type = Triangulation<2>; - NodeObject::register_type(); - NodeObjectPtr obj = make_node(); - (*obj)(); - - ASSERT_TRUE(obj->ready()); - - // Serialize to json - json j = obj; - ASSERT_TRUE(j["node_type"] == "empty_constructor"); - - // Deserialize from json. This will call the constructor. - auto obj2 = j.get(); - - ASSERT_EQ(obj->hash(), obj2->hash()); - ASSERT_TRUE(obj2->ready()); - - // The two objects are actually different objects in memory. - ASSERT_NE(&obj->get(), &obj2->get()); - - // Dump the json to a file - std::ofstream ofs("triangulation.json"); - ofs << j.dump(2); - ofs.close(); -} - - - TEST(Serialize, String) { using type = std::string; @@ -130,8 +64,6 @@ TEST(Serialize, String) ASSERT_EQ("test", obj2->to_string()); } - - TEST(Serialize, ToFromJson) { using type = int; @@ -145,14 +77,13 @@ TEST(Serialize, ToFromJson) ASSERT_EQ(j.dump(2), j2.dump(2)); ASSERT_EQ(obj->hash(), obj2->hash()); ASSERT_TRUE(obj2->ready()); - ASSERT_EQ(obj->get(), - obj2->get()); // Check the value of the deserialized object + ASSERT_EQ(obj->get(), obj2->get()); ASSERT_EQ("42", obj2->to_string()); - // Make sure that after executing the object, it still holds the same value + (*obj2)(); ASSERT_EQ("42", obj2->to_string()); - // Same with obj (*obj)(); ASSERT_EQ("42", obj->to_string()); -} \ No newline at end of file +} + diff --git a/gtests/trivial_types.cc b/core/tests/trivial_types.cc similarity index 68% rename from gtests/trivial_types.cc rename to core/tests/trivial_types.cc index 82a86b7..cbb91ee 100644 --- a/gtests/trivial_types.cc +++ b/core/tests/trivial_types.cc @@ -1,12 +1,7 @@ -#include -#include - #include #include "coral.h" -#include "register_types.h" -using namespace dealii; using namespace coral; TEST(TrivialTypes, Int) @@ -42,34 +37,12 @@ TEST(TrivialTypes, String) ASSERT_EQ(s, coral::detail::hash()); } -TEST(TrivialTypes, Point) -{ - using type = Point<2>; - // Now we can use register_elementary_type since we have JSON serialization - // for Point - NodeObject::register_elementary_type(); - NodeObject obj = type(1.0, 2.0); - ASSERT_EQ(type(1.0, 2.0), obj.get()); - const auto &j = obj.get_info(); - std::string s = j.at("type"); - ASSERT_EQ(s, coral::detail::hash()); - - // Verify JSON serialization is working correctly - ASSERT_TRUE(j.contains("value")); - auto json_value = json::parse(j.at("value").get()); - ASSERT_EQ(json_value.size(), 2); - ASSERT_EQ(json_value[0], 1.0); - ASSERT_EQ(json_value[1], 2.0); -} - - TEST(TrivialTypes, Unregistered) { // Check we throw a runtime error if we try to get an unregistered type ASSERT_THROW(NodeObject((float)42.0), std::runtime_error); } - TEST(TrivialTypes, CopyType) { NodeObject::register_elementary_type(); @@ -79,7 +52,6 @@ TEST(TrivialTypes, CopyType) ASSERT_EQ(42, obj2.get()); } - TEST(TrivialTypes, RunInitializerInt) { NodeObject::register_elementary_type(); @@ -93,3 +65,4 @@ TEST(TrivialTypes, RunInitializerInt) (*obj)(); ASSERT_EQ(42, obj->get()); } + diff --git a/gtests/CMakeLists.txt b/gtests/CMakeLists.txt deleted file mode 100644 index 7584512..0000000 --- a/gtests/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -## --------------------------------------------------------------------- -## -## Copyright (C) 2000 2020 by the deal.II authors -## -## This file is part of the deal.II library. -## -## The deal.II library is free software; you can use it, redistribute -## it, and/or modify it under the terms of the GNU Lesser General -## Public License as published by the Free Software Foundation; either -## version 2.1 of the License, or (at your option) any later version. -## The full text of the license can be found in the file LICENSE.md at -## the top level directory of deal.II. -## -## --------------------------------------------------------------------- -set(_target gtests) - -file(GLOB _test_files *cc) - -include(GoogleTest) - -FOREACH(_build_type ${_d2_build_types}) - # Postfix to use everywhere - SET(_p "${${_build_type}_postfix}") - # Only build this type, if deal.II was compiled with it. - IF(CMAKE_BUILD_TYPE MATCHES "${_build_type}" AND - DEAL_II_BUILD_TYPE MATCHES "${_build_type}") - - MESSAGE("-- Found ${_build_type} version of deal.II.") - - STRING(TOUPPER "${_build_type}" _BUILD_TYPE) - SET(_exe "${_target}${${_build_type}_postfix}") - - MESSAGE("-- Setting up ${_exe}") - add_executable(${_exe} ${_test_files}) - - TARGET_LINK_LIBRARIES(${_exe} ${TEST_LIBRARIES_${_BUILD_TYPE}} - ${GTEST_LIBRARIES}) - DEAL_II_SETUP_TARGET(${_exe} ${_BUILD_TYPE}) - include_directories(${GTEST_INCLUDE_DIRS}) - - # gtest_discover_tests(${_exe}) - ENDIF() -ENDFOREACH() diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt new file mode 100644 index 0000000..229d2d8 --- /dev/null +++ b/gui/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(coral_gui LANGUAGES CXX) +endif() + +option(CORAL_GUI_BUILD_EDITOR "Build the node editor (registry-driven)." ON) +option(CORAL_GUI_BUILD_MANIPULATOR "Build the VTK-based manipulator." ON) + +if(CORAL_GUI_BUILD_EDITOR) + add_subdirectory(editor) +endif() + +if(CORAL_GUI_BUILD_MANIPULATOR) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/manipulator/CMakeLists.txt") + add_subdirectory(manipulator) + else() + message(WARNING + "CORAL_GUI_BUILD_MANIPULATOR is ON, but gui/manipulator has no CMakeLists.txt; skipping.") + endif() +endif() diff --git a/gui/editor/CMakeLists.txt b/gui/editor/CMakeLists.txt new file mode 100644 index 0000000..9346287 --- /dev/null +++ b/gui/editor/CMakeLists.txt @@ -0,0 +1,105 @@ +include(FetchContent) + +set(_node_editor_root "${CMAKE_CURRENT_LIST_DIR}/imgui-node-editor") + +add_subdirectory( + ${_node_editor_root}/external/imgui + ${CMAKE_CURRENT_BINARY_DIR}/imgui + EXCLUDE_FROM_ALL) + +if(TARGET imgui) + if(MSVC) + target_compile_options(imgui PRIVATE /W0) + else() + target_compile_options(imgui PRIVATE -w) + endif() +endif() + +add_library(coral_imgui_node_editor STATIC + ${_node_editor_root}/crude_json.cpp + ${_node_editor_root}/imgui_canvas.cpp + ${_node_editor_root}/imgui_node_editor.cpp + ${_node_editor_root}/imgui_node_editor_api.cpp) + +target_include_directories(coral_imgui_node_editor SYSTEM PUBLIC + ${_node_editor_root} + ${_node_editor_root}/external/imgui) + +target_link_libraries(coral_imgui_node_editor PUBLIC imgui) + +if(MSVC) + target_compile_options(coral_imgui_node_editor PRIVATE /W0) +else() + target_compile_options(coral_imgui_node_editor PRIVATE -w) +endif() + +find_package(glfw3 QUIET) +if(NOT glfw3_FOUND AND NOT TARGET glfw AND NOT TARGET glfw3) + set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) + set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + glfw + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.3.8 + ) + FetchContent_MakeAvailable(glfw) +endif() + +find_package(OpenGL REQUIRED) + +if(TARGET glfw) + set(_glfw_target glfw) +elseif(TARGET glfw3) + set(_glfw_target glfw3) +else() + set(_glfw_target glfw) +endif() + +if(TARGET ${_glfw_target}) + if(MSVC) + target_compile_options(${_glfw_target} PRIVATE /W0) + else() + target_compile_options(${_glfw_target} PRIVATE -w) + endif() +endif() + +add_executable(coral_node_editor + source/node_editor_main.cc + ${_node_editor_root}/examples/application/source/imgui_impl_glfw.cpp + ${_node_editor_root}/examples/application/source/imgui_impl_opengl3.cpp) + +target_compile_features(coral_node_editor PRIVATE cxx_std_17) + +target_include_directories(coral_node_editor SYSTEM PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/include + ${CMAKE_CURRENT_LIST_DIR}/../../core/include + ${_node_editor_root} + ${_node_editor_root}/external/imgui + ${_node_editor_root}/examples/application/source) + +target_link_libraries(coral_node_editor PRIVATE + coral_imgui_node_editor + imgui + ${_glfw_target}) + +if(MSVC) + target_compile_options(coral_node_editor PRIVATE /W0) +else() + target_compile_options(coral_node_editor PRIVATE -w) +endif() + +if(TARGET OpenGL::GL) + target_link_libraries(coral_node_editor PRIVATE OpenGL::GL) +else() + target_link_libraries(coral_node_editor PRIVATE ${OPENGL_gl_LIBRARY}) +endif() + +if(APPLE) + target_compile_definitions(coral_node_editor PRIVATE GL_SILENCE_DEPRECATION=1) + target_link_libraries(coral_node_editor PRIVATE + "-framework CoreFoundation" + "-framework Cocoa" + "-framework IOKit" + "-framework CoreVideo") +endif() diff --git a/gui/editor/imgui-node-editor b/gui/editor/imgui-node-editor new file mode 160000 index 0000000..e78e447 --- /dev/null +++ b/gui/editor/imgui-node-editor @@ -0,0 +1 @@ +Subproject commit e78e447900909a051817a760efe13fe83e6e1afc diff --git a/gui/editor/include/coral_editor/node_editor_types.h b/gui/editor/include/coral_editor/node_editor_types.h new file mode 100644 index 0000000..6c8c045 --- /dev/null +++ b/gui/editor/include/coral_editor/node_editor_types.h @@ -0,0 +1,183 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#ifndef CORAL_NODE_EDITOR_ENABLE_MANIPULATOR +# define CORAL_NODE_EDITOR_ENABLE_MANIPULATOR 0 +#endif + +#ifndef CORAL_NODE_EDITOR_ENABLE_BACKEND_RUNNER +# define CORAL_NODE_EDITOR_ENABLE_BACKEND_RUNNER 0 +#endif + +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR +# include +#endif + +struct RegistryArgument +{ + std::string name; + std::string type; + std::string connection_type; +}; + +struct RegistryNodeType +{ + std::string type; + std::string node_type; + std::vector arguments; + std::vector inputs; + std::vector outputs; + std::string default_value; +}; + +struct EditorNode +{ + unsigned int id = 0; + std::string type; + std::string name; + std::string value; + char value_buffer[256] = {}; + bool value_buffer_init = false; + char name_buffer[256] = {}; + bool name_buffer_init = false; + bool needs_position = false; + ImVec2 desired_position = ImVec2(0.0f, 0.0f); +}; + +struct EditorLink +{ + unsigned int id = 0; + unsigned int source_node = 0; + unsigned int target_node = 0; + unsigned int source_output = 0; + unsigned int target_input = 0; +}; + +struct NetworkTab +{ + std::string name; + std::map nodes; + std::map links; + unsigned int next_node_id = 1; + unsigned int next_link_id = 1; +}; + +struct EditorState +{ + std::map registry; + std::vector tabs; + int active_tab = 0; + int pending_tab_switch = -1; + std::map tab_name_index; + std::map nodes; + std::map links; + unsigned int next_node_id = 1; + unsigned int next_link_id = 1; + char registry_path[256] = {}; + char network_path[256] = {}; + char new_tab_name[128] = {}; + std::string status; + std::string log_path = "coral_node_editor.log"; + int log_level = 3; + bool request_delete = false; + bool delete_key_down = false; + bool backspace_key_down = false; + bool text_input_active = false; + +#if CORAL_NODE_EDITOR_ENABLE_BACKEND_RUNNER + char backend_path[256] = {}; +#endif + + std::string console_output; + bool console_scroll_to_bottom = false; + bool show_error_popup = false; + std::string error_message; + bool force_tab_select = false; + int tab_bar_uid = 0; + bool left_collapsed = false; + bool right_collapsed = false; + float left_width = 300.0f; + float right_width = 360.0f; + bool request_focus = false; + float node_types_height = 260.0f; + bool show_module_delete_popup = false; + std::string module_delete_candidate; + std::string pending_module_delete_name; + bool show_module_rename_popup = false; + std::string module_rename_candidate; + std::string pending_module_rename_from; + std::string pending_module_rename_to; + char module_rename_buffer[128] = {}; + +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR + struct MeshToolState + { + bool open = false; + bool pending_load = false; + unsigned int target_node_id = 0; + int target_tab_index = -1; + std::string load_error; + + dealii::Triangulation<2, 2> tria; + bool has_tria = false; + + struct CellTri + { + ImVec2 p0; + ImVec2 p1; + ImVec2 p2; + unsigned int cell_index = 0; + unsigned int material_id = 0; + }; + + struct BoundarySeg + { + ImVec2 p0; + ImVec2 p1; + unsigned int cell_index = 0; + unsigned int face_no = 0; + unsigned int boundary_id = 0; + }; + + std::vector cell_tris; + std::vector boundary_segs; + std::map boundary_counts; + std::map material_counts; + std::map::active_cell_iterator> + cell_by_index; + + bool view_initialized = false; + ImVec2 view_center = ImVec2(0.0f, 0.0f); + float view_scale = 100.0f; + + enum class Mode + { + boundary = 0, + cell = 1, + }; + Mode mode = Mode::boundary; + + int selected_boundary_id = -1; + int selected_material_id = -1; + std::set> selected_boundary_faces; + std::set selected_cells; + + unsigned int assign_boundary_id = 0; + unsigned int assign_material_id = 0; + + float left_panel_width = 320.0f; + + bool box_select_active = false; + ImVec2 box_start_screen = ImVec2(0.0f, 0.0f); + ImVec2 box_end_screen = ImVec2(0.0f, 0.0f); + + float cell_edge_thickness = 1.0f; + } mesh_tool; +#endif +}; diff --git a/gui/editor/source/node_editor_main.cc b/gui/editor/source/node_editor_main.cc new file mode 100644 index 0000000..4d6e4f5 --- /dev/null +++ b/gui/editor/source/node_editor_main.cc @@ -0,0 +1,3625 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GLFW_INCLUDE_NONE +#include + +#if __APPLE__ +# include +#else +# include +#endif + +#include +#include +#include + +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" + +#include "coral_editor/node_editor_types.h" + +namespace ed = ax::NodeEditor; +using json = nlohmann::json; + +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR +# include "coral.h" +# include "coral_network.h" +# include "register_types.h" +#endif + +struct PinMeta +{ + std::string label; + std::string type; + std::string connection_type; + bool is_self = false; +}; + +struct NodeColors +{ + ImVec4 header; + ImVec4 body; + ImVec4 border; + ImVec4 border_hover; + ImVec4 border_selected; +}; + +struct NetworkInterface +{ + RegistryNodeType node_type; + bool valid = false; +}; + +static NetworkInterface +BuildNetworkInterface(const EditorState &state, const json &data); +static bool +GetNodeTypeForNode(const EditorState &state, + const EditorNode &node, + RegistryNodeType &out); +static void +UpdateTabIndex(EditorState &state); +static void +CreateTabsFromNetworkNodes(EditorState &state); +static void +SwitchToTab(EditorState &state, int tab_index); +static void +SyncNetworkNodesFromTabs(EditorState &state); + +static void +LogDebugV(EditorState &state, int level, const char *fmt, va_list args) +{ + if (level > state.log_level) + return; + + std::ofstream file(state.log_path.c_str(), std::ios::app); + if (!file.is_open()) + return; + + auto now = std::chrono::system_clock::now(); + auto now_time_t = std::chrono::system_clock::to_time_t(now); + std::tm tm_snapshot{}; +#if defined(_WIN32) + localtime_s(&tm_snapshot, &now_time_t); +#else + localtime_r(&now_time_t, &tm_snapshot); +#endif + + char time_buf[32]; + std::strftime(time_buf, sizeof(time_buf), "%H:%M:%S", &tm_snapshot); + file << "[" << time_buf << "] "; + + char message[1024]; + std::vsnprintf(message, sizeof(message), fmt, args); + file << message << "\n"; +} + +static void +LogDebug(EditorState &state, int level, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogDebugV(state, level, fmt, args); + va_end(args); +} + +static void +LogDebug(EditorState &state, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogDebugV(state, 5, fmt, args); + va_end(args); +} + +static bool +GetPinMeta(const RegistryNodeType *node_type, + ed::PinKind kind, + unsigned int index, + PinMeta &out) +{ + if (!node_type) + return false; + + const std::vector &pins = + (kind == ed::PinKind::Input) ? node_type->inputs : node_type->outputs; + if (index >= pins.size()) + return false; + + const int arg_index = pins[index]; + if (arg_index < 0) + { + out.label = "self"; + out.type = node_type->type; + out.connection_type = "self"; + out.is_self = true; + return true; + } + + if (static_cast(arg_index) >= node_type->arguments.size()) + { + out.label = "arg"; + out.type = ""; + out.connection_type = ""; + out.is_self = false; + return true; + } + + const auto &arg = node_type->arguments[static_cast(arg_index)]; + out.label = !arg.name.empty() ? arg.name : "arg"; + out.type = arg.type; + out.connection_type = arg.connection_type; + out.is_self = false; + return true; +} + +static bool +GetPinMetaForNode(const EditorState &state, + unsigned int node_id, + ed::PinKind kind, + unsigned int index, + PinMeta &out) +{ + auto node_it = state.nodes.find(node_id); + if (node_it == state.nodes.end()) + return false; + + RegistryNodeType node_type; + if (!GetNodeTypeForNode(state, node_it->second, node_type)) + return false; + + return GetPinMeta(&node_type, kind, index, out); +} + +static uint64_t +MakePinId(unsigned int coral_node_id, ed::PinKind kind, unsigned int index) +{ + const uint64_t editor_node_id = static_cast(coral_node_id) + 1u; + const uint64_t kind_bits = (kind == ed::PinKind::Input) ? 1u : 2u; + return (editor_node_id << 32) | (kind_bits << 24) | + (static_cast(index) & 0xFFFFFFu); +} + +static bool +DecodePinId(ed::PinId pin_id, + unsigned int &coral_node_id, + ed::PinKind &kind, + unsigned int &index) +{ + const uint64_t raw = pin_id.Get(); + if (raw == 0) + return false; + const unsigned int editor_node_id = static_cast(raw >> 32); + if (editor_node_id == 0) + return false; + coral_node_id = editor_node_id - 1u; + const uint64_t kind_bits = (raw >> 24) & 0xFFu; + if (kind_bits == 1u) + kind = ed::PinKind::Input; + else if (kind_bits == 2u) + kind = ed::PinKind::Output; + else + return false; + index = static_cast(raw & 0xFFFFFFu); + return true; +} + +static ImVec4 +GetPinDotColor(ed::PinKind kind, const PinMeta &meta) +{ + if (meta.is_self) + return ImVec4(0.2f, 0.9f, 0.3f, 1.0f); + if (meta.connection_type == "pass_through") + return ImVec4(0.9f, 0.8f, 0.2f, 1.0f); + if (kind == ed::PinKind::Input) + return ImVec4(0.2f, 0.6f, 0.9f, 1.0f); + return ImVec4(0.95f, 0.5f, 0.2f, 1.0f); +} + +static NodeColors +GetNodeColors(const RegistryNodeType *node_type) +{ + const std::string category = node_type ? node_type->node_type : ""; + const bool is_method_or_function = + category.find("method") != std::string::npos || + category.find("function") != std::string::npos; + if (category == "constructor" || category == "empty_constructor") + { + return {ImVec4(0.20f, 0.50f, 0.65f, 0.95f), + ImVec4(0.12f, 0.16f, 0.20f, 0.95f), + ImVec4(0.30f, 0.65f, 0.80f, 0.95f), + ImVec4(0.40f, 0.75f, 0.90f, 1.0f), + ImVec4(0.55f, 0.85f, 1.0f, 1.0f)}; + } + if (category == "elementary_constructor") + { + return {ImVec4(0.80f, 0.48f, 0.16f, 0.95f), + ImVec4(0.18f, 0.12f, 0.08f, 0.95f), + ImVec4(0.92f, 0.62f, 0.25f, 0.95f), + ImVec4(0.98f, 0.72f, 0.35f, 1.0f), + ImVec4(1.0f, 0.80f, 0.50f, 1.0f)}; + } + if (category == "network") + { + return {ImVec4(0.45f, 0.35f, 0.78f, 0.95f), + ImVec4(0.16f, 0.14f, 0.22f, 0.95f), + ImVec4(0.62f, 0.52f, 0.90f, 0.95f), + ImVec4(0.74f, 0.64f, 1.0f, 1.0f), + ImVec4(0.82f, 0.74f, 1.0f, 1.0f)}; + } + if (is_method_or_function) + { + return {ImVec4(0.25f, 0.60f, 0.30f, 0.95f), + ImVec4(0.10f, 0.16f, 0.10f, 0.95f), + ImVec4(0.40f, 0.72f, 0.45f, 0.95f), + ImVec4(0.52f, 0.82f, 0.56f, 1.0f), + ImVec4(0.62f, 0.90f, 0.66f, 1.0f)}; + } + + return {ImVec4(0.35f, 0.36f, 0.38f, 0.95f), + ImVec4(0.14f, 0.15f, 0.16f, 0.95f), + ImVec4(0.50f, 0.52f, 0.55f, 0.95f), + ImVec4(0.60f, 0.63f, 0.66f, 1.0f), + ImVec4(0.70f, 0.73f, 0.76f, 1.0f)}; +} + +static bool +LoadJsonFile(const std::string &path, json &out, std::string &error) +{ + std::ifstream file(path); + if (!file.is_open()) + { + error = "Failed to open " + path; + return false; + } + + try + { + file >> out; + } + catch (const std::exception &e) + { + error = std::string("JSON parse error: ") + e.what(); + return false; + } + + return true; +} + +static bool +SaveJsonFile(const std::string &path, const json &data, std::string &error) +{ + std::ofstream file(path); + if (!file.is_open()) + { + error = "Failed to open " + path; + return false; + } + + try + { + file << data.dump(2) << std::endl; + } + catch (const std::exception &e) + { + error = std::string("JSON write error: ") + e.what(); + return false; + } + + return true; +} + +static void +UpdateNextIds(EditorState &state) +{ + state.next_node_id = 1; + state.next_link_id = 1; + if (!state.nodes.empty()) + state.next_node_id = state.nodes.rbegin()->first + 1; + if (!state.links.empty()) + state.next_link_id = state.links.rbegin()->first + 1; +} + +static const RegistryNodeType * +FindNodeType(const EditorState &state, const std::string &type) +{ + auto it = state.registry.find(type); + if (it != state.registry.end()) + return &it->second; + return nullptr; +} + +static bool +GetNodeTypeForNode(const EditorState &state, + const EditorNode &node, + RegistryNodeType &out) +{ + const RegistryNodeType *base = FindNodeType(state, node.type); + if (!base) + return false; + + if (base->node_type != "network") + { + out = *base; + return true; + } + + if (node.value.empty()) + { + out = *base; + return true; + } + + try + { + json data = json::parse(node.value); + NetworkInterface iface = BuildNetworkInterface(state, data); + if (iface.valid) + { + out = iface.node_type; + out.type = base->type; + return true; + } + } + catch (const std::exception &) + {} + + out = *base; + return true; +} + +static void +SaveActiveTab(EditorState &state) +{ + if (state.tabs.empty() || state.active_tab < 0 || + state.active_tab >= static_cast(state.tabs.size())) + return; + + NetworkTab &tab = state.tabs[static_cast(state.active_tab)]; + tab.nodes = state.nodes; + tab.links = state.links; + tab.next_node_id = state.next_node_id; + tab.next_link_id = state.next_link_id; +} + +static void +LoadTab(EditorState &state, int tab_index) +{ + if (tab_index < 0 || tab_index >= static_cast(state.tabs.size())) + return; + + const NetworkTab &tab = state.tabs[static_cast(tab_index)]; + state.nodes = tab.nodes; + state.links = tab.links; + state.next_node_id = tab.next_node_id; + state.next_link_id = tab.next_link_id; + state.active_tab = tab_index; +} + +static bool +LoadRegistry(EditorState &state) +{ + json data; + std::string error; + if (!LoadJsonFile(state.registry_path, data, error)) + { + state.status = error; + return false; + } + + state.registry.clear(); + for (auto it = data.begin(); it != data.end(); ++it) + { + const std::string key = it.key(); + const auto &value = it.value(); + + RegistryNodeType node_type; + node_type.type = key; + if (value.contains("type")) + node_type.type = value.at("type").get(); + node_type.node_type = value.value("node_type", ""); + + if (value.contains("arguments") && value.at("arguments").is_array()) + { + for (const auto &arg : value.at("arguments")) + { + RegistryArgument entry; + entry.name = arg.value("name", ""); + entry.type = arg.value("type", ""); + entry.connection_type = arg.value("connection_type", ""); + node_type.arguments.push_back(entry); + } + } + + if (value.contains("inputs") && value.at("inputs").is_array()) + { + for (const auto &input : value.at("inputs")) + node_type.inputs.push_back(input.get()); + } + + if (value.contains("outputs") && value.at("outputs").is_array()) + { + for (const auto &output : value.at("outputs")) + node_type.outputs.push_back(output.get()); + } + + if (value.contains("value")) + { + if (value.at("value").is_string()) + node_type.default_value = value.at("value").get(); + else + node_type.default_value = value.at("value").dump(); + } + + state.registry[node_type.type] = node_type; + } + + state.status = + "Loaded registry: " + std::to_string(state.registry.size()) + " types"; + return true; +} + +static NetworkInterface +BuildNetworkInterface(const EditorState &state, const json &data) +{ + NetworkInterface iface; + iface.node_type.type = "coral::Network"; + iface.node_type.node_type = "network"; + + if (!data.contains("workflow")) + return iface; + const auto &workflow = data.at("workflow"); + if (!workflow.contains("nodes") || !workflow.contains("edges")) + return iface; + + struct NodeInfo + { + std::string type; + }; + struct EdgeInfo + { + unsigned int source = 0; + unsigned int target = 0; + unsigned int source_output = 0; + unsigned int target_input = 0; + }; + + std::map nodes; + for (auto it = workflow.at("nodes").begin(); it != workflow.at("nodes").end(); + ++it) + { + const unsigned int id = static_cast(std::stoul(it.key())); + const auto &value = it.value(); + NodeInfo info; + info.type = value.value("type", ""); + nodes[id] = info; + } + + std::vector edges; + for (auto it = workflow.at("edges").begin(); it != workflow.at("edges").end(); + ++it) + { + const auto &value = it.value(); + EdgeInfo edge; + edge.source = value.value("source", 0u); + edge.target = value.value("target", 0u); + edge.source_output = value.value("source_output", 0u); + edge.target_input = value.value("target_input", 0u); + edges.push_back(edge); + } + + struct Dangling + { + int input_index = -1; + int output_index = -1; + std::string type; + std::string name; + }; + + for (const auto &[node_id, info] : nodes) + { + auto reg_it = state.registry.find(info.type); + if (reg_it == state.registry.end()) + continue; + const RegistryNodeType &node_type = reg_it->second; + std::map by_argument; + + for (unsigned int i = 0; i < node_type.inputs.size(); ++i) + { + const int arg_index = node_type.inputs[i]; + bool connected = false; + for (const auto &edge : edges) + { + if (edge.target == node_id && edge.target_input == i) + { + connected = true; + break; + } + } + if (!connected) + { + auto &entry = by_argument[arg_index]; + entry.input_index = static_cast(i); + if (arg_index >= 0 && + static_cast(arg_index) < node_type.arguments.size()) + { + entry.type = node_type.arguments[arg_index].type; + entry.name = node_type.arguments[arg_index].name; + } + } + } + + for (unsigned int i = 0; i < node_type.outputs.size(); ++i) + { + const int arg_index = node_type.outputs[i]; + if (arg_index < 0) + continue; + bool connected = false; + for (const auto &edge : edges) + { + if (edge.source == node_id && edge.source_output == i) + { + connected = true; + break; + } + } + if (!connected) + { + auto &entry = by_argument[arg_index]; + entry.output_index = static_cast(i); + if (static_cast(arg_index) < node_type.arguments.size()) + { + entry.type = node_type.arguments[arg_index].type; + entry.name = node_type.arguments[arg_index].name; + } + } + } + + for (auto &pair : by_argument) + { + const int arg_index = pair.first; + Dangling &entry = pair.second; + const bool has_input = entry.input_index >= 0; + const bool has_output = entry.output_index >= 0; + if (arg_index < 0) + continue; + if (entry.type.empty()) + continue; + + RegistryArgument arg; + arg.name = entry.name; + arg.type = entry.type; + if (has_input && has_output) + arg.connection_type = "pass_through"; + else if (has_input) + arg.connection_type = "input"; + else if (has_output) + arg.connection_type = "output"; + else + continue; + + const unsigned int arg_pos = + static_cast(iface.node_type.arguments.size()); + iface.node_type.arguments.push_back(arg); + if (has_input) + iface.node_type.inputs.push_back(static_cast(arg_pos)); + if (has_output) + iface.node_type.outputs.push_back(static_cast(arg_pos)); + } + } + + iface.valid = true; + return iface; +} + +static ImVec2 +ComputeNodePosition(int node_index) +{ + const float x = 40.0f + static_cast(node_index % 4) * 280.0f; + const float y = 40.0f + static_cast(node_index / 4) * 180.0f; + return ImVec2(x, y); +} + +static bool +LoadNetworkFromJson(EditorState &state, + const json &data, + std::map &nodes, + std::map &links, + unsigned int &next_node_id, + unsigned int &next_link_id, + std::string &error) +{ + if (!data.contains("workflow")) + { + error = "Network JSON missing workflow"; + return false; + } + + const auto &workflow = data.at("workflow"); + if (!workflow.contains("nodes")) + { + error = "Network JSON missing workflow.nodes"; + return false; + } + + nodes.clear(); + links.clear(); + + int node_index = 0; + for (auto it = workflow.at("nodes").begin(); it != workflow.at("nodes").end(); + ++it) + { + const std::string key = it.key(); + const auto &value = it.value(); + + const unsigned int id = static_cast(std::stoul(key)); + EditorNode node; + node.id = id; + node.type = value.value("type", ""); + node.name = value.value("name", ""); + node.name_buffer_init = false; + if (value.contains("value")) + { + if (value.at("value").is_string()) + node.value = value.at("value").get(); + else + node.value = value.at("value").dump(); + } + else + { + auto reg_it = state.registry.find(node.type); + if (reg_it != state.registry.end()) + node.value = reg_it->second.default_value; + } + if (!node.value.empty()) + { + std::snprintf(node.value_buffer, + sizeof(node.value_buffer), + "%s", + node.value.c_str()); + node.value_buffer_init = true; + } + + bool has_position = false; + ImVec2 position(0.0f, 0.0f); + if (value.contains("position")) + { + const auto &pos = value.at("position"); + if (pos.contains("x") && pos.contains("y")) + { + position.x = pos.value("x", 0.0f); + position.y = pos.value("y", 0.0f); + has_position = true; + } + } + + nodes[id] = node; + nodes[id].desired_position = + has_position ? position : ComputeNodePosition(node_index++); + nodes[id].needs_position = true; + } + + if (workflow.contains("edges")) + { + for (auto it = workflow.at("edges").begin(); + it != workflow.at("edges").end(); + ++it) + { + const std::string key = it.key(); + const auto &value = it.value(); + + const unsigned int id = static_cast(std::stoul(key)); + EditorLink link; + link.id = id; + link.source_node = value.value("source", 0u); + link.target_node = value.value("target", 0u); + link.source_output = value.value("source_output", 0u); + link.target_input = value.value("target_input", 0u); + links[id] = link; + } + } + + next_node_id = 1; + next_link_id = 1; + if (!nodes.empty()) + next_node_id = nodes.rbegin()->first + 1; + if (!links.empty()) + next_link_id = links.rbegin()->first + 1; + + return true; +} + +static bool +LoadNetwork(EditorState &state) +{ + json data; + std::string error; + if (!LoadJsonFile(state.network_path, data, error)) + { + state.status = error; + return false; + } + + if (state.tabs.empty()) + { + NetworkTab main_tab; + main_tab.name = "Main"; + state.tabs.push_back(main_tab); + state.active_tab = 0; + } + if (state.active_tab < 0 || + state.active_tab >= static_cast(state.tabs.size())) + state.active_tab = 0; + + std::string load_error; + if (!LoadNetworkFromJson( + state, + data, + state.tabs[static_cast(state.active_tab)].nodes, + state.tabs[static_cast(state.active_tab)].links, + state.tabs[static_cast(state.active_tab)].next_node_id, + state.tabs[static_cast(state.active_tab)].next_link_id, + load_error)) + { + state.status = load_error; + return false; + } + + LoadTab(state, state.active_tab); + state.force_tab_select = true; + ++state.tab_bar_uid; + UpdateTabIndex(state); + if (state.active_tab == 0) + { + CreateTabsFromNetworkNodes(state); + UpdateTabIndex(state); + } + state.status = "Loaded network into " + + state.tabs[static_cast(state.active_tab)].name + ": " + + std::to_string(state.nodes.size()) + " nodes"; + return true; +} + +static json +BuildNetworkJsonFrom(const std::map &nodes, + const std::map &links, + const std::map ®istry) +{ + json result; + json nodes_json = json::object(); + json edges_json = json::object(); + + for (auto it = nodes.begin(); it != nodes.end(); ++it) + { + const unsigned int id = it->first; + const EditorNode &node = it->second; + + json node_json; + node_json["type"] = node.type; + if (!node.name.empty() && node.name != node.type) + node_json["name"] = node.name; + node_json["position"]["x"] = node.desired_position.x; + node_json["position"]["y"] = node.desired_position.y; + + const RegistryNodeType *node_type = nullptr; + auto reg_it = registry.find(node.type); + if (reg_it != registry.end()) + node_type = ®_it->second; + + if (node_type && (node_type->node_type == "elementary_constructor" || + node_type->node_type == "network")) + { + node_json["value"] = node.value; + } + + nodes_json[std::to_string(id)] = node_json; + } + + for (auto it = links.begin(); it != links.end(); ++it) + { + const unsigned int id = it->first; + const EditorLink &link = it->second; + + json link_json; + link_json["source"] = link.source_node; + link_json["target"] = link.target_node; + link_json["source_output"] = link.source_output; + link_json["target_input"] = link.target_input; + edges_json[std::to_string(id)] = link_json; + } + + result["workflow"]["nodes"] = nodes_json; + result["workflow"]["edges"] = edges_json; + result["version"] = 1; + result["author"] = "coral-editor"; + + auto now = std::chrono::system_clock::now(); + auto now_time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::gmtime(&now_time_t), "%FT%T.000Z"); + result["date_time_utc"] = ss.str(); + + return result; +} + +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR +static void +EnsureCoralTypesRegistered() +{ + static bool registered = false; + if (registered) + return; + coral::register_all_types(); + registered = true; +} + +static bool +LoadTriangulationForEditorNode( + EditorState &state, + const NetworkTab &tab, + const unsigned int node_id, + dealii::Triangulation<2, 2> &out, + std::map::active_cell_iterator> + &cell_by_index, + std::string &error) +{ + try + { + EnsureCoralTypesRegistered(); + json data = BuildNetworkJsonFrom(tab.nodes, tab.links, state.registry); + coral::Network net; + net.from_json(data); + net.run(); + + auto node = net.get_node(node_id); + if (!node) + { + error = "Node not found in executed network."; + return false; + } + + const auto &tria_ref = node->get>(); + out.clear(); + out.copy_triangulation(tria_ref); + + cell_by_index.clear(); + for (auto cell = out.begin_active(); cell != out.end(); ++cell) + { + cell_by_index[cell->active_cell_index()] = cell; + } + + return true; + } + catch (const std::exception &e) + { + error = e.what(); + return false; + } +} + +static void +RebuildMeshToolCaches(EditorState::MeshToolState &tool) +{ + tool.cell_tris.clear(); + tool.boundary_segs.clear(); + tool.boundary_counts.clear(); + tool.material_counts.clear(); + tool.selected_boundary_faces.clear(); + tool.selected_cells.clear(); + tool.selected_boundary_id = -1; + tool.selected_material_id = -1; + + for (auto cell = tool.tria.begin_active(); cell != tool.tria.end(); ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + const unsigned int material_id = + static_cast(cell->material_id()); + tool.material_counts[material_id] += 1u; + + const unsigned int n_vertices = cell->reference_cell().n_vertices(); + auto get_v = [&](unsigned int v) -> ImVec2 { + const auto p = cell->vertex(v); + return ImVec2(static_cast(p[0]), static_cast(p[1])); + }; + + if (n_vertices == 3) + { + tool.cell_tris.push_back( + {get_v(0), get_v(1), get_v(2), cell_index, material_id}); + } + else if (n_vertices == 4) + { + // deal.II quadrilateral vertices are typically stored in lexicographic + // order (0:(0,0), 1:(1,0), 2:(0,1), 3:(1,1)), which is not a cyclic + // polygon order. Reorder to a consistent loop: 0-1-3-2. + const ImVec2 v0 = get_v(0); + const ImVec2 v1 = get_v(1); + const ImVec2 v2 = get_v(3); + const ImVec2 v3 = get_v(2); + tool.cell_tris.push_back({v0, v1, v2, cell_index, material_id}); + tool.cell_tris.push_back({v0, v2, v3, cell_index, material_id}); + } + + for (unsigned int f = 0; f < cell->n_faces(); ++f) + { + const auto face = cell->face(f); + if (!face->at_boundary()) + continue; + + const unsigned int boundary_id = + static_cast(face->boundary_id()); + tool.boundary_counts[boundary_id] += 1u; + + const auto p0 = face->vertex(0); + const auto p1 = face->vertex(1); + tool.boundary_segs.push_back( + {ImVec2(static_cast(p0[0]), static_cast(p0[1])), + ImVec2(static_cast(p1[0]), static_cast(p1[1])), + cell_index, + f, + boundary_id}); + } + } + + tool.view_initialized = false; +} + +static ImVec2 +WorldToScreen(const EditorState::MeshToolState &tool, + const ImVec2 canvas_min, + const ImVec2 canvas_size, + const ImVec2 world) +{ + const ImVec2 canvas_center(canvas_min.x + 0.5f * canvas_size.x, + canvas_min.y + 0.5f * canvas_size.y); + const ImVec2 delta(world.x - tool.view_center.x, + world.y - tool.view_center.y); + return ImVec2(canvas_center.x + delta.x * tool.view_scale, + canvas_center.y - delta.y * tool.view_scale); +} + +static ImVec2 +ScreenToWorld(const EditorState::MeshToolState &tool, + const ImVec2 canvas_min, + const ImVec2 canvas_size, + const ImVec2 screen) +{ + const ImVec2 canvas_center(canvas_min.x + 0.5f * canvas_size.x, + canvas_min.y + 0.5f * canvas_size.y); + const ImVec2 delta(screen.x - canvas_center.x, + screen.y - canvas_center.y); + return ImVec2(tool.view_center.x + delta.x / tool.view_scale, + tool.view_center.y - delta.y / tool.view_scale); +} + +static float +DistPointToSegSq(const ImVec2 p, const ImVec2 a, const ImVec2 b) +{ + const ImVec2 ab(b.x - a.x, b.y - a.y); + const ImVec2 ap(p.x - a.x, p.y - a.y); + const float ab_len_sq = ab.x * ab.x + ab.y * ab.y; + if (ab_len_sq <= 1e-12f) + { + const float dx = p.x - a.x; + const float dy = p.y - a.y; + return dx * dx + dy * dy; + } + float t = (ap.x * ab.x + ap.y * ab.y) / ab_len_sq; + t = std::max(0.0f, std::min(1.0f, t)); + const ImVec2 proj(a.x + t * ab.x, a.y + t * ab.y); + const float dx = p.x - proj.x; + const float dy = p.y - proj.y; + return dx * dx + dy * dy; +} + +struct Box2f +{ + ImVec2 min; + ImVec2 max; +}; + +static Box2f +MakeBoxFromScreen(const ImVec2 a, const ImVec2 b) +{ + return {ImVec2(std::min(a.x, b.x), std::min(a.y, b.y)), + ImVec2(std::max(a.x, b.x), std::max(a.y, b.y))}; +} + +static bool +PointInBox(const ImVec2 p, const Box2f &box) +{ + return p.x >= box.min.x && p.x <= box.max.x && p.y >= box.min.y && + p.y <= box.max.y; +} + +static bool +SegmentIntersectsSegment(const ImVec2 a, + const ImVec2 b, + const ImVec2 c, + const ImVec2 d) +{ + auto cross = [](const ImVec2 u, const ImVec2 v) { + return u.x * v.y - u.y * v.x; + }; + const ImVec2 r(b.x - a.x, b.y - a.y); + const ImVec2 s(d.x - c.x, d.y - c.y); + const ImVec2 cma(a.x - c.x, a.y - c.y); + + const float rxs = cross(r, s); + const float qpxr = cross(ImVec2(c.x - a.x, c.y - a.y), r); + + if (std::fabs(rxs) < 1e-12f && std::fabs(qpxr) < 1e-12f) + { + auto dot = [](const ImVec2 u, const ImVec2 v) { return u.x * v.x + u.y * v.y; }; + const float rr = dot(r, r); + if (rr < 1e-12f) + return false; + const float t0 = dot(ImVec2(c.x - a.x, c.y - a.y), r) / rr; + const float t1 = dot(ImVec2(d.x - a.x, d.y - a.y), r) / rr; + const float tmin = std::min(t0, t1); + const float tmax = std::max(t0, t1); + return tmax >= 0.0f && tmin <= 1.0f; + } + + if (std::fabs(rxs) < 1e-12f) + return false; + + const float t = cross(ImVec2(c.x - a.x, c.y - a.y), s) / rxs; + const float u = cross(ImVec2(c.x - a.x, c.y - a.y), r) / rxs; + return t >= 0.0f && t <= 1.0f && u >= 0.0f && u <= 1.0f; +} + +static bool +SegmentIntersectsBox(const ImVec2 a, const ImVec2 b, const Box2f &box) +{ + if (PointInBox(a, box) || PointInBox(b, box)) + return true; + const ImVec2 r0(box.min.x, box.min.y); + const ImVec2 r1(box.max.x, box.min.y); + const ImVec2 r2(box.max.x, box.max.y); + const ImVec2 r3(box.min.x, box.max.y); + if (SegmentIntersectsSegment(a, b, r0, r1)) + return true; + if (SegmentIntersectsSegment(a, b, r1, r2)) + return true; + if (SegmentIntersectsSegment(a, b, r2, r3)) + return true; + if (SegmentIntersectsSegment(a, b, r3, r0)) + return true; + return false; +} + +static bool +PointInTri(const ImVec2 p, const ImVec2 a, const ImVec2 b, const ImVec2 c); + +static bool +TriIntersectsBox(const ImVec2 a, const ImVec2 b, const ImVec2 c, const Box2f &box) +{ + if (PointInBox(a, box) || PointInBox(b, box) || PointInBox(c, box)) + return true; + + const ImVec2 r0(box.min.x, box.min.y); + const ImVec2 r1(box.max.x, box.min.y); + const ImVec2 r2(box.max.x, box.max.y); + const ImVec2 r3(box.min.x, box.max.y); + + if (PointInTri(r0, a, b, c) || PointInTri(r1, a, b, c) || + PointInTri(r2, a, b, c) || PointInTri(r3, a, b, c)) + return true; + + if (SegmentIntersectsBox(a, b, box)) + return true; + if (SegmentIntersectsBox(b, c, box)) + return true; + if (SegmentIntersectsBox(c, a, box)) + return true; + + return false; +} + +static bool +PointInTri(const ImVec2 p, const ImVec2 a, const ImVec2 b, const ImVec2 c) +{ + const auto sign = [](const ImVec2 p1, const ImVec2 p2, const ImVec2 p3) { + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); + }; + const float d1 = sign(p, a, b); + const float d2 = sign(p, b, c); + const float d3 = sign(p, c, a); + const bool has_neg = (d1 < 0.0f) || (d2 < 0.0f) || (d3 < 0.0f); + const bool has_pos = (d1 > 0.0f) || (d2 > 0.0f) || (d3 > 0.0f); + return !(has_neg && has_pos); +} + +static void +InitMeshToolView(EditorState::MeshToolState &tool, const ImVec2 canvas_size) +{ + if (tool.cell_tris.empty() && tool.boundary_segs.empty()) + return; + + float min_x = std::numeric_limits::infinity(); + float min_y = std::numeric_limits::infinity(); + float max_x = -std::numeric_limits::infinity(); + float max_y = -std::numeric_limits::infinity(); + + auto consider = [&](const ImVec2 p) { + min_x = std::min(min_x, p.x); + min_y = std::min(min_y, p.y); + max_x = std::max(max_x, p.x); + max_y = std::max(max_y, p.y); + }; + + for (const auto &tri : tool.cell_tris) + { + consider(tri.p0); + consider(tri.p1); + consider(tri.p2); + } + for (const auto &seg : tool.boundary_segs) + { + consider(seg.p0); + consider(seg.p1); + } + + tool.view_center = ImVec2(0.5f * (min_x + max_x), 0.5f * (min_y + max_y)); + const float span_x = std::max(1e-6f, max_x - min_x); + const float span_y = std::max(1e-6f, max_y - min_y); + const float scale_x = (canvas_size.x * 0.85f) / span_x; + const float scale_y = (canvas_size.y * 0.85f) / span_y; + tool.view_scale = std::max(10.0f, std::min(scale_x, scale_y)); + tool.view_initialized = true; +} + +static void +FitMeshToolView(EditorState::MeshToolState &tool, const ImVec2 canvas_size) +{ + tool.view_initialized = false; + InitMeshToolView(tool, canvas_size); +} + +static void +DrawMeshTool(EditorState &state) +{ + auto &tool = state.mesh_tool; + if (!tool.open) + return; + + ImGui::SetNextWindowSize(ImVec2(1040.0f, 720.0f), ImGuiCond_FirstUseEver); + if (!ImGui::BeginPopupModal("Mesh Tool", &tool.open)) + return; + + if (tool.pending_load) + { + tool.pending_load = false; + tool.load_error.clear(); + tool.has_tria = false; + tool.cell_by_index.clear(); + + if (tool.target_tab_index < 0 || + tool.target_tab_index >= static_cast(state.tabs.size())) + { + tool.load_error = "Invalid tab index."; + } + else + { + auto &tab = state.tabs[static_cast(tool.target_tab_index)]; + if (LoadTriangulationForEditorNode(state, + tab, + tool.target_node_id, + tool.tria, + tool.cell_by_index, + tool.load_error)) + { + if (tool.tria.n_active_cells() == 0) + { + state.console_output += + "Mesh Tool: triangulation is empty; nothing to select.\n"; + state.console_scroll_to_bottom = true; + tool.open = false; + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + tool.has_tria = true; + RebuildMeshToolCaches(tool); + } + } + } + + if (!tool.load_error.empty()) + { + ImGui::TextUnformatted("Could not load triangulation:"); + ImGui::Separator(); + ImGui::TextWrapped("%s", tool.load_error.c_str()); + if (ImGui::Button("Close")) + tool.open = false; + ImGui::EndPopup(); + return; + } + + const float splitter_width = 6.0f; + const float min_left = 240.0f; + const float min_canvas = 420.0f; + const float full_width = ImGui::GetContentRegionAvail().x; + const float max_left = + std::max(min_left, full_width - splitter_width - min_canvas); + tool.left_panel_width = + std::max(min_left, std::min(max_left, tool.left_panel_width)); + + ImGui::BeginChild("##mesh_tool_left", + ImVec2(tool.left_panel_width, 0.0f), + true); + ImGui::TextUnformatted("Selection"); + ImGui::Separator(); + + ImGui::TextUnformatted("View"); + ImGui::Separator(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::SliderFloat("Cell edge thickness", + &tool.cell_edge_thickness, + 0.0f, + 6.0f, + "%.1f"); + tool.cell_edge_thickness = std::max(0.0f, tool.cell_edge_thickness); + ImGui::Spacing(); + + int mode_int = static_cast(tool.mode); + if (ImGui::RadioButton("Boundary faces", mode_int == 0)) + tool.mode = EditorState::MeshToolState::Mode::boundary; + if (ImGui::RadioButton("Cells", mode_int == 1)) + tool.mode = EditorState::MeshToolState::Mode::cell; + + ImGui::Spacing(); + if (tool.mode == EditorState::MeshToolState::Mode::boundary) + { + ImGui::TextUnformatted("Boundary IDs"); + ImGui::Separator(); + for (const auto &[bid, count] : tool.boundary_counts) + { + const bool selected = + tool.selected_boundary_id == static_cast(bid); + std::string label = + std::to_string(bid) + " (" + std::to_string(count) + ")"; + if (ImGui::Selectable(label.c_str(), selected)) + { + tool.selected_boundary_id = static_cast(bid); + tool.selected_boundary_faces.clear(); + for (const auto &seg : tool.boundary_segs) + { + if (seg.boundary_id == bid) + tool.selected_boundary_faces.insert( + {seg.cell_index, seg.face_no}); + } + } + } + + ImGui::Spacing(); + if (ImGui::Button("Select all faces with active ID")) + { + if (tool.selected_boundary_id >= 0) + { + tool.selected_boundary_faces.clear(); + for (const auto &seg : tool.boundary_segs) + { + if (seg.boundary_id == + static_cast(tool.selected_boundary_id)) + tool.selected_boundary_faces.insert( + {seg.cell_index, seg.face_no}); + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Clear selection")) + tool.selected_boundary_faces.clear(); + + ImGui::InputScalar("Assign boundary id", + ImGuiDataType_U32, + &tool.assign_boundary_id); + if (ImGui::Button("Apply to selection")) + { + if (!tool.selected_boundary_faces.empty()) + { + for (const auto &[cell_idx, face_no] : + tool.selected_boundary_faces) + { + auto it = tool.cell_by_index.find(cell_idx); + if (it == tool.cell_by_index.end()) + continue; + it->second->face(face_no)->set_boundary_id( + static_cast( + tool.assign_boundary_id)); + } + RebuildMeshToolCaches(tool); + } + } + if (ImGui::Button("Export selection to console")) + { + json out = json::array(); + for (const auto &[cell_idx, face_no] : + tool.selected_boundary_faces) + out.push_back({{"cell_index", cell_idx}, {"face_no", face_no}}); + state.console_output += + "MeshTool boundary selection: " + out.dump() + "\n"; + state.console_scroll_to_bottom = true; + } + } + else + { + ImGui::TextUnformatted("Material IDs"); + ImGui::Separator(); + for (const auto &[mid, count] : tool.material_counts) + { + const bool selected = + tool.selected_material_id == static_cast(mid); + std::string label = + std::to_string(mid) + " (" + std::to_string(count) + ")"; + if (ImGui::Selectable(label.c_str(), selected)) + { + tool.selected_material_id = static_cast(mid); + tool.selected_cells.clear(); + for (const auto &tri : tool.cell_tris) + { + if (tri.material_id == mid) + tool.selected_cells.insert(tri.cell_index); + } + } + } + + ImGui::Spacing(); + if (ImGui::Button("Select all cells with active ID")) + { + if (tool.selected_material_id >= 0) + { + tool.selected_cells.clear(); + for (const auto &tri : tool.cell_tris) + { + if (tri.material_id == + static_cast(tool.selected_material_id)) + tool.selected_cells.insert(tri.cell_index); + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Clear selection")) + tool.selected_cells.clear(); + + ImGui::InputScalar("Assign material id", + ImGuiDataType_U32, + &tool.assign_material_id); + if (ImGui::Button("Apply to selection")) + { + if (!tool.selected_cells.empty()) + { + for (const auto cell_idx : tool.selected_cells) + { + auto it = tool.cell_by_index.find(cell_idx); + if (it == tool.cell_by_index.end()) + continue; + it->second->set_material_id( + static_cast( + tool.assign_material_id)); + } + RebuildMeshToolCaches(tool); + } + } + if (ImGui::Button("Export selection to console")) + { + json out = json::array(); + for (const auto cell_idx : tool.selected_cells) + out.push_back(cell_idx); + state.console_output += + "MeshTool cell selection: " + out.dump() + "\n"; + state.console_scroll_to_bottom = true; + } + } + + ImGui::EndChild(); + ImGui::SameLine(0.0f, 0.0f); + + ImGui::BeginChild("##mesh_tool_splitter", + ImVec2(splitter_width, 0.0f), + false); + const float splitter_height = ImGui::GetContentRegionAvail().y; + ImGui::InvisibleButton("##mesh_tool_splitter_btn", + ImVec2(splitter_width, splitter_height)); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (ImGui::IsItemActive()) + { + tool.left_panel_width += ImGui::GetIO().MouseDelta.x; + tool.left_panel_width = + std::max(min_left, std::min(max_left, tool.left_panel_width)); + } + ImGui::EndChild(); + + ImGui::SameLine(0.0f, 0.0f); + + ImGui::BeginChild("##mesh_tool_canvas", + ImVec2(0.0f, 0.0f), + true, + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse); + const ImVec2 canvas_min = ImGui::GetCursorScreenPos(); + const ImVec2 canvas_size = ImGui::GetContentRegionAvail(); + const ImVec2 canvas_max(canvas_min.x + canvas_size.x, + canvas_min.y + canvas_size.y); + const ImVec2 mouse = ImGui::GetIO().MousePos; + const bool mouse_in_canvas = + mouse.x >= canvas_min.x && mouse.x <= canvas_max.x && + mouse.y >= canvas_min.y && mouse.y <= canvas_max.y; + + ImDrawList *dl = ImGui::GetWindowDrawList(); + // Filled triangles/quads look "striped" with ImGui's AA fill because the + // feathered edges remain visible along internal triangulation diagonals. + // Disable AA fill for the mesh canvas to make cell coloring uniform. + const ImDrawListFlags old_flags = dl->Flags; + dl->Flags &= ~ImDrawListFlags_AntiAliasedFill; + dl->AddRectFilled(canvas_min, + canvas_max, + ImGui::GetColorU32(ImVec4(0.10f, 0.10f, 0.12f, 1.0f))); + + if (!tool.view_initialized) + InitMeshToolView(tool, canvas_size); + + const bool hovered = ImGui::IsWindowHovered( + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + if (!state.text_input_active && ImGui::IsKeyPressed(ImGuiKey_F) && + mouse_in_canvas) + FitMeshToolView(tool, canvas_size); + + if (hovered && !tool.box_select_active) + { + const float wheel = ImGui::GetIO().MouseWheel; + if (wheel != 0.0f) + { + const float zoom_factor = (wheel > 0.0f) ? 1.15f : 1.0f / 1.15f; + const ImVec2 mouse = ImGui::GetIO().MousePos; + const ImVec2 before = + ScreenToWorld(tool, canvas_min, canvas_size, mouse); + tool.view_scale = std::max( + 5.0f, + std::min(5000.0f, tool.view_scale * zoom_factor)); + const ImVec2 after = + ScreenToWorld(tool, canvas_min, canvas_size, mouse); + tool.view_center.x += (before.x - after.x); + tool.view_center.y += (before.y - after.y); + } + + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right, 0.0f)) + { + const ImVec2 delta = ImGui::GetIO().MouseDelta; + tool.view_center.x -= delta.x / tool.view_scale; + tool.view_center.y += delta.y / tool.view_scale; + } + } + + // Draw cells. Prefer quads as quads to avoid diagonal seams. + for (auto cell = tool.tria.begin_active(); cell != tool.tria.end(); ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + const bool is_selected = tool.selected_cells.count(cell_index) > 0; + const ImU32 col = + is_selected ? ImGui::GetColorU32(ImVec4(0.10f, 0.75f, 0.25f, 0.45f)) + : ImGui::GetColorU32(ImVec4(0.25f, 0.25f, 0.28f, 0.20f)); + const ImU32 edge_col = + ImGui::GetColorU32(ImVec4(0.85f, 0.85f, 0.90f, 0.35f)); + const float edge_thickness = tool.cell_edge_thickness; + const bool draw_edges = edge_thickness > 0.0f; + + const unsigned int n_vertices = cell->reference_cell().n_vertices(); + auto get_v = [&](unsigned int v) -> ImVec2 { + const auto p = cell->vertex(v); + return ImVec2(static_cast(p[0]), static_cast(p[1])); + }; + + if (n_vertices == 3) + { + const ImVec2 a = + WorldToScreen(tool, canvas_min, canvas_size, get_v(0)); + const ImVec2 b = + WorldToScreen(tool, canvas_min, canvas_size, get_v(1)); + const ImVec2 c = + WorldToScreen(tool, canvas_min, canvas_size, get_v(2)); + dl->AddTriangleFilled(a, b, c, col); + if (draw_edges) + { + dl->AddLine(a, b, edge_col, edge_thickness); + dl->AddLine(b, c, edge_col, edge_thickness); + dl->AddLine(c, a, edge_col, edge_thickness); + } + } + else if (n_vertices == 4) + { + // Reorder quad vertices from lexicographic to cyclic order 0-1-3-2. + const ImVec2 a = + WorldToScreen(tool, canvas_min, canvas_size, get_v(0)); + const ImVec2 b = + WorldToScreen(tool, canvas_min, canvas_size, get_v(1)); + const ImVec2 c = + WorldToScreen(tool, canvas_min, canvas_size, get_v(3)); + const ImVec2 d = + WorldToScreen(tool, canvas_min, canvas_size, get_v(2)); + ImVec2 pts[4] = {a, b, c, d}; + dl->AddConvexPolyFilled(pts, 4, col); + if (draw_edges) + { + dl->AddLine(a, b, edge_col, edge_thickness); + dl->AddLine(b, c, edge_col, edge_thickness); + dl->AddLine(c, d, edge_col, edge_thickness); + dl->AddLine(d, a, edge_col, edge_thickness); + } + } + } + + for (const auto &seg : tool.boundary_segs) + { + const bool matches_group = + tool.selected_boundary_id >= 0 && + static_cast(tool.selected_boundary_id) == seg.boundary_id; + const bool is_selected = + tool.selected_boundary_faces.count({seg.cell_index, seg.face_no}) > 0; + ImVec4 base = matches_group ? ImVec4(0.95f, 0.75f, 0.15f, 1.0f) + : ImVec4(0.80f, 0.80f, 0.85f, 0.70f); + if (is_selected) + base = ImVec4(1.0f, 0.25f, 0.25f, 1.0f); + const ImU32 col = ImGui::GetColorU32(base); + const ImVec2 a = WorldToScreen(tool, canvas_min, canvas_size, seg.p0); + const ImVec2 b = WorldToScreen(tool, canvas_min, canvas_size, seg.p1); + dl->AddLine(a, b, col, is_selected ? 3.0f : 2.0f); + } + + if (hovered && mouse_in_canvas && + ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !state.text_input_active) + { + tool.box_select_active = true; + tool.box_start_screen = mouse; + tool.box_end_screen = mouse; + } + + if (tool.box_select_active && ImGui::IsMouseDown(ImGuiMouseButton_Left)) + tool.box_end_screen = mouse; + + if (tool.box_select_active && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) + { + const ImVec2 delta(tool.box_end_screen.x - tool.box_start_screen.x, + tool.box_end_screen.y - tool.box_start_screen.y); + const float drag_dist_sq = delta.x * delta.x + delta.y * delta.y; + const bool is_box = drag_dist_sq > 16.0f; // >4px + + const bool add_mode = ImGui::GetIO().KeyShift; + const bool subtract_mode = ImGui::GetIO().KeyAlt; + const bool replace_mode = !add_mode && !subtract_mode; + + if (is_box) + { + const ImVec2 w0 = + ScreenToWorld(tool, canvas_min, canvas_size, tool.box_start_screen); + const ImVec2 w1 = + ScreenToWorld(tool, canvas_min, canvas_size, tool.box_end_screen); + const Box2f world_box = MakeBoxFromScreen(w0, w1); + + if (tool.mode == EditorState::MeshToolState::Mode::boundary) + { + if (replace_mode) + tool.selected_boundary_faces.clear(); + + for (const auto &seg : tool.boundary_segs) + { + if (!SegmentIntersectsBox(seg.p0, seg.p1, world_box)) + continue; + const auto key = std::make_pair(seg.cell_index, seg.face_no); + if (subtract_mode) + tool.selected_boundary_faces.erase(key); + else + tool.selected_boundary_faces.insert(key); + } + } + else + { + if (replace_mode) + tool.selected_cells.clear(); + + for (const auto &tri : tool.cell_tris) + { + if (!TriIntersectsBox(tri.p0, tri.p1, tri.p2, world_box)) + continue; + if (subtract_mode) + tool.selected_cells.erase(tri.cell_index); + else + tool.selected_cells.insert(tri.cell_index); + } + } + } + else + { + // Click selection. + const ImVec2 world = + ScreenToWorld(tool, canvas_min, canvas_size, tool.box_start_screen); + + if (tool.mode == EditorState::MeshToolState::Mode::boundary) + { + const float pick_radius_px = 6.0f; + const float pick_radius_w = pick_radius_px / tool.view_scale; + const float pick_radius_sq = pick_radius_w * pick_radius_w; + int best = -1; + float best_d = std::numeric_limits::infinity(); + for (int i = 0; + i < static_cast(tool.boundary_segs.size()); + ++i) + { + const auto &seg = + tool.boundary_segs[static_cast(i)]; + const float d = DistPointToSegSq(world, seg.p0, seg.p1); + if (d < best_d) + { + best_d = d; + best = i; + } + } + if (best >= 0 && best_d <= pick_radius_sq) + { + const auto &seg = + tool.boundary_segs[static_cast(best)]; + tool.selected_boundary_id = static_cast(seg.boundary_id); + const auto key = std::make_pair(seg.cell_index, seg.face_no); + if (subtract_mode) + tool.selected_boundary_faces.erase(key); + else if (!add_mode) + { + tool.selected_boundary_faces.clear(); + tool.selected_boundary_faces.insert(key); + } + else if (tool.selected_boundary_faces.count(key) > 0) + tool.selected_boundary_faces.erase(key); + else + tool.selected_boundary_faces.insert(key); + } + } + else + { + int best_cell = -1; + unsigned int best_mat = 0; + for (const auto &tri : tool.cell_tris) + { + if (PointInTri(world, tri.p0, tri.p1, tri.p2)) + { + best_cell = static_cast(tri.cell_index); + best_mat = tri.material_id; + break; + } + } + if (best_cell >= 0) + { + const unsigned int cell_idx = + static_cast(best_cell); + tool.selected_material_id = static_cast(best_mat); + if (subtract_mode) + tool.selected_cells.erase(cell_idx); + else if (!add_mode) + { + tool.selected_cells.clear(); + tool.selected_cells.insert(cell_idx); + } + else if (tool.selected_cells.count(cell_idx) > 0) + tool.selected_cells.erase(cell_idx); + else + tool.selected_cells.insert(cell_idx); + } + } + } + + tool.box_select_active = false; + } + + if (tool.box_select_active) + { + const Box2f rect = + MakeBoxFromScreen(tool.box_start_screen, tool.box_end_screen); + dl->AddRectFilled(rect.min, + rect.max, + ImGui::GetColorU32(ImVec4(0.20f, 0.60f, 0.95f, 0.12f))); + dl->AddRect(rect.min, + rect.max, + ImGui::GetColorU32(ImVec4(0.20f, 0.60f, 0.95f, 0.85f)), + 0.0f, + 0, + 1.5f); + } + + dl->AddText(ImVec2(canvas_min.x + 8.0f, canvas_min.y + 8.0f), + ImGui::GetColorU32(ImVec4(1, 1, 1, 0.85f)), + "F fit view | LMB select/box | Shift add | Alt subtract | RMB pan | wheel zoom"); + + dl->Flags = old_flags; + ImGui::EndChild(); + + ImGui::EndPopup(); +} + +#endif // CORAL_NODE_EDITOR_ENABLE_MANIPULATOR + +static json +BuildNetworkJson(const EditorState &state) +{ + return BuildNetworkJsonFrom(state.nodes, state.links, state.registry); +} + +static void +UpdateTabIndex(EditorState &state) +{ + state.tab_name_index.clear(); + for (size_t i = 0; i < state.tabs.size(); ++i) + state.tab_name_index[state.tabs[i].name] = static_cast(i); +} + +static int +FindTabByName(const EditorState &state, const std::string &name) +{ + auto it = state.tab_name_index.find(name); + if (it == state.tab_name_index.end()) + return -1; + return it->second; +} + +static void +DeleteModule(EditorState &state, const std::string &module_name) +{ + if (module_name.empty() || state.tabs.size() <= 1) + return; + + const int tab_index = FindTabByName(state, module_name); + if (tab_index <= 0) + return; + + // Remove all module nodes from the main tab. + NetworkTab &main_tab = state.tabs[0]; + std::vector nodes_to_remove; + for (const auto &pair : main_tab.nodes) + { + const EditorNode &node = pair.second; + if (node.type == "coral::Network" && node.name == module_name) + nodes_to_remove.push_back(pair.first); + } + for (unsigned int id : nodes_to_remove) + { + main_tab.nodes.erase(id); + for (auto it = main_tab.links.begin(); it != main_tab.links.end();) + { + const EditorLink &link = it->second; + if (link.source_node == id || link.target_node == id) + it = main_tab.links.erase(it); + else + ++it; + } + } + if (!nodes_to_remove.empty()) + { + if (!main_tab.nodes.empty()) + main_tab.next_node_id = main_tab.nodes.rbegin()->first + 1; + else + main_tab.next_node_id = 1; + if (!main_tab.links.empty()) + main_tab.next_link_id = main_tab.links.rbegin()->first + 1; + else + main_tab.next_link_id = 1; + } + + // Switch away if we are deleting the active tab (or any tab before it). + const int old_active = state.active_tab; + if (old_active == tab_index) + LoadTab(state, 0); + + state.tabs.erase(state.tabs.begin() + tab_index); + UpdateTabIndex(state); + + if (old_active > tab_index) + { + state.active_tab = old_active - 1; + LoadTab(state, state.active_tab); + } + + if (state.active_tab == 0) + state.nodes = main_tab.nodes; + + state.force_tab_select = true; + state.request_focus = true; + state.status = "Deleted module: " + module_name; +} + +static void +RenameModule(EditorState &state, + const std::string &old_name, + const std::string &new_name) +{ + if (old_name.empty() || new_name.empty() || old_name == new_name) + return; + if (new_name == "Main") + { + state.status = "Cannot rename module to Main"; + return; + } + + const int tab_index = FindTabByName(state, old_name); + if (tab_index <= 0) + return; + if (FindTabByName(state, new_name) >= 0) + { + state.status = "Module name already exists: " + new_name; + return; + } + + state.tabs[static_cast(tab_index)].name = new_name; + + NetworkTab &main_tab = state.tabs[0]; + for (auto &pair : main_tab.nodes) + { + EditorNode &node = pair.second; + if (node.type == "coral::Network" && node.name == old_name) + { + node.name = new_name; + std::snprintf(node.name_buffer, + sizeof(node.name_buffer), + "%s", + node.name.c_str()); + node.name_buffer_init = true; + } + } + + UpdateTabIndex(state); + state.force_tab_select = true; + state.request_focus = true; + state.status = "Renamed module: " + old_name + " -> " + new_name; +} + +static void +SwitchToTab(EditorState &state, int tab_index) +{ + if (tab_index < 0 || tab_index >= static_cast(state.tabs.size())) + return; + if (tab_index == state.active_tab) + return; + + SaveActiveTab(state); + LoadTab(state, tab_index); + SyncNetworkNodesFromTabs(state); + state.force_tab_select = true; + state.request_focus = true; +} + +static std::string +SerializeTab(const NetworkTab &tab, const EditorState &state) +{ + return BuildNetworkJsonFrom(tab.nodes, tab.links, state.registry).dump(); +} + +static void +SyncNetworkNodesFromTabs(EditorState &state) +{ + if (state.tabs.empty()) + return; + + NetworkTab &main_tab = state.tabs[0]; + for (size_t i = 1; i < state.tabs.size(); ++i) + { + const NetworkTab &tab = state.tabs[i]; + const std::string value = SerializeTab(tab, state); + for (auto &pair : main_tab.nodes) + { + EditorNode &node = pair.second; + if (node.type == "coral::Network" && node.name == tab.name) + { + node.value = value; + std::snprintf(node.value_buffer, + sizeof(node.value_buffer), + "%s", + node.value.c_str()); + node.value_buffer_init = true; + } + } + } + + if (state.active_tab == 0) + state.nodes = main_tab.nodes; +} + +static void +CreateTabsFromNetworkNodes(EditorState &state) +{ + if (state.tabs.empty()) + return; + + std::vector network_nodes; + network_nodes.reserve(state.tabs[0].nodes.size()); + for (const auto &pair : state.tabs[0].nodes) + { + const EditorNode &node = pair.second; + if (node.type != "coral::Network") + continue; + if (node.name.empty()) + continue; + if (state.tab_name_index.find(node.name) != state.tab_name_index.end()) + continue; + network_nodes.push_back(node); + } + + for (const auto &node : network_nodes) + { + NetworkTab tab; + tab.name = node.name; + if (!node.value.empty()) + { + try + { + json data = json::parse(node.value); + std::string error; + if (LoadNetworkFromJson(state, + data, + tab.nodes, + tab.links, + tab.next_node_id, + tab.next_link_id, + error)) + state.tabs.push_back(tab); + } + catch (const std::exception &) + { + state.tabs.push_back(tab); + } + } + else + { + state.tabs.push_back(tab); + } + } +} + +static unsigned int +AddNodeToTab(EditorState &state, + NetworkTab &tab, + const std::string &type, + const std::string &display_name, + const std::string &value_override) +{ + const unsigned int id = tab.next_node_id++; + EditorNode node; + node.id = id; + node.type = type; + node.name_buffer_init = false; + if (!display_name.empty()) + node.name = display_name; + + if (!value_override.empty()) + node.value = value_override; + else + { + const RegistryNodeType *node_type = FindNodeType(state, type); + if (node_type && !node_type->default_value.empty()) + node.value = node_type->default_value; + } + + if (!node.value.empty()) + { + std::snprintf(node.value_buffer, + sizeof(node.value_buffer), + "%s", + node.value.c_str()); + node.value_buffer_init = true; + } + + tab.nodes[id] = node; + tab.nodes[id].desired_position = + ComputeNodePosition(static_cast(tab.nodes.size()) - 1); + tab.nodes[id].needs_position = true; + return id; +} + +static unsigned int +AddNodeFromType(EditorState &state, + const std::string &type, + const std::string &display_name = std::string(), + const std::string &value_override = std::string()) +{ + const unsigned int id = state.next_node_id++; + EditorNode node; + node.id = id; + node.type = type; + node.name_buffer_init = false; + if (!display_name.empty()) + node.name = display_name; + + if (!value_override.empty()) + node.value = value_override; + else + { + const RegistryNodeType *node_type = FindNodeType(state, type); + if (node_type && !node_type->default_value.empty()) + node.value = node_type->default_value; + } + + if (!node.value.empty()) + { + std::snprintf(node.value_buffer, + sizeof(node.value_buffer), + "%s", + node.value.c_str()); + node.value_buffer_init = true; + } + + state.nodes[id] = node; + state.nodes[id].desired_position = + ComputeNodePosition(static_cast(state.nodes.size()) - 1); + state.nodes[id].needs_position = true; + return id; +} + +static bool +IsInputConnected(const EditorState &state, + unsigned int node_id, + unsigned int input_index) +{ + for (auto it = state.links.begin(); it != state.links.end(); ++it) + { + const EditorLink &link = it->second; + if (link.target_node == node_id && link.target_input == input_index) + return true; + } + return false; +} + +static bool +FindSelfPin(const RegistryNodeType *node_type, + ed::PinKind &kind, + unsigned int &index) +{ + if (!node_type) + return false; + + for (unsigned int i = 0; i < node_type->outputs.size(); ++i) + { + if (node_type->outputs[i] < 0) + { + kind = ed::PinKind::Output; + index = i; + return true; + } + } + + for (unsigned int i = 0; i < node_type->inputs.size(); ++i) + { + if (node_type->inputs[i] < 0) + { + kind = ed::PinKind::Input; + index = i; + return true; + } + } + + return false; +} + +static void +DrawPin(const RegistryNodeType *node_type, + unsigned int node_id, + ed::PinKind kind, + unsigned int index, + bool align_right) +{ + PinMeta meta; + if (!GetPinMeta(node_type, kind, index, meta)) + return; + + const float dot_size = 8.0f; + + ed::BeginPin(ed::PinId(MakePinId(node_id, kind, index)), kind); + if (kind == ed::PinKind::Input) + ed::PinPivotAlignment(ImVec2(0.0f, 0.5f)); + else + ed::PinPivotAlignment(ImVec2(1.0f, 0.5f)); + ed::PinPivotSize(ImVec2(0.0f, 0.0f)); + + ImGuiColorEditFlags color_flags = + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop | + ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoInputs | + ImGuiColorEditFlags_NoLabel; + const ImVec4 color = GetPinDotColor(kind, meta); + char color_id[64]; + std::snprintf(color_id, + sizeof(color_id), + "##pin_dot_%u_%u_%u", + node_id, + kind == ed::PinKind::Input ? 0u : 1u, + index); + + if (kind == ed::PinKind::Input) + { + ImGui::ColorButton(color_id, + color, + color_flags, + ImVec2(dot_size, dot_size)); + ImGui::SameLine(); + ImGui::TextUnformatted(meta.label.c_str()); + } + else + { + ImGui::TextUnformatted(meta.label.c_str()); + ImGui::SameLine(); + ImGui::ColorButton(color_id, + color, + color_flags, + ImVec2(dot_size, dot_size)); + } + + ed::EndPin(); +} + +static void +DrawSelfPinInline(const RegistryNodeType *node_type, + unsigned int node_id, + ed::PinKind kind, + unsigned int index) +{ + PinMeta meta; + if (!GetPinMeta(node_type, kind, index, meta)) + return; + + const float dot_size = 8.0f; + ed::BeginPin(ed::PinId(MakePinId(node_id, kind, index)), kind); + if (kind == ed::PinKind::Input) + ed::PinPivotAlignment(ImVec2(0.0f, 0.5f)); + else + ed::PinPivotAlignment(ImVec2(1.0f, 0.5f)); + ed::PinPivotSize(ImVec2(0.0f, 0.0f)); + ImGuiColorEditFlags color_flags = + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop | + ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoInputs | + ImGuiColorEditFlags_NoLabel; + const ImVec4 color = GetPinDotColor(kind, meta); + char color_id[64]; + std::snprintf(color_id, + sizeof(color_id), + "##pin_self_%u_%u", + node_id, + kind == ed::PinKind::Input ? 0u : 1u); + + if (kind == ed::PinKind::Input) + { + ImGui::ColorButton(color_id, + color, + color_flags, + ImVec2(dot_size, dot_size)); + } + else + { + ImGui::ColorButton(color_id, + color, + color_flags, + ImVec2(dot_size, dot_size)); + } + ed::EndPin(); +} + +static void +DrawHoverTooltips(const EditorState &state) +{ + const ed::PinId hovered_pin = ed::GetHoveredPin(); + if (hovered_pin) + { + unsigned int coral_id = 0; + unsigned int index = 0; + ed::PinKind kind = ed::PinKind::Input; + if (DecodePinId(hovered_pin, coral_id, kind, index)) + { + PinMeta meta; + if (GetPinMetaForNode(state, coral_id, kind, index, meta)) + { + ImGui::BeginTooltip(); + ImGui::Text("Type: %s", + meta.type.empty() ? "" : meta.type.c_str()); + ImGui::EndTooltip(); + return; + } + } + } + + const ed::NodeId hovered_node = ed::GetHoveredNode(); + if (hovered_node) + { + const unsigned int editor_id = + static_cast(hovered_node.Get()); + if (editor_id > 0) + { + const unsigned int coral_id = editor_id - 1u; + auto it = state.nodes.find(coral_id); + if (it != state.nodes.end()) + { + ImGui::BeginTooltip(); + ImGui::Text("Type: %s", it->second.type.c_str()); + ImGui::EndTooltip(); + } + } + } +} + +static void +DrawNode(EditorState &state, EditorNode &node) +{ + RegistryNodeType resolved_type; + const RegistryNodeType *node_type = nullptr; + if (GetNodeTypeForNode(state, node, resolved_type)) + node_type = &resolved_type; + + LogDebug( + state, 10, "BeginNode coral_id=%u editor_id=%u", node.id, node.id + 1u); + const NodeColors node_colors = GetNodeColors(node_type); + ed::PushStyleColor(ed::StyleColor_NodeBg, node_colors.body); + ed::PushStyleColor(ed::StyleColor_NodeBorder, node_colors.border); + ed::PushStyleColor(ed::StyleColor_HovNodeBorder, node_colors.border_hover); + ed::PushStyleColor(ed::StyleColor_SelNodeBorder, node_colors.border_selected); + ed::BeginNode(ed::NodeId(node.id + 1u)); + if (!node.name_buffer_init) + { + const std::string &initial_name = + node.name.empty() ? node.type : node.name; + std::snprintf(node.name_buffer, + sizeof(node.name_buffer), + "%s", + initial_name.c_str()); + node.name_buffer_init = true; + } + + const bool has_value_field = + node_type && node_type->node_type == "elementary_constructor"; + if (has_value_field && !node.value_buffer_init) + { + std::snprintf(node.value_buffer, + sizeof(node.value_buffer), + "%s", + node.value.c_str()); + node.value_buffer_init = true; + } + + std::vector input_indices; + std::vector output_indices; + if (node_type) + { + input_indices.reserve(node_type->inputs.size()); + output_indices.reserve(node_type->outputs.size()); + for (unsigned int i = 0; i < node_type->inputs.size(); ++i) + { + PinMeta meta; + if (GetPinMeta(node_type, ed::PinKind::Input, i, meta) && + meta.is_self) + continue; + input_indices.push_back(i); + } + for (unsigned int i = 0; i < node_type->outputs.size(); ++i) + { + PinMeta meta; + if (GetPinMeta(node_type, ed::PinKind::Output, i, meta) && + meta.is_self) + continue; + output_indices.push_back(i); + } + } + + const ImVec4 header_color = node_colors.header; + const float min_name_width = 120.0f; + const float name_text_width = ImGui::CalcTextSize(node.name_buffer).x + 16.0f; + const float name_width = std::max(min_name_width, name_text_width); + ed::PinKind self_kind = ed::PinKind::Output; + unsigned int self_index = 0; + PinMeta self_meta; + const bool has_self_pin = + FindSelfPin(node_type, self_kind, self_index) && + GetPinMeta(node_type, self_kind, self_index, self_meta); + const float dot_size = 8.0f; + const float self_width = has_self_pin ? + ImGui::CalcTextSize(self_meta.label.c_str()).x + + ImGui::GetStyle().ItemSpacing.x + dot_size : + 0.0f; + + float max_input_width = 0.0f; + float max_output_width = 0.0f; + if (node_type) + { + const float spacing = ImGui::GetStyle().ItemSpacing.x; + for (unsigned int idx : input_indices) + { + PinMeta meta; + if (GetPinMeta(node_type, ed::PinKind::Input, idx, meta)) + { + const float width = + ImGui::CalcTextSize(meta.label.c_str()).x + dot_size + spacing; + if (width > max_input_width) + max_input_width = width; + } + } + for (unsigned int idx : output_indices) + { + PinMeta meta; + if (GetPinMeta(node_type, ed::PinKind::Output, idx, meta)) + { + const float width = + ImGui::CalcTextSize(meta.label.c_str()).x + dot_size + spacing; + if (width > max_output_width) + max_output_width = width; + } + } + } + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float pins_width = + max_input_width + max_output_width + + (max_input_width > 0.0f && max_output_width > 0.0f ? spacing : 0.0f); + float value_width = 0.0f; + if (has_value_field) + { + const float text_width = ImGui::CalcTextSize(node.value_buffer).x + 16.0f; + value_width = std::max(min_name_width, text_width); + } + const float header_width = + std::max(name_width + (has_self_pin ? spacing + self_width : 0.0f), + std::max(pins_width, value_width)); + const ImVec2 header_min = ImGui::GetCursorScreenPos(); + const float header_height = ImGui::GetFrameHeight(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(header_min, + ImVec2(header_min.x + header_width, + header_min.y + header_height), + ImGui::ColorConvertFloat4ToU32(header_color), + 6.0f, + ImDrawFlags_RoundCornersTop); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + const float name_input_width = + header_width - (has_self_pin ? spacing + dot_size : 0.0f); + ImGui::SetNextItemWidth(name_input_width); + std::string name_label = "##name_" + std::to_string(node.id); + if (ImGui::InputText(name_label.c_str(), + node.name_buffer, + sizeof(node.name_buffer))) + { + const std::string new_name = node.name_buffer; + if (new_name.empty() || new_name == node.type) + node.name.clear(); + else + node.name = new_name; + } + if (ImGui::IsItemActive()) + state.text_input_active = true; + if (has_self_pin) + { + ImGui::SameLine(0.0f, spacing); + DrawSelfPinInline(node_type, node.id, self_kind, self_index); + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + ImGui::Spacing(); + + if (has_value_field) + { + std::string value_label = "##value" + std::to_string(node.id); + ImGui::SetNextItemWidth(header_width); + if (ImGui::InputText(value_label.c_str(), + node.value_buffer, + sizeof(node.value_buffer))) + node.value = node.value_buffer; + if (ImGui::IsItemActive()) + state.text_input_active = true; + } + +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR + const bool is_triangulation_2d = + node.type.find("dealii::Triangulation<2") != std::string::npos; + if (is_triangulation_2d) + { + std::string button_id = "Mesh Tool##mesh_tool_" + std::to_string(node.id); + if (ImGui::Button(button_id.c_str())) + { + state.mesh_tool.open = true; + state.mesh_tool.pending_load = true; + state.mesh_tool.target_node_id = node.id; + state.mesh_tool.target_tab_index = state.active_tab; + } + } +#endif + + if (node_type) + { + const size_t row_count = + std::max(input_indices.size(), output_indices.size()); + if (row_count > 0) + { + std::string table_id = "##pins_" + std::to_string(node.id); + if (ImGui::BeginTable(table_id.c_str(), + 2, + ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_NoPadOuterX)) + { + ImGui::TableSetupColumn("Inputs", + ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Outputs", + ImGuiTableColumnFlags_WidthFixed); + for (size_t row = 0; row < row_count; ++row) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (row < input_indices.size()) + DrawPin(node_type, + node.id, + ed::PinKind::Input, + input_indices[row], + false); + ImGui::TableSetColumnIndex(1); + if (row < output_indices.size()) + DrawPin(node_type, + node.id, + ed::PinKind::Output, + output_indices[row], + true); + } + ImGui::EndTable(); + } + } + } + + ed::EndNode(); + ed::PopStyleColor(4); + LogDebug(state, 10, "EndNode coral_id=%u", node.id); +} + +static void +HandleLinkCreation(EditorState &state) +{ + LogDebug(state, 10, "BeginCreate()"); + if (ed::BeginCreate()) + { + ed::PinId start_pin; + ed::PinId end_pin; + if (ed::QueryNewLink(&start_pin, &end_pin)) + { + LogDebug(state, + "QueryNewLink start=%llu end=%llu", + static_cast(start_pin.Get()), + static_cast(end_pin.Get())); + unsigned int start_node = 0; + unsigned int start_index = 0; + ed::PinKind start_kind = ed::PinKind::Input; + unsigned int end_node = 0; + unsigned int end_index = 0; + ed::PinKind end_kind = ed::PinKind::Input; + + if (DecodePinId(start_pin, start_node, start_kind, start_index) && + DecodePinId(end_pin, end_node, end_kind, end_index)) + { + unsigned int source_node = 0; + unsigned int source_output = 0; + unsigned int target_node = 0; + unsigned int target_input = 0; + + if (start_kind == ed::PinKind::Output && + end_kind == ed::PinKind::Input) + { + source_node = start_node; + source_output = start_index; + target_node = end_node; + target_input = end_index; + } + else if (start_kind == ed::PinKind::Input && + end_kind == ed::PinKind::Output) + { + source_node = end_node; + source_output = end_index; + target_node = start_node; + target_input = start_index; + } + + const bool can_create = + (start_kind != end_kind) && + !IsInputConnected(state, target_node, target_input); + + bool types_match = false; + PinMeta source_meta; + PinMeta target_meta; + if (start_kind != end_kind) + { + const bool have_types = GetPinMetaForNode(state, + source_node, + ed::PinKind::Output, + source_output, + source_meta) && + GetPinMetaForNode(state, + target_node, + ed::PinKind::Input, + target_input, + target_meta); + types_match = have_types && !source_meta.type.empty() && + (source_meta.type == target_meta.type); + } + + LogDebug( + state, + "Link candidate source=%u out=%u target=%u in=%u can_create=%d", + source_node, + source_output, + target_node, + target_input, + can_create ? 1 : 0); + + if (start_kind != end_kind && !types_match) + { + state.status = "Link rejected: type mismatch (" + + source_meta.type + " -> " + target_meta.type + + ")"; + ed::RejectNewItem(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), 2.0f); + } + + if (can_create && types_match && ed::AcceptNewItem()) + { + EditorLink link; + link.id = state.next_link_id++; + link.source_node = source_node; + link.target_node = target_node; + link.source_output = source_output; + link.target_input = target_input; + state.links[link.id] = link; + LogDebug(state, "Link created id=%u", link.id); + + auto source_it = state.nodes.find(source_node); + if (source_it != state.nodes.end()) + { + EditorNode &source = source_it->second; + const bool is_default_name = + source.name.empty() || source.name == source.type; + if (is_default_name) + { + PinMeta target_meta; + if (GetPinMetaForNode(state, + target_node, + ed::PinKind::Input, + target_input, + target_meta) && + !target_meta.label.empty() && + target_meta.label != "self") + { + source.name = target_meta.label; + std::snprintf(source.name_buffer, + sizeof(source.name_buffer), + "%s", + source.name.c_str()); + source.name_buffer_init = true; + } + } + } + } + } + } + } + + ed::EndCreate(); + LogDebug(state, 10, "EndCreate()"); +} + +static void +RemoveNode(EditorState &state, unsigned int coral_node_id) +{ + std::string maybe_module_name; + { + auto it = state.nodes.find(coral_node_id); + if (it != state.nodes.end()) + { + const EditorNode &node = it->second; + if (state.active_tab == 0 && node.type == "coral::Network" && + !node.name.empty() && FindTabByName(state, node.name) > 0) + maybe_module_name = node.name; + } + } + + for (auto it = state.links.begin(); it != state.links.end();) + { + const EditorLink &link = it->second; + if (link.source_node == coral_node_id || + link.target_node == coral_node_id) + it = state.links.erase(it); + else + ++it; + } + state.nodes.erase(coral_node_id); + UpdateNextIds(state); + + if (!maybe_module_name.empty()) + { + bool any_remaining = false; + for (const auto &pair : state.nodes) + { + const EditorNode &node = pair.second; + if (node.type == "coral::Network" && node.name == maybe_module_name) + { + any_remaining = true; + break; + } + } + if (!any_remaining) + { + state.module_delete_candidate = maybe_module_name; + state.show_module_delete_popup = true; + } + } +} + +static void +HandleDeletion(EditorState &state) +{ + LogDebug(state, 10, "BeginDelete()"); + if (ed::BeginDelete()) + { + ed::NodeId node_id; + while (ed::QueryDeletedNode(&node_id)) + { + if (ed::AcceptDeletedItem()) + { + const unsigned int editor_id = + static_cast(node_id.Get()); + if (editor_id > 0) + { + const unsigned int coral_id = editor_id - 1u; + RemoveNode(state, coral_id); + LogDebug(state, "Node deleted editor_id=%u", editor_id); + } + } + } + + ed::LinkId link_id; + while (ed::QueryDeletedLink(&link_id)) + { + if (ed::AcceptDeletedItem()) + { + const unsigned int editor_id = + static_cast(link_id.Get()); + if (editor_id > 0) + state.links.erase(editor_id - 1u); + LogDebug(state, "Link deleted editor_id=%u", editor_id); + } + } + } + + ed::EndDelete(); + LogDebug(state, 10, "EndDelete()"); +} + +static void +DrawEditor(EditorState &state) +{ + LogDebug(state, 10, "Editor Begin"); + ed::Begin("Node Editor"); + state.text_input_active = false; + + static ed::NodeId context_node_id; + static ed::LinkId context_link_id; + ed::Suspend(); + if (ed::ShowNodeContextMenu(&context_node_id)) + ImGui::OpenPopup("Node Context"); + else if (ed::ShowLinkContextMenu(&context_link_id)) + ImGui::OpenPopup("Link Context"); + else if (ed::ShowBackgroundContextMenu()) + ImGui::OpenPopup("Background Context"); + ed::Resume(); + + ed::Suspend(); + if (ImGui::BeginPopup("Node Context")) + { + if (ImGui::MenuItem("Delete Node")) + { + ed::DeleteNode(context_node_id); + const unsigned int editor_id = + static_cast(context_node_id.Get()); + if (editor_id > 0) + RemoveNode(state, editor_id - 1u); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("Link Context")) + { + if (ImGui::MenuItem("Delete Link")) + { + ed::DeleteLink(context_link_id); + const unsigned int editor_id = + static_cast(context_link_id.Get()); + if (editor_id > 0) + state.links.erase(editor_id - 1u); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("Background Context")) + { + if (ImGui::MenuItem("Delete Selected")) + state.request_delete = true; + ImGui::EndPopup(); + } + ed::Resume(); + + for (auto it = state.nodes.begin(); it != state.nodes.end(); ++it) + { + EditorNode &node = it->second; + if (node.needs_position) + { + ed::SetNodePosition(ed::NodeId(node.id + 1u), node.desired_position); + node.needs_position = false; + } + } + + for (auto it = state.nodes.begin(); it != state.nodes.end(); ++it) + DrawNode(state, it->second); + + for (auto it = state.links.begin(); it != state.links.end(); ++it) + { + const unsigned int id = it->first; + const EditorLink &link = it->second; + const auto output_pin = ed::PinId( + MakePinId(link.source_node, ed::PinKind::Output, link.source_output)); + const auto input_pin = ed::PinId( + MakePinId(link.target_node, ed::PinKind::Input, link.target_input)); + ed::Link(ed::LinkId(id + 1u), output_pin, input_pin); + } + + HandleLinkCreation(state); + HandleDeletion(state); + DrawHoverTooltips(state); + if (state.request_focus) + { + ed::NavigateToContent(); + state.request_focus = false; + } + + const bool delete_requested = state.request_delete; + state.request_delete = false; + if (delete_requested && !state.text_input_active) + { + bool deleted_any = false; + const int selected_nodes = ed::GetSelectedNodes(nullptr, 0); + if (selected_nodes > 0) + { + std::vector nodes(static_cast(selected_nodes)); + ed::GetSelectedNodes(nodes.data(), selected_nodes); + for (const ed::NodeId &node_id : nodes) + { + ed::DeleteNode(node_id); + const unsigned int editor_id = + static_cast(node_id.Get()); + if (editor_id > 0) + RemoveNode(state, editor_id - 1u); + deleted_any = true; + } + } + + const int selected_links = ed::GetSelectedLinks(nullptr, 0); + if (selected_links > 0) + { + std::vector links(static_cast(selected_links)); + ed::GetSelectedLinks(links.data(), selected_links); + for (const ed::LinkId &link_id : links) + { + ed::DeleteLink(link_id); + const unsigned int editor_id = + static_cast(link_id.Get()); + if (editor_id > 0) + state.links.erase(editor_id - 1u); + deleted_any = true; + } + } + if (!deleted_any) + state.status = "Nothing selected to delete"; + } + + if (ed::HasSelectionChanged()) + { + const int selected_nodes = ed::GetSelectedNodes(nullptr, 0); + const int selected_links = ed::GetSelectedLinks(nullptr, 0); + LogDebug(state, + "Selection changed nodes=%d links=%d", + selected_nodes, + selected_links); + if (selected_nodes > 0) + { + ed::NodeId nodes[8]; + const int count = ed::GetSelectedNodes(nodes, 8); + for (int i = 0; i < count; ++i) + LogDebug(state, + " selected node editor_id=%llu", + static_cast(nodes[i].Get())); + } + if (selected_links > 0) + { + ed::LinkId links[8]; + const int count = ed::GetSelectedLinks(links, 8); + for (int i = 0; i < count; ++i) + LogDebug(state, + " selected link editor_id=%llu", + static_cast(links[i].Get())); + } + } + + for (auto it = state.nodes.begin(); it != state.nodes.end(); ++it) + { + const EditorNode &node = it->second; + it->second.desired_position = + ed::GetNodePosition(ed::NodeId(node.id + 1u)); + } + + ed::End(); + LogDebug(state, 10, "Editor End"); +} + +static void +DrawSidebar(EditorState &state) +{ + auto draw_section_header = [](const char *label) { + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const float width = ImGui::GetContentRegionAvail().x; + const float height = ImGui::GetFrameHeight(); + const ImU32 bg = ImGui::GetColorU32(ImVec4(0.18f, 0.20f, 0.24f, 1.0f)); + + draw_list->AddRectFilled(pos, + ImVec2(pos.x + width, pos.y + height), + bg, + 4.0f); + draw_list->AddText(ImVec2(pos.x + ImGui::GetStyle().FramePadding.x, + pos.y + ImGui::GetStyle().FramePadding.y), + ImGui::GetColorU32(ImGuiCol_Text), + label); + ImGui::Dummy(ImVec2(width, height)); + ImGui::Spacing(); + }; + + draw_section_header("Registry"); + ImGui::InputText("Registry Path", + state.registry_path, + sizeof(state.registry_path)); + if (ImGui::Button("Load Registry")) + LoadRegistry(state); + + ImGui::Separator(); + draw_section_header("Network"); + ImGui::InputText("Network Path", + state.network_path, + sizeof(state.network_path)); + if (ImGui::Button("Load Network")) + LoadNetwork(state); + ImGui::SameLine(); + if (ImGui::Button("Save Network")) + { + std::string error; + SaveActiveTab(state); + SyncNetworkNodesFromTabs(state); + if (state.tabs.empty()) + { + state.status = "No network to save"; + } + else + { + json data = BuildNetworkJsonFrom(state.tabs[0].nodes, + state.tabs[0].links, + state.registry); + if (!SaveJsonFile(state.network_path, data, error)) + state.status = error; + else + state.status = "Saved network"; + } + } + + ImGui::Separator(); + draw_section_header("Node Types"); + static char filter[128] = {0}; + ImGui::InputText("Filter", filter, sizeof(filter)); + + const float node_types_min = 120.0f; + const float node_types_rest_min = 280.0f; + const float node_types_max = + std::max(node_types_min, + ImGui::GetContentRegionAvail().y - node_types_rest_min); + if (state.node_types_height < node_types_min) + state.node_types_height = node_types_min; + if (state.node_types_height > node_types_max) + state.node_types_height = node_types_max; + + if (ImGui::BeginChild("##node_types_list", + ImVec2(0.0f, state.node_types_height), + true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) + { + for (auto it = state.registry.begin(); it != state.registry.end(); ++it) + { + const std::string &type = it->first; + if (filter[0] != '\0') + { + if (type.find(filter) == std::string::npos) + continue; + } + if (ImGui::Selectable(type.c_str())) + AddNodeFromType(state, type); + } + } + ImGui::EndChild(); + + ImGui::InvisibleButton("##node_types_splitter", + ImVec2(ImGui::GetContentRegionAvail().x, 6.0f)); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + if (ImGui::IsItemActive()) + { + state.node_types_height += ImGui::GetIO().MouseDelta.y; + if (state.node_types_height < node_types_min) + state.node_types_height = node_types_min; + if (state.node_types_height > node_types_max) + state.node_types_height = node_types_max; + } + + if (state.tabs.size() > 1) + { + ImGui::Separator(); + draw_section_header("Modules"); + for (size_t i = 1; i < state.tabs.size(); ++i) + { + const std::string &tab_name = state.tabs[i].name; + ImGui::PushID(static_cast(i)); + if (ImGui::Selectable(tab_name.c_str())) + AddNodeFromType(state, + "coral::Network", + tab_name, + SerializeTab(state.tabs[i], state)); + if (ImGui::BeginPopupContextItem("##module_ctx")) + { + if (ImGui::MenuItem("Delete Module")) + { + state.module_delete_candidate = tab_name; + state.show_module_delete_popup = true; + } + if (ImGui::MenuItem("Rename Module")) + { + state.module_rename_candidate = tab_name; + state.show_module_rename_popup = true; + std::snprintf(state.module_rename_buffer, + sizeof(state.module_rename_buffer), + "%s", + tab_name.c_str()); + } + ImGui::EndPopup(); + } + ImGui::PopID(); + } + } + + ImGui::Separator(); + draw_section_header("Networks"); + ImGui::InputText("New Network", + state.new_tab_name, + sizeof(state.new_tab_name)); + if (ImGui::Button("Add Network")) + { + if (state.new_tab_name[0] != '\0') + { + NetworkTab tab; + tab.name = state.new_tab_name; + state.tabs.push_back(tab); + state.new_tab_name[0] = '\0'; + state.pending_tab_switch = static_cast(state.tabs.size()) - 1; + state.force_tab_select = true; + UpdateTabIndex(state); + if (!state.tabs.empty()) + { + NetworkTab &main_tab = state.tabs[0]; + const std::string value = SerializeTab(tab, state); + AddNodeToTab(state, + main_tab, + "coral::Network", + state.tabs.back().name, + value); + if (state.active_tab == 0) + LoadTab(state, 0); + } + } + } + + ImGui::Separator(); + if (!state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + + ImGui::Separator(); + draw_section_header("Logging"); + static const char *log_levels[] = {"0 - Off", + "1 - Error", + "2 - Warn", + "3 - Info", + "4 - Debug", + "5 - Trace", + "6 - Verbose", + "7 - Verbose+", + "8 - Verbose++", + "9 - Verbose+++", + "10 - All"}; + int log_level = state.log_level; + if (ImGui::Combo( + "Log Level", &log_level, log_levels, IM_ARRAYSIZE(log_levels))) + state.log_level = log_level; +} + +static void +ApplyPendingEditorStateUpdates(EditorState &state) +{ + SaveActiveTab(state); + SyncNetworkNodesFromTabs(state); + UpdateTabIndex(state); + if (!state.pending_module_rename_from.empty() && + !state.pending_module_rename_to.empty()) + { + const std::string from = state.pending_module_rename_from; + const std::string to = state.pending_module_rename_to; + state.pending_module_rename_from.clear(); + state.pending_module_rename_to.clear(); + RenameModule(state, from, to); + SyncNetworkNodesFromTabs(state); + UpdateTabIndex(state); + } + if (!state.pending_module_delete_name.empty()) + { + const std::string name = state.pending_module_delete_name; + state.pending_module_delete_name.clear(); + DeleteModule(state, name); + SyncNetworkNodesFromTabs(state); + UpdateTabIndex(state); + } + if (state.pending_tab_switch >= 0 && + state.pending_tab_switch < static_cast(state.tabs.size())) + { + SwitchToTab(state, state.pending_tab_switch); + state.pending_tab_switch = -1; + } +} + +static void +DrawCanvasTabs(EditorState &state, ed::EditorContext *editor, float canvas_width) +{ + ImGui::BeginChild("##canvas_panel", ImVec2(canvas_width, 0.0f), false); + const std::string tab_bar_id = + "##network_tabs_" + std::to_string(state.tab_bar_uid); + bool did_switch = false; + if (ImGui::BeginTabBar(tab_bar_id.c_str(), + ImGuiTabBarFlags_AutoSelectNewTabs | + ImGuiTabBarFlags_Reorderable)) + { + for (size_t i = 0; i < state.tabs.size(); ++i) + { + const std::string &tab_name = state.tabs[i].name; + ImGuiTabItemFlags flags = 0; + if (state.force_tab_select && static_cast(i) == state.active_tab) + flags |= ImGuiTabItemFlags_SetSelected; + if (ImGui::BeginTabItem(tab_name.c_str(), nullptr, flags)) + { + if (static_cast(i) != state.active_tab) + SwitchToTab(state, static_cast(i)); + + if (i > 0) + { + if (ImGui::Button("Delete Module")) + { + state.module_delete_candidate = tab_name; + state.show_module_delete_popup = true; + } + ImGui::SameLine(); + if (ImGui::Button("Rename Module")) + { + state.module_rename_candidate = tab_name; + state.show_module_rename_popup = true; + std::snprintf(state.module_rename_buffer, + sizeof(state.module_rename_buffer), + "%s", + tab_name.c_str()); + } + ImGui::Separator(); + } + + ed::SetCurrentEditor(editor); + DrawEditor(state); + ed::SetCurrentEditor(nullptr); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } + if (state.pending_tab_switch >= 0 && + state.pending_tab_switch < static_cast(state.tabs.size())) + { + const int target_tab = state.pending_tab_switch; + state.pending_tab_switch = -1; + SwitchToTab(state, target_tab); + did_switch = true; + } + if (!did_switch) + state.force_tab_select = false; + ImGui::EndChild(); +} + +static void +DrawBackendPanel(EditorState &state) +{ + if (!state.right_collapsed) + ImGui::BeginChild("##backend_panel", ImVec2(0.0f, 0.0f), true); + + ImGui::TextUnformatted("Backend"); +#if CORAL_NODE_EDITOR_ENABLE_BACKEND_RUNNER + ImGui::InputText("Path", state.backend_path, sizeof(state.backend_path)); + if (ImGui::Button("Save and Execute")) + { + SaveActiveTab(state); + SyncNetworkNodesFromTabs(state); + std::string error; + if (state.tabs.empty()) + { + state.console_output += "No network to save.\n"; + } + else + { + json data = BuildNetworkJsonFrom(state.tabs[0].nodes, + state.tabs[0].links, + state.registry); + if (!SaveJsonFile(state.network_path, data, error)) + { + state.console_output += "Save failed: " + error + "\n"; + } + else + { + const std::string cmd = std::string(state.backend_path) + + " run " + + std::string(state.network_path) + " 2>&1"; + state.console_output += "$ " + cmd + "\n"; + std::array buffer{}; + FILE *pipe = popen(cmd.c_str(), "r"); + if (!pipe) + { + state.console_output += "Failed to start backend.\n"; + } + else + { + while ( + fgets(buffer.data(), static_cast(buffer.size()), pipe)) + state.console_output += buffer.data(); + pclose(pipe); + } + state.console_scroll_to_bottom = true; + } + } + } +#else + ImGui::TextWrapped( + "Backend execution is disabled in this build. " + "Use the separate runner/manipulator application to execute a network."); +#endif + + ImGui::SameLine(); + if (ImGui::Button("Clear Console")) + state.console_output.clear(); + ImGui::Separator(); + ImGui::BeginChild("##console_panel", ImVec2(0.0f, 0.0f), true); + ImGui::TextUnformatted(state.console_output.c_str()); + if (state.console_scroll_to_bottom) + { + ImGui::SetScrollHereY(1.0f); + state.console_scroll_to_bottom = false; + } + ImGui::EndChild(); + + if (!state.right_collapsed) + ImGui::EndChild(); +} + +static void +DrawModalPopups(EditorState &state) +{ +#if CORAL_NODE_EDITOR_ENABLE_MANIPULATOR + if (state.mesh_tool.open) + ImGui::OpenPopup("Mesh Tool"); + DrawMeshTool(state); +#endif + + if (state.show_module_delete_popup && !state.module_delete_candidate.empty()) + ImGui::OpenPopup("Delete Module?"); + if (ImGui::BeginPopupModal("Delete Module?", + nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("Do you want to delete the module \"%s\"?", + state.module_delete_candidate.c_str()); + ImGui::Spacing(); + if (ImGui::Button("Delete")) + { + state.pending_module_delete_name = state.module_delete_candidate; + state.module_delete_candidate.clear(); + state.show_module_delete_popup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Keep")) + { + state.module_delete_candidate.clear(); + state.show_module_delete_popup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (state.show_module_rename_popup && !state.module_rename_candidate.empty()) + ImGui::OpenPopup("Rename Module?"); + if (ImGui::BeginPopupModal("Rename Module?", + nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("Rename module \"%s\" to:", + state.module_rename_candidate.c_str()); + ImGui::Spacing(); + ImGui::InputText("##new_module_name", + state.module_rename_buffer, + sizeof(state.module_rename_buffer)); + ImGui::Spacing(); + if (ImGui::Button("Rename")) + { + state.pending_module_rename_from = state.module_rename_candidate; + state.pending_module_rename_to = state.module_rename_buffer; + state.module_rename_candidate.clear(); + state.module_rename_buffer[0] = '\0'; + state.show_module_rename_popup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + state.module_rename_candidate.clear(); + state.module_rename_buffer[0] = '\0'; + state.show_module_rename_popup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +static void +DrawLayout(EditorState &state, ed::EditorContext *editor) +{ + ApplyPendingEditorStateUpdates(state); + + ImGuiViewport *viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::Begin("##CoralNodeEditor", + nullptr, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); + const float avail_width = ImGui::GetContentRegionAvail().x; + const float min_left = 220.0f; + const float max_left = std::max(min_left, avail_width - 220.0f); + if (state.left_width < min_left) + state.left_width = min_left; + if (state.left_width > max_left) + state.left_width = max_left; + + const float splitter_width = 8.0f; + const float left_panel_width = state.left_collapsed ? 0.0f : state.left_width; + if (!state.left_collapsed) + { + ImGui::BeginChild("##left_panel", + ImVec2(left_panel_width, 0.0f), + true, + ImGuiWindowFlags_AlwaysVerticalScrollbar); + DrawSidebar(state); + ImGui::EndChild(); + ImGui::SameLine(0.0f, 0.0f); + } + + ImGui::BeginChild("##left_splitter", ImVec2(splitter_width, 0.0f), false); + const float splitter_height = ImGui::GetContentRegionAvail().y; + ImGui::InvisibleButton("##left_splitter_btn", + ImVec2(splitter_width, splitter_height)); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (ImGui::IsItemActive() && !state.left_collapsed) + state.left_width += ImGui::GetIO().MouseDelta.x; + ImDrawList *splitter_draw = ImGui::GetWindowDrawList(); + ImVec2 split_min = ImGui::GetItemRectMin(); + ImVec2 split_max = ImGui::GetItemRectMax(); + const ImVec2 arrow_center = ImVec2((split_min.x + split_max.x) * 0.5f, + (split_min.y + split_max.y) * 0.5f); + const char *left_arrow = state.left_collapsed ? ">" : "<"; + ImVec2 left_arrow_pos(arrow_center.x - 6.0f, arrow_center.y - 8.0f); + ImVec2 left_arrow_size(12.0f, 16.0f); + ImGui::SetCursorScreenPos(left_arrow_pos); + if (ImGui::InvisibleButton("##left_toggle", left_arrow_size)) + state.left_collapsed = !state.left_collapsed; + splitter_draw->AddText(left_arrow_pos, + ImGui::GetColorU32(ImGuiCol_Text), + left_arrow); + ImGui::EndChild(); + ImGui::SameLine(0.0f, 0.0f); + + ImGui::BeginChild("##right_panel", ImVec2(0.0f, 0.0f), false); + const float right_width = ImGui::GetContentRegionAvail().x; + const float min_backend = 260.0f; + const float max_backend = std::max(min_backend, right_width - 260.0f); + if (state.right_width < min_backend) + state.right_width = min_backend; + if (state.right_width > max_backend) + state.right_width = max_backend; + const float backend_width = state.right_collapsed ? 0.0f : state.right_width; + const float canvas_width = + std::max(0.0f, right_width - backend_width - splitter_width); + DrawCanvasTabs(state, editor, canvas_width); + + ImGui::SameLine(0.0f, 0.0f); + ImGui::BeginChild("##right_splitter", ImVec2(splitter_width, 0.0f), false); + const float right_splitter_height = ImGui::GetContentRegionAvail().y; + ImGui::InvisibleButton("##right_splitter_btn", + ImVec2(splitter_width, right_splitter_height)); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (ImGui::IsItemActive() && !state.right_collapsed) + state.right_width -= ImGui::GetIO().MouseDelta.x; + ImVec2 right_min = ImGui::GetItemRectMin(); + ImVec2 right_max = ImGui::GetItemRectMax(); + const char *right_arrow = state.right_collapsed ? "<" : ">"; + ImVec2 right_arrow_pos((right_min.x + right_max.x) * 0.5f - 6.0f, + (right_min.y + right_max.y) * 0.5f - 8.0f); + ImVec2 right_arrow_size(12.0f, 16.0f); + ImGui::SetCursorScreenPos(right_arrow_pos); + if (ImGui::InvisibleButton("##right_toggle", right_arrow_size)) + state.right_collapsed = !state.right_collapsed; + splitter_draw->AddText(right_arrow_pos, + ImGui::GetColorU32(ImGuiCol_Text), + right_arrow); + ImGui::EndChild(); + ImGui::SameLine(0.0f, 0.0f); + DrawBackendPanel(state); + ImGui::EndChild(); + ImGui::End(); + DrawModalPopups(state); +} + +static void +SetupTrueTypeFonts(ImGuiIO &io) +{ + io.FontGlobalScale = 1.0f; + + io.Fonts->Clear(); + + auto file_exists = [](const char *path) -> bool { + if (!path || path[0] == '\0') + return false; + std::ifstream f(path); + return f.good(); + }; + + struct FontCandidate + { + const char *path; + float size_px; + }; + + const FontCandidate candidates[] = { + {"bin/data/Play-Regular.ttf", 19.0f}, + {"bin/data/Oswald-Regular.ttf", 19.0f}, + {"bin/data/Cuprum-Bold.ttf", 19.0f}, + {"gui/imgui-node-editor/examples/data/Play-Regular.ttf", 19.0f}, + {"gui/imgui-node-editor/examples/data/Oswald-Regular.ttf", 19.0f}, + {"gui/imgui-node-editor/examples/data/Cuprum-Bold.ttf", 19.0f}, + {"../gui/imgui-node-editor/examples/data/Play-Regular.ttf", 19.0f}, + {"../gui/imgui-node-editor/examples/data/Oswald-Regular.ttf", 19.0f}, + {"../gui/imgui-node-editor/examples/data/Cuprum-Bold.ttf", 19.0f}, + }; + + ImFont *font = nullptr; + for (const auto &candidate : candidates) + { + if (!file_exists(candidate.path)) + continue; + font = io.Fonts->AddFontFromFileTTF(candidate.path, candidate.size_px); + if (font) + break; + } + + if (!font) + { + std::fprintf(stderr, + "Warning: failed to load .ttf fonts, using default font.\n"); + font = io.Fonts->AddFontDefault(); + } + + io.FontDefault = font; +} + +int +main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + if (!glfwInit()) + return 1; + +#if __APPLE__ + const char *glsl_version = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#else + const char *glsl_version = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); +#endif + + GLFWwindow *window = + glfwCreateWindow(1280, 720, "Coral Node Editor", nullptr, nullptr); + if (!window) + { + glfwTerminate(); + return 1; + } + + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + io.ConfigWindowsMoveFromTitleBarOnly = true; + + SetupTrueTypeFonts(io); + + ImGui::StyleColorsDark(); + ImGuiStyle &style = ImGui::GetStyle(); + style.WindowRounding = 6.0f; + style.FrameRounding = 4.0f; + style.ScrollbarSize = 14.0f; + style.FramePadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(6.0f, 6.0f); + + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init(glsl_version); + + ed::Config config; + config.SettingsFile = "coral_node_editor.json"; + ed::EditorContext *editor = ed::CreateEditor(&config); + ed::SetCurrentEditor(editor); + ed::Style &editor_style = ed::GetStyle(); + editor_style.NodeRounding = 8.0f; + editor_style.NodeBorderWidth = 2.0f; + editor_style.HoveredNodeBorderWidth = 3.0f; + editor_style.SelectedNodeBorderWidth = 3.0f; + editor_style.Colors[ed::StyleColor_NodeBg] = ImColor(30, 34, 40, 230); + editor_style.Colors[ed::StyleColor_NodeBorder] = ImColor(200, 140, 60, 200); + editor_style.Colors[ed::StyleColor_HovNodeBorder] = + ImColor(240, 180, 90, 255); + editor_style.Colors[ed::StyleColor_SelNodeBorder] = + ImColor(255, 210, 120, 255); + ed::EnableShortcuts(true); + ed::SetCurrentEditor(nullptr); + + EditorState state; + NetworkTab main_tab; + main_tab.name = "Main"; + state.tabs.push_back(main_tab); + state.active_tab = 0; + std::snprintf(state.registry_path, + sizeof(state.registry_path), + "%s", + "node_types.json"); + std::snprintf(state.network_path, + sizeof(state.network_path), + "%s", + "mwe.json"); +#if CORAL_NODE_EDITOR_ENABLE_BACKEND_RUNNER + std::snprintf(state.backend_path, + sizeof(state.backend_path), + "%s", + "./dealii_backend.g"); +#endif + { + std::ofstream(state.log_path.c_str(), std::ios::trunc); + } + LogDebug(state, 10, "Editor start"); + LoadRegistry(state); + + while (!glfwWindowShouldClose(window)) + { + glfwPollEvents(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + try + { + const int delete_down = + glfwGetKey(window, GLFW_KEY_DELETE) == GLFW_PRESS; + const int backspace_down = + glfwGetKey(window, GLFW_KEY_BACKSPACE) == GLFW_PRESS; + const bool delete_pressed = delete_down && !state.delete_key_down; + const bool backspace_pressed = + backspace_down && !state.backspace_key_down; + state.delete_key_down = delete_down; + state.backspace_key_down = backspace_down; + + if (delete_pressed || backspace_pressed) + state.request_delete = true; + + if (!state.show_error_popup) + DrawLayout(state, editor); + } + catch (const std::exception &e) + { + state.error_message = std::string("Unhandled error: ") + e.what(); + state.show_error_popup = true; + } + catch (...) + { + state.error_message = "Unhandled error: unknown exception."; + state.show_error_popup = true; + } + + if (state.show_error_popup) + { + ImGui::OpenPopup("Fatal Error"); + if (ImGui::BeginPopupModal("Fatal Error", + nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextWrapped("%s", state.error_message.c_str()); + if (ImGui::Button("Close")) + { + ImGui::CloseCurrentPopup(); + glfwSetWindowShouldClose(window, GL_TRUE); + } + ImGui::EndPopup(); + } + } + + ImGui::Render(); + int display_w = 0; + int display_h = 0; + glfwGetFramebufferSize(window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(window); + } + + ed::DestroyEditor(editor); + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); + + return 0; +} diff --git a/gui/manipulator/CMakeLists.txt b/gui/manipulator/CMakeLists.txt new file mode 100644 index 0000000..bea5080 --- /dev/null +++ b/gui/manipulator/CMakeLists.txt @@ -0,0 +1,114 @@ +cmake_minimum_required(VERSION 3.16) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(coral_gui_manipulator LANGUAGES CXX) +endif() + +find_package(deal.II 9.5 QUIET + HINTS ${deal.II_DIR} ${DEAL_II_DIR} ../.. ../../.. $ENV{DEAL_II_DIR}) + +if(NOT deal.II_FOUND AND NOT DEAL_II_FOUND) + message(WARNING "deal.II not found; skipping gui/manipulator") + return() +endif() + +deal_ii_initialize_cached_variables() + +if(NOT DEAL_II_WITH_VTK) + message(WARNING + "deal.II was found but was built without VTK support (DEAL_II_WITH_VTK=OFF); skipping gui/manipulator") + return() +endif() + +include(FetchContent) + +set(_imgui_root "${CMAKE_CURRENT_LIST_DIR}/../editor/imgui-node-editor/external/imgui") +set(_imgui_impl_root "${CMAKE_CURRENT_LIST_DIR}/../editor/imgui-node-editor/examples/application/source") + +if(NOT TARGET imgui) + add_subdirectory( + ${_imgui_root} + ${CMAKE_CURRENT_BINARY_DIR}/imgui + EXCLUDE_FROM_ALL) + if(MSVC) + target_compile_options(imgui PRIVATE /W0) + else() + target_compile_options(imgui PRIVATE -w) + endif() +endif() + +find_package(glfw3 QUIET) +if(NOT glfw3_FOUND AND NOT TARGET glfw AND NOT TARGET glfw3) + set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) + set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + glfw + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.3.8 + ) + FetchContent_MakeAvailable(glfw) +endif() + +find_package(OpenGL REQUIRED) + +if(TARGET glfw) + set(_glfw_target glfw) +elseif(TARGET glfw3) + set(_glfw_target glfw3) +else() + set(_glfw_target glfw) +endif() + +if(TARGET ${_glfw_target}) + if(MSVC) + target_compile_options(${_glfw_target} PRIVATE /W0) + else() + target_compile_options(${_glfw_target} PRIVATE -w) + endif() +endif() + +add_executable(coral_manipulator + source/main.cpp + source/manipulator.cpp + ${_imgui_impl_root}/imgui_impl_glfw.cpp + ${_imgui_impl_root}/imgui_impl_opengl3.cpp + ${CMAKE_CURRENT_LIST_DIR}/imgui/misc/cpp/imgui_stdlib.cpp) + +target_compile_features(coral_manipulator PRIVATE cxx_std_17) + +target_include_directories(coral_manipulator SYSTEM PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/source + ${CMAKE_CURRENT_LIST_DIR}/include + ${CMAKE_CURRENT_LIST_DIR}/imgui + ${_imgui_root} + ${_imgui_impl_root} + ${PROJECT_SOURCE_DIR}/core/include) + +target_link_libraries(coral_manipulator + coral_core + imgui + ${_glfw_target}) + +deal_ii_setup_target(coral_manipulator) + +if(MSVC) + target_compile_options(coral_manipulator PRIVATE /W0) +else() + target_compile_options(coral_manipulator PRIVATE -w) +endif() + +if(TARGET OpenGL::GL) + target_link_libraries(coral_manipulator OpenGL::GL) +else() + target_link_libraries(coral_manipulator ${OPENGL_gl_LIBRARY}) +endif() + +if(APPLE) + target_compile_definitions(coral_manipulator PRIVATE GL_SILENCE_DEPRECATION=1) + target_link_libraries(coral_manipulator + "-framework CoreFoundation" + "-framework Cocoa" + "-framework IOKit" + "-framework CoreVideo") +endif() diff --git a/gui/manipulator/imgui/misc/cpp/imgui_stdlib.cpp b/gui/manipulator/imgui/misc/cpp/imgui_stdlib.cpp new file mode 100644 index 0000000..62bf43e --- /dev/null +++ b/gui/manipulator/imgui/misc/cpp/imgui_stdlib.cpp @@ -0,0 +1,117 @@ +// imgui_stdlib.cpp +// Helper functions to use std::string with Dear ImGui. +// This is a lightly vendored copy of Dear ImGui's misc/cpp/imgui_stdlib.cpp. +// The original project is https://github.com/ocornut/imgui + +#include "imgui_stdlib.h" + +#include + +namespace ImGui +{ +namespace +{ +struct InputTextCallback_UserData +{ + std::string *Str; + ImGuiInputTextCallback ChainCallback; + void *ChainCallbackUserData; +}; + +static int +InputTextCallback(ImGuiInputTextCallbackData *data) +{ + auto *user_data = static_cast(data->UserData); + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + std::string *str = user_data->Str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(static_cast(data->BufTextLen)); + data->Buf = const_cast(str->c_str()); + } + else if (user_data->ChainCallback) + { + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} +} // namespace + +bool +InputText(const char *label, + std::string *str, + ImGuiInputTextFlags flags, + ImGuiInputTextCallback callback, + void *user_data) +{ + IM_ASSERT(str != nullptr); + + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + + return ImGui::InputText(label, + const_cast(str->c_str()), + str->capacity() + 1, + flags, + InputTextCallback, + &cb_user_data); +} + +bool +InputTextMultiline(const char *label, + std::string *str, + const ImVec2 &size, + ImGuiInputTextFlags flags, + ImGuiInputTextCallback callback, + void *user_data) +{ + IM_ASSERT(str != nullptr); + + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + + return ImGui::InputTextMultiline(label, + const_cast(str->c_str()), + str->capacity() + 1, + size, + flags, + InputTextCallback, + &cb_user_data); +} + +bool +InputTextWithHint(const char *label, + const char *hint, + std::string *str, + ImGuiInputTextFlags flags, + ImGuiInputTextCallback callback, + void *user_data) +{ + IM_ASSERT(str != nullptr); + + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + + return ImGui::InputTextWithHint(label, + hint, + const_cast(str->c_str()), + str->capacity() + 1, + flags, + InputTextCallback, + &cb_user_data); +} +} // namespace ImGui + diff --git a/gui/manipulator/imgui/misc/cpp/imgui_stdlib.h b/gui/manipulator/imgui/misc/cpp/imgui_stdlib.h new file mode 100644 index 0000000..687ee96 --- /dev/null +++ b/gui/manipulator/imgui/misc/cpp/imgui_stdlib.h @@ -0,0 +1,37 @@ +// imgui_stdlib.h +// Helper functions to use std::string with Dear ImGui. +// This is a lightly vendored copy of Dear ImGui's misc/cpp/imgui_stdlib.h. +// The original project is https://github.com/ocornut/imgui + +#pragma once + +#include + +#include + +namespace ImGui +{ +IMGUI_API bool +InputText(const char *label, + std::string *str, + ImGuiInputTextFlags flags = 0, + ImGuiInputTextCallback callback = nullptr, + void *user_data = nullptr); + +IMGUI_API bool +InputTextMultiline(const char *label, + std::string *str, + const ImVec2 &size = ImVec2(0, 0), + ImGuiInputTextFlags flags = 0, + ImGuiInputTextCallback callback = nullptr, + void *user_data = nullptr); + +IMGUI_API bool +InputTextWithHint(const char *label, + const char *hint, + std::string *str, + ImGuiInputTextFlags flags = 0, + ImGuiInputTextCallback callback = nullptr, + void *user_data = nullptr); +} // namespace ImGui + diff --git a/gui/manipulator/include/coral_manipulator/manipulator.h b/gui/manipulator/include/coral_manipulator/manipulator.h new file mode 100644 index 0000000..05db04a --- /dev/null +++ b/gui/manipulator/include/coral_manipulator/manipulator.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +struct GLFWwindow; + +namespace coral::manipulator +{ +class ManipulatorApp +{ +public: + ManipulatorApp(); + ~ManipulatorApp(); + + ManipulatorApp(const ManipulatorApp &) = delete; + ManipulatorApp &operator=(const ManipulatorApp &) = delete; + + void initialize(GLFWwindow *glfw_window); + void set_framebuffer_size(int width, int height); + + // GLFW callback entry points (call these from your GLFW callbacks, and also + // forward the event to ImGui's GLFW backend callbacks). + void on_glfw_cursor_pos(GLFWwindow *glfw_window, double xpos, double ypos); + void on_glfw_mouse_button(GLFWwindow *glfw_window, + int button, + int action, + int mods); + void on_glfw_scroll(GLFWwindow *glfw_window, double xoffset, double yoffset); + + void process_input(GLFWwindow *glfw_window); + void render_frame(); + void draw_ui(); + + std::string status_text() const; + +private: + struct Impl; + std::unique_ptr impl; +}; +} // namespace coral::manipulator diff --git a/gui/manipulator/source/main.cpp b/gui/manipulator/source/main.cpp new file mode 100644 index 0000000..3bea5cd --- /dev/null +++ b/gui/manipulator/source/main.cpp @@ -0,0 +1,135 @@ +#include +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" + +#include + +#ifdef IMGUI_IMPL_OPENGL_ES2 +# include +#endif + +#include + +#include "coral_manipulator/manipulator.h" + +namespace +{ +static coral::manipulator::ManipulatorApp *g_app = nullptr; + +static void +glfw_error_callback(int error, const char *description) +{ + (void)error; + std::fprintf(stderr, "glfw-err: %s\n", description); +} + +static void +glfw_cursor_pos_callback(GLFWwindow *window, double xpos, double ypos) +{ + if (g_app) + g_app->on_glfw_cursor_pos(window, xpos, ypos); +} + +static void +glfw_mouse_button_callback(GLFWwindow *window, int button, int action, int mods) +{ + ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods); + if (g_app) + g_app->on_glfw_mouse_button(window, button, action, mods); +} + +static void +glfw_scroll_callback(GLFWwindow *window, double xoffset, double yoffset) +{ + ImGui_ImplGlfw_ScrollCallback(window, xoffset, yoffset); + if (g_app) + g_app->on_glfw_scroll(window, xoffset, yoffset); +} +} // namespace + +int +main(int argc, const char *argv[]) +{ + (void)argc; + (void)argv; + + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) + return 1; + +#if defined(IMGUI_IMPL_OPENGL_ES2) + const char *glsl_version = "#version 100"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); +#elif defined(__APPLE__) + const char *glsl_version = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#else + const char *glsl_version = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); +#endif + + GLFWwindow *window = + glfwCreateWindow(1280, 720, "CORAL Manipulator (VTK)", nullptr, nullptr); + if (!window) + return 1; + + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + (void)io; + + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init(glsl_version); + ImGui::StyleColorsDark(); + + coral::manipulator::ManipulatorApp app; + g_app = &app; + app.initialize(window); + + // Override ImGui's installed callbacks with wrappers that also forward events + // to the manipulator (for VTK camera controls). The wrappers call ImGui's + // callbacks explicitly. + glfwSetCursorPosCallback(window, glfw_cursor_pos_callback); + glfwSetMouseButtonCallback(window, glfw_mouse_button_callback); + glfwSetScrollCallback(window, glfw_scroll_callback); + + while (!glfwWindowShouldClose(window)) + { + glfwPollEvents(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + int display_w = 0; + int display_h = 0; + glfwGetFramebufferSize(window, &display_w, &display_h); + app.set_framebuffer_size(display_w, display_h); + + app.draw_ui(); + app.process_input(window); + + ImGui::Render(); + + app.render_frame(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(window); + } + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); + return 0; +} diff --git a/gui/manipulator/source/manipulator.cpp b/gui/manipulator/source/manipulator.cpp new file mode 100644 index 0000000..b2430b7 --- /dev/null +++ b/gui/manipulator/source/manipulator.cpp @@ -0,0 +1,2457 @@ +#include "coral_manipulator/manipulator.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define GLFW_INCLUDE_NONE +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace coral::manipulator +{ +namespace +{ +struct ButtonRowLayout +{ + float w = 120.0f; + float h = 0.0f; // use default frame height (from padding) +}; + +static ButtonRowLayout +GetButtonRowLayout(const int count, const float min_w = 110.0f) +{ + const float avail = ImGui::GetContentRegionAvail().x; + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float w = + std::max(min_w, (avail - gap * static_cast(count - 1)) / + static_cast(std::max(1, count))); + return ButtonRowLayout{w, 0.0f}; +} + +class TallButtonStyle +{ +public: + TallButtonStyle() + { + const ImVec2 pad = ImGui::GetStyle().FramePadding; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2(pad.x, std::max(pad.y, 10.0f))); + } + ~TallButtonStyle() { ImGui::PopStyleVar(); } +}; + +static bool +IsFlatManifoldId(const unsigned int id) +{ + return id == static_cast(dealii::numbers::flat_manifold_id); +} + +static bool +ManifoldIdMatches(const dealii::types::manifold_id id, const int selected) +{ + if (selected < 0) + return id == dealii::numbers::flat_manifold_id; + return static_cast(id) == selected; +} + +static int +RoundScrollSteps(const double v) +{ + if (v > 0.0) + return static_cast(v + 0.5); + if (v < 0.0) + return static_cast(v - 0.5); + return 0; +} + +static vtkOpenGLRenderWindow::VTKOpenGLAPIProc +GlfwGetProcAddress(void *userptr, const char *name) +{ + (void)userptr; + if (auto *p = glfwGetProcAddress(name)) + return reinterpret_cast(p); + + if (auto *p = dlsym(RTLD_DEFAULT, name)) + return reinterpret_cast(p); + + return nullptr; +} + +static GLADapiproc +GlfwGladGetProcAddress(void *userptr, const char *name) +{ + return reinterpret_cast(GlfwGetProcAddress(userptr, name)); +} + +struct FilePicker +{ + bool open = false; + bool pending_open_popup = false; + std::string title = "Open"; + std::string current_dir; + std::string selected_name; + std::string filter_extension; // e.g. ".vtk" + std::string error; + + void open_modal(std::string title_in, + std::string initial_dir, + std::string filter_extension_in) + { + title = std::move(title_in); + current_dir = std::move(initial_dir); + filter_extension = std::move(filter_extension_in); + selected_name.clear(); + error.clear(); + open = true; + pending_open_popup = true; + } + + std::optional draw() + { + std::optional result; + if (!open) + return result; + + const ImGuiWindowFlags flags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings; + if (pending_open_popup) + { + ImGui::OpenPopup(title.c_str()); + pending_open_popup = false; + } + if (!ImGui::BeginPopupModal(title.c_str(), &open, flags)) + return result; + + if (current_dir.empty()) + current_dir = std::filesystem::current_path().string(); + + std::filesystem::path dir(current_dir); + if (!std::filesystem::exists(dir)) + dir = std::filesystem::current_path(); + + ImGui::TextUnformatted("Directory"); + ImGui::SetNextItemWidth(520.0f); + ImGui::InputText("##dir", ¤t_dir); + + if (ImGui::Button("Up")) + { + const auto parent = dir.parent_path(); + if (!parent.empty()) + current_dir = parent.string(); + } + + ImGui::Separator(); + + std::vector entries; + try + { + for (const auto &entry : std::filesystem::directory_iterator(dir)) + entries.push_back(entry); + } + catch (const std::exception &e) + { + error = e.what(); + } + + std::sort(entries.begin(), + entries.end(), + [](const auto &a, const auto &b) { + const bool ad = a.is_directory(); + const bool bd = b.is_directory(); + if (ad != bd) + return ad > bd; + return a.path().filename().string() < b.path().filename().string(); + }); + + ImGui::BeginChild("##file_list", ImVec2(560.0f, 320.0f), true); + for (const auto &entry : entries) + { + const auto name = entry.path().filename().string(); + const bool is_dir = entry.is_directory(); + const std::string label = is_dir ? (name + "/") : name; + + if (!is_dir && !filter_extension.empty() && + entry.path().extension() != filter_extension) + continue; + + const bool selected = selected_name == name; + if (ImGui::Selectable(label.c_str(), selected)) + selected_name = name; + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) + { + if (is_dir) + { + current_dir = entry.path().string(); + selected_name = {}; + } + else + { + result = entry.path(); + open = false; + ImGui::CloseCurrentPopup(); + } + } + } + ImGui::EndChild(); + + if (!error.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", error.c_str()); + } + + ImGui::Separator(); + + const bool can_open = !selected_name.empty() && + std::filesystem::is_regular_file(dir / selected_name); + if (can_open) + { + if (ImGui::Button("Open")) + { + result = dir / selected_name; + open = false; + ImGui::CloseCurrentPopup(); + } + } + else + { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, + ImGui::GetStyle().Alpha * 0.5f); + ImGui::Button("Open"); + ImGui::PopStyleVar(); + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + open = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + return result; + } +}; + +struct NewGridDialog +{ + bool open = false; + bool pending_open_popup = false; + std::string name; + std::string arguments; + int refinements = 0; + std::string error; + + void open_modal() + { + open = true; + error.clear(); + if (name.empty()) + name = "hyper_cube"; + if (arguments.empty()) + arguments = "0 : 1 : true"; + pending_open_popup = true; + } +}; + +struct SaveDialog +{ + bool open = false; + bool pending_open_popup = false; + std::string directory; + std::string filename = "mesh"; + int format = 1; // 0: vtk, 1: vtu + std::string error; + + void open_modal(std::string initial_dir) + { + open = true; + error.clear(); + directory = std::move(initial_dir); + if (directory.empty()) + directory = std::filesystem::current_path().string(); + pending_open_popup = true; + } + + struct Result + { + std::filesystem::path path; + int format; + }; + + std::optional draw() + { + std::optional result; + if (!open) + return result; + + const ImGuiWindowFlags flags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings; + if (pending_open_popup) + { + ImGui::OpenPopup("Save Mesh"); + pending_open_popup = false; + } + if (!ImGui::BeginPopupModal("Save Mesh", &open, flags)) + return result; + + ImGui::TextUnformatted("Directory"); + ImGui::SetNextItemWidth(520.0f); + ImGui::InputText("##save_dir", &directory); + + ImGui::TextUnformatted("Filename"); + ImGui::SetNextItemWidth(520.0f); + ImGui::InputText("##save_name", &filename); + + const char *formats[] = {"VTK (.vtk)", "VTU (.vtu)"}; + ImGui::Combo("Format", &format, formats, 2); + + if (!error.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", error.c_str()); + } + + const char *ext = (format == 0) ? ".vtk" : ".vtu"; + std::filesystem::path out_dir(directory.empty() ? "." : directory); + std::filesystem::path out_path = out_dir / filename; + if (out_path.extension() != ext) + out_path.replace_extension(ext); + + const bool can_save = !filename.empty(); + if (can_save) + { + if (ImGui::Button("Save")) + { + result = Result{out_path, format}; + open = false; + ImGui::CloseCurrentPopup(); + } + } + else + { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, + ImGui::GetStyle().Alpha * 0.5f); + ImGui::Button("Save"); + ImGui::PopStyleVar(); + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + open = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + return result; + } +}; + +struct MeshState +{ + dealii::Triangulation<2, 2> tria; + + std::map::active_cell_iterator> + cell_by_index; + + enum class Mode + { + boundary = 0, + cell = 1, + }; + Mode mode = Mode::boundary; + + std::map boundary_counts; + std::map material_counts; + std::map face_manifold_counts; + std::map cell_manifold_counts; + + std::map boundary_colors; + std::map material_colors; + + int selected_boundary_id = -1; + int selected_material_id = -1; + int selected_face_manifold_id = -1; + int selected_cell_manifold_id = -1; + + std::set> selected_boundary_faces; + std::set selected_cells; + + std::optional> hovered_boundary_face; + std::optional hovered_cell; + + unsigned int assign_boundary_id = 0; + unsigned int assign_material_id = 0; + unsigned int assign_face_manifold_id = 0; + unsigned int assign_cell_manifold_id = 0; + + std::string current_label; +}; + +struct VtkScene +{ + vtkNew render_window; + vtkNew renderer; + vtkNew interactor; + vtkNew style; + + vtkNew grid; + vtkNew boundary; + + vtkNew cell_mapper; + vtkNew cell_actor; + vtkNew cell_lut; + + vtkNew cell_selected_threshold; + vtkNew cell_selected_mapper; + vtkNew cell_selected_actor; + vtkNew cell_selected_fill_mapper; + vtkNew cell_selected_fill_actor; + + vtkNew cell_hover_threshold; + vtkNew cell_hover_mapper; + vtkNew cell_hover_actor; + + vtkNew boundary_selected_threshold; + vtkNew boundary_selected_geom; + vtkNew boundary_selected_mapper; + vtkNew boundary_selected_actor; + + vtkNew boundary_hover_threshold; + vtkNew boundary_hover_geom; + vtkNew boundary_hover_mapper; + vtkNew boundary_hover_actor; + + vtkNew boundary_mapper; + vtkNew boundary_actor; + vtkNew boundary_lut; + + vtkNew picker; + + vtkNew make_current_cb; + vtkNew is_current_cb; + vtkNew supports_gl_cb; + vtkNew is_direct_cb; + vtkNew frame_cb; + + int viewport_w = 1; + int viewport_h = 1; +}; + +static vtkSmartPointer +NewIntCellArray(const char *name) +{ + auto arr = vtkSmartPointer::New(); + arr->SetName(name); + arr->SetNumberOfComponents(1); + return arr; +} + +static ImVec4 +DefaultColorForId(const unsigned int id) +{ + // Deterministic HSV palette based on the id. + const float h = std::fmod(0.6180339887f * static_cast(id), 1.0f); + const float s = 0.55f; + const float v = 0.95f; + + const float i = std::floor(h * 6.0f); + const float f = h * 6.0f - i; + const float p = v * (1.0f - s); + const float q = v * (1.0f - f * s); + const float t = v * (1.0f - (1.0f - f) * s); + + float r = v, g = v, b = v; + switch (static_cast(i) % 6) + { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + default: break; + } + return ImVec4(r, g, b, 1.0f); +} + +static void +RebuildCounts(MeshState &state) +{ + state.boundary_counts.clear(); + state.material_counts.clear(); + state.face_manifold_counts.clear(); + state.cell_manifold_counts.clear(); + + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + { + state.material_counts[static_cast(cell->material_id())] += + 1u; + state.cell_manifold_counts[static_cast(cell->manifold_id())] += + 1u; + + for (unsigned int f = 0; f < cell->n_faces(); ++f) + { + const auto face = cell->face(f); + if (!face->at_boundary()) + continue; + state.boundary_counts[static_cast(face->boundary_id())] += + 1u; + state.face_manifold_counts[static_cast(face->manifold_id())] += + 1u; + } + } + + // Ensure default colors exist for discovered ids. + for (const auto &[bid, count] : state.boundary_counts) + { + (void)count; + if (state.boundary_colors.count(bid) == 0) + state.boundary_colors[bid] = DefaultColorForId(bid); + } + for (const auto &[mid, count] : state.material_counts) + { + (void)count; + if (state.material_colors.count(mid) == 0) + state.material_colors[mid] = DefaultColorForId(mid); + } +} + +static void +RebuildCellIndexMap(MeshState &state) +{ + state.cell_by_index.clear(); + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + state.cell_by_index[cell->active_cell_index()] = cell; +} + +static void +BuildVtkMeshesFromTriangulation(const MeshState &state, VtkScene &scene) +{ + scene.grid->Reset(); + scene.boundary->Reset(); + + const auto &vertices = state.tria.get_vertices(); + const auto &used_vertices = state.tria.get_used_vertices(); + + std::vector vertex_to_vtk(vertices.size(), -1); + vtkNew points; + points->SetDataTypeToDouble(); + + for (size_t i = 0; i < vertices.size(); ++i) + { + if (!used_vertices[i]) + continue; + const vtkIdType pid = + points->InsertNextPoint(vertices[i][0], vertices[i][1], 0.0); + vertex_to_vtk[i] = pid; + } + + scene.grid->SetPoints(points); + + auto active_cell_index = NewIntCellArray("active_cell_index"); + auto material_id = NewIntCellArray("material_id"); + auto cell_selected = NewIntCellArray("selected"); + auto cell_hovered = NewIntCellArray("hovered"); + + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + const unsigned int mat = + static_cast(cell->material_id()); + + const unsigned int n_vertices = cell->reference_cell().n_vertices(); + if (n_vertices == 3) + { + vtkIdType ids[3] = {}; + for (unsigned int v = 0; v < 3; ++v) + ids[v] = vertex_to_vtk[cell->vertex_index(v)]; + scene.grid->InsertNextCell(VTK_TRIANGLE, 3, ids); + } + else if (n_vertices == 4) + { + vtkIdType ids[4] = {}; + ids[0] = vertex_to_vtk[cell->vertex_index(0)]; + ids[1] = vertex_to_vtk[cell->vertex_index(1)]; + ids[2] = vertex_to_vtk[cell->vertex_index(3)]; + ids[3] = vertex_to_vtk[cell->vertex_index(2)]; + scene.grid->InsertNextCell(VTK_QUAD, 4, ids); + } + else + { + continue; + } + + active_cell_index->InsertNextValue(static_cast(cell_index)); + material_id->InsertNextValue(static_cast(mat)); + cell_selected->InsertNextValue(0); + cell_hovered->InsertNextValue(0); + } + + scene.grid->GetCellData()->AddArray(active_cell_index); + scene.grid->GetCellData()->AddArray(material_id); + scene.grid->GetCellData()->AddArray(cell_selected); + scene.grid->GetCellData()->AddArray(cell_hovered); + + vtkNew lines; + auto boundary_cell_index = NewIntCellArray("cell_index"); + auto boundary_face_no = NewIntCellArray("face_no"); + auto boundary_id = NewIntCellArray("boundary_id"); + auto boundary_selected = NewIntCellArray("selected"); + auto boundary_hovered = NewIntCellArray("hovered"); + + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + for (unsigned int f = 0; f < cell->n_faces(); ++f) + { + const auto face = cell->face(f); + if (!face->at_boundary()) + continue; + + vtkIdType ids[2] = {}; + ids[0] = vertex_to_vtk[face->vertex_index(0)]; + ids[1] = vertex_to_vtk[face->vertex_index(1)]; + lines->InsertNextCell(2, ids); + + boundary_cell_index->InsertNextValue(static_cast(cell_index)); + boundary_face_no->InsertNextValue(static_cast(f)); + boundary_id->InsertNextValue( + static_cast(static_cast(face->boundary_id()))); + boundary_selected->InsertNextValue(0); + boundary_hovered->InsertNextValue(0); + } + } + + scene.boundary->SetPoints(points); + scene.boundary->SetLines(lines); + scene.boundary->GetCellData()->AddArray(boundary_cell_index); + scene.boundary->GetCellData()->AddArray(boundary_face_no); + scene.boundary->GetCellData()->AddArray(boundary_id); + scene.boundary->GetCellData()->AddArray(boundary_selected); + scene.boundary->GetCellData()->AddArray(boundary_hovered); +} + +static vtkIntArray * +GetCellArray(vtkDataSet *dataset, const char *name) +{ + if (!dataset) + return nullptr; + return vtkIntArray::SafeDownCast(dataset->GetCellData()->GetArray(name)); +} + +static void +UpdateVtkSelectionArrays(const MeshState &state, VtkScene &scene); + +static bool +HitOnAnyGeometry(VtkScene &scene, const int x, const int y) +{ + scene.picker->InitializePickList(); + scene.picker->AddPickList(scene.cell_actor); + scene.picker->AddPickList(scene.boundary_actor); + if (!scene.picker->Pick(x, y, 0.0, scene.renderer)) + return false; + vtkDataSet *ds = vtkDataSet::SafeDownCast(scene.picker->GetDataSet()); + if (!ds) + return false; + return scene.picker->GetCellId() >= 0; +} + +static void +UpdateRendererViewport(VtkScene &scene, + const double xmin, + const double ymin, + const double xmax, + const double ymax) +{ + const double clamped_xmin = std::max(0.0, std::min(1.0, xmin)); + const double clamped_ymin = std::max(0.0, std::min(1.0, ymin)); + const double clamped_xmax = std::max(0.0, std::min(1.0, xmax)); + const double clamped_ymax = std::max(0.0, std::min(1.0, ymax)); + scene.renderer->SetViewport(clamped_xmin, clamped_ymin, clamped_xmax, clamped_ymax); +} + +static void +ApplyBoxSelection(MeshState &state, + VtkScene &scene, + const int x0, + const int y0, + const int x1, + const int y1, + const bool add_mode, + const bool subtract_mode) +{ + const bool replace_mode = !add_mode && !subtract_mode; + const int xmin = std::min(x0, x1); + const int xmax = std::max(x0, x1); + const int ymin = std::min(y0, y1); + const int ymax = std::max(y0, y1); + + vtkNew selector; + selector->SetRenderer(scene.renderer); + selector->SetArea(xmin, ymin, xmax, ymax); + selector->SetFieldAssociation(vtkDataObject::FIELD_ASSOCIATION_CELLS); + + // Ensure buffers are up-to-date for selection. + scene.render_window->Render(); + + vtkSmartPointer sel; + try + { + sel = selector->Select(); + } + catch (...) + { + return; + } + + if (!sel) + return; + + if (state.mode == MeshState::Mode::cell) + { + if (replace_mode) + state.selected_cells.clear(); + + vtkIntArray *active_cell_index = GetCellArray(scene.grid, "active_cell_index"); + if (!active_cell_index) + return; + + for (unsigned int n = 0; n < sel->GetNumberOfNodes(); ++n) + { + vtkSelectionNode *node = sel->GetNode(n); + if (!node) + continue; + + vtkAbstractArray *ids = node->GetSelectionList(); + if (!ids) + continue; + + for (vtkIdType i = 0; i < ids->GetNumberOfTuples(); ++i) + { + const vtkIdType cid = + static_cast(ids->GetVariantValue(i).ToLongLong()); + if (cid < 0 || cid >= active_cell_index->GetNumberOfTuples()) + continue; + + const unsigned int idx = + static_cast(active_cell_index->GetValue(cid)); + if (subtract_mode) + state.selected_cells.erase(idx); + else + state.selected_cells.insert(idx); + } + } + + UpdateVtkSelectionArrays(state, scene); + } + else + { + if (replace_mode) + state.selected_boundary_faces.clear(); + + vtkIntArray *cell_index = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("cell_index")); + vtkIntArray *face_no = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("face_no")); + if (!cell_index || !face_no) + return; + + for (unsigned int n = 0; n < sel->GetNumberOfNodes(); ++n) + { + vtkSelectionNode *node = sel->GetNode(n); + if (!node) + continue; + + vtkAbstractArray *ids = node->GetSelectionList(); + if (!ids) + continue; + + for (vtkIdType i = 0; i < ids->GetNumberOfTuples(); ++i) + { + const vtkIdType cid = + static_cast(ids->GetVariantValue(i).ToLongLong()); + if (cid < 0 || cid >= cell_index->GetNumberOfTuples()) + continue; + + const unsigned int c = + static_cast(cell_index->GetValue(cid)); + const unsigned int f = + static_cast(face_no->GetValue(cid)); + const auto key = std::make_pair(c, f); + + if (subtract_mode) + state.selected_boundary_faces.erase(key); + else + state.selected_boundary_faces.insert(key); + } + } + + UpdateVtkSelectionArrays(state, scene); + } +} + +static void +UpdateVtkSelectionArrays(const MeshState &state, VtkScene &scene) +{ + vtkIntArray *cell_selected = GetCellArray(scene.grid, "selected"); + if (cell_selected) + { + for (vtkIdType i = 0; i < cell_selected->GetNumberOfTuples(); ++i) + cell_selected->SetValue(i, 0); + + vtkIntArray *active_cell_index = + GetCellArray(scene.grid, "active_cell_index"); + if (active_cell_index) + { + for (vtkIdType i = 0; i < active_cell_index->GetNumberOfTuples(); ++i) + { + const unsigned int idx = + static_cast(active_cell_index->GetValue(i)); + if (state.selected_cells.count(idx) > 0) + cell_selected->SetValue(i, 1); + } + } + cell_selected->Modified(); + } + + vtkIntArray *boundary_selected = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("selected")); + if (boundary_selected) + { + for (vtkIdType i = 0; i < boundary_selected->GetNumberOfTuples(); ++i) + boundary_selected->SetValue(i, 0); + + vtkIntArray *cell_index = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("cell_index")); + vtkIntArray *face_no = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("face_no")); + + if (cell_index && face_no) + { + for (vtkIdType i = 0; i < boundary_selected->GetNumberOfTuples(); ++i) + { + const unsigned int c = + static_cast(cell_index->GetValue(i)); + const unsigned int f = + static_cast(face_no->GetValue(i)); + if (state.selected_boundary_faces.count({c, f}) > 0) + boundary_selected->SetValue(i, 1); + } + } + boundary_selected->Modified(); + } + + scene.grid->Modified(); + scene.boundary->Modified(); +} + +static void +UpdateVtkHoverArrays(const MeshState &state, VtkScene &scene) +{ + vtkIntArray *cell_hovered = GetCellArray(scene.grid, "hovered"); + if (cell_hovered) + { + for (vtkIdType i = 0; i < cell_hovered->GetNumberOfTuples(); ++i) + cell_hovered->SetValue(i, 0); + + vtkIntArray *active_cell_index = + GetCellArray(scene.grid, "active_cell_index"); + if (active_cell_index && state.hovered_cell) + { + for (vtkIdType i = 0; i < active_cell_index->GetNumberOfTuples(); ++i) + { + const unsigned int idx = + static_cast(active_cell_index->GetValue(i)); + if (idx == *state.hovered_cell) + { + cell_hovered->SetValue(i, 1); + break; + } + } + } + cell_hovered->Modified(); + } + + vtkIntArray *boundary_hovered = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("hovered")); + if (boundary_hovered) + { + for (vtkIdType i = 0; i < boundary_hovered->GetNumberOfTuples(); ++i) + boundary_hovered->SetValue(i, 0); + + vtkIntArray *cell_index = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("cell_index")); + vtkIntArray *face_no = vtkIntArray::SafeDownCast( + scene.boundary->GetCellData()->GetArray("face_no")); + + if (cell_index && face_no && state.hovered_boundary_face) + { + const unsigned int hc = state.hovered_boundary_face->first; + const unsigned int hf = state.hovered_boundary_face->second; + for (vtkIdType i = 0; i < boundary_hovered->GetNumberOfTuples(); ++i) + { + const unsigned int c = + static_cast(cell_index->GetValue(i)); + const unsigned int f = + static_cast(face_no->GetValue(i)); + if (c == hc && f == hf) + { + boundary_hovered->SetValue(i, 1); + break; + } + } + } + boundary_hovered->Modified(); + } + + scene.grid->Modified(); + scene.boundary->Modified(); +} + +static void +UpdateLutForMaterialIds(const MeshState &state, VtkScene &scene) +{ + unsigned int max_id = 0; + for (const auto &[id, count] : state.material_counts) + { + (void)count; + max_id = std::max(max_id, id); + } + const int n = std::max(1, static_cast(max_id + 1)); + scene.cell_lut->SetRange(0.0, static_cast(n - 1)); + scene.cell_lut->SetNumberOfTableValues(n); + scene.cell_lut->Build(); + + for (int i = 0; i < n; ++i) + { + ImVec4 c = DefaultColorForId(static_cast(i)); + if (auto it = state.material_colors.find(static_cast(i)); + it != state.material_colors.end()) + c = it->second; + scene.cell_lut->SetTableValue(i, c.x, c.y, c.z, 1.0); + } + scene.cell_lut->Modified(); +} + +static void +UpdateLutForBoundaryIds(const MeshState &state, VtkScene &scene) +{ + unsigned int max_id = 0; + for (const auto &[id, count] : state.boundary_counts) + { + (void)count; + max_id = std::max(max_id, id); + } + const int n = std::max(1, static_cast(max_id + 1)); + scene.boundary_lut->SetRange(0.0, static_cast(n - 1)); + scene.boundary_lut->SetNumberOfTableValues(n); + scene.boundary_lut->Build(); + + for (int i = 0; i < n; ++i) + { + ImVec4 c = DefaultColorForId(static_cast(i)); + if (auto it = state.boundary_colors.find(static_cast(i)); + it != state.boundary_colors.end()) + c = it->second; + scene.boundary_lut->SetTableValue(i, c.x, c.y, c.z, 1.0); + } + scene.boundary_lut->Modified(); +} + +static void +InitScene(GLFWwindow *glfw_window, MeshState &state, VtkScene &scene) +{ + scene.render_window->AddRenderer(scene.renderer); + scene.renderer->SetBackground(0.07, 0.08, 0.10); + + scene.render_window->SetReadyForRendering(true); + scene.render_window->SetMapped(1); + scene.render_window->SetOwnContext(0); + scene.render_window->SetOpenGLSymbolLoader(&GlfwGetProcAddress, glfw_window); + + scene.make_current_cb->SetClientData(glfw_window); + scene.make_current_cb->SetCallback([](vtkObject *, unsigned long, void *client_data, void *) { + glfwMakeContextCurrent(static_cast(client_data)); + }); + scene.render_window->AddObserver(vtkCommand::WindowMakeCurrentEvent, + scene.make_current_cb); + + scene.is_current_cb->SetClientData(glfw_window); + scene.is_current_cb->SetCallback( + [](vtkObject *, unsigned long, void *client_data, void *call_data) { + auto *flag = static_cast(call_data); + if (!flag) + return; + *flag = glfwGetCurrentContext() == static_cast(client_data); + }); + scene.render_window->AddObserver(vtkCommand::WindowIsCurrentEvent, + scene.is_current_cb); + + scene.supports_gl_cb->SetCallback( + [](vtkObject *, unsigned long, void *, void *call_data) { + auto *flag = static_cast(call_data); + if (flag) + *flag = 1; + }); + scene.render_window->AddObserver(vtkCommand::WindowSupportsOpenGLEvent, + scene.supports_gl_cb); + + scene.is_direct_cb->SetCallback( + [](vtkObject *, unsigned long, void *, void *call_data) { + auto *flag = static_cast(call_data); + if (flag) + *flag = 1; + }); + scene.render_window->AddObserver(vtkCommand::WindowIsDirectEvent, + scene.is_direct_cb); + + scene.frame_cb->SetCallback([](vtkObject *, unsigned long, void *, void *) { + // Swap buffers is handled by the GLFW loop. + }); + scene.render_window->AddObserver(vtkCommand::WindowFrameEvent, scene.frame_cb); + + scene.interactor->SetRenderWindow(scene.render_window); + scene.interactor->SetInteractorStyle(scene.style); + scene.interactor->Initialize(); + + BuildVtkMeshesFromTriangulation(state, scene); + RebuildCounts(state); + UpdateLutForMaterialIds(state, scene); + UpdateLutForBoundaryIds(state, scene); + + scene.cell_mapper->SetInputData(scene.grid); + scene.cell_mapper->SetScalarModeToUseCellFieldData(); + scene.cell_mapper->SelectColorArray("material_id"); + scene.cell_mapper->SetLookupTable(scene.cell_lut); + scene.cell_mapper->SetUseLookupTableScalarRange(true); + + scene.cell_actor->SetMapper(scene.cell_mapper); + scene.cell_actor->GetProperty()->SetEdgeVisibility(true); + scene.cell_actor->GetProperty()->SetEdgeColor(0.9, 0.9, 0.95); + scene.cell_actor->GetProperty()->SetLineWidth(1.5); + scene.cell_actor->GetProperty()->SetRenderLinesAsTubes(true); + + scene.boundary_mapper->SetInputData(scene.boundary); + scene.boundary_mapper->SetScalarModeToUseCellFieldData(); + scene.boundary_mapper->SelectColorArray("boundary_id"); + scene.boundary_mapper->SetLookupTable(scene.boundary_lut); + scene.boundary_mapper->SetUseLookupTableScalarRange(true); + scene.boundary_mapper->ScalarVisibilityOn(); + scene.boundary_mapper->SetColorModeToMapScalars(); + scene.boundary_actor->SetMapper(scene.boundary_mapper); + scene.boundary_actor->GetProperty()->SetLineWidth(4.0); + scene.boundary_actor->GetProperty()->SetRenderLinesAsTubes(true); + + scene.cell_selected_threshold->SetInputData(scene.grid); + scene.cell_selected_threshold->SetInputArrayToProcess( + 0, + 0, + 0, + vtkDataObject::FIELD_ASSOCIATION_CELLS, + "selected"); + scene.cell_selected_threshold->SetThresholdFunction( + vtkThreshold::THRESHOLD_BETWEEN); + scene.cell_selected_threshold->SetLowerThreshold(0.5); + scene.cell_selected_threshold->SetUpperThreshold(1.5); + + scene.cell_selected_mapper->SetInputConnection( + scene.cell_selected_threshold->GetOutputPort()); + scene.cell_selected_actor->SetMapper(scene.cell_selected_mapper); + scene.cell_selected_actor->GetProperty()->SetRepresentationToWireframe(); + scene.cell_selected_actor->GetProperty()->SetColor(0.10, 0.92, 1.00); + scene.cell_selected_actor->GetProperty()->SetOpacity(1.0); + scene.cell_selected_actor->GetProperty()->SetLineWidth(7.0); + scene.cell_selected_actor->GetProperty()->SetRenderLinesAsTubes(true); + scene.cell_selected_actor->GetProperty()->LightingOff(); + scene.cell_selected_actor->SetPosition(0.0, 0.0, 0.004); + scene.cell_selected_actor->PickableOff(); + + scene.cell_selected_fill_mapper->SetInputConnection( + scene.cell_selected_threshold->GetOutputPort()); + scene.cell_selected_fill_actor->SetMapper(scene.cell_selected_fill_mapper); + scene.cell_selected_fill_actor->GetProperty()->SetRepresentationToSurface(); + scene.cell_selected_fill_actor->GetProperty()->SetColor(0.10, 0.92, 1.00); + scene.cell_selected_fill_actor->GetProperty()->SetOpacity(0.22); + scene.cell_selected_fill_actor->GetProperty()->LightingOff(); + scene.cell_selected_fill_actor->SetPosition(0.0, 0.0, 0.002); + scene.cell_selected_fill_actor->PickableOff(); + + scene.cell_hover_threshold->SetInputData(scene.grid); + scene.cell_hover_threshold->SetInputArrayToProcess( + 0, + 0, + 0, + vtkDataObject::FIELD_ASSOCIATION_CELLS, + "hovered"); + scene.cell_hover_threshold->SetThresholdFunction( + vtkThreshold::THRESHOLD_BETWEEN); + scene.cell_hover_threshold->SetLowerThreshold(0.5); + scene.cell_hover_threshold->SetUpperThreshold(1.5); + scene.cell_hover_mapper->SetInputConnection( + scene.cell_hover_threshold->GetOutputPort()); + scene.cell_hover_actor->SetMapper(scene.cell_hover_mapper); + scene.cell_hover_actor->GetProperty()->SetRepresentationToWireframe(); + scene.cell_hover_actor->GetProperty()->SetColor(0.95, 0.85, 0.20); + scene.cell_hover_actor->GetProperty()->SetOpacity(1.0); + scene.cell_hover_actor->GetProperty()->SetLineWidth(4.0); + scene.cell_hover_actor->GetProperty()->SetRenderLinesAsTubes(true); + scene.cell_hover_actor->GetProperty()->LightingOff(); + scene.cell_hover_actor->SetPosition(0.0, 0.0, 0.006); + scene.cell_hover_actor->PickableOff(); + + scene.boundary_selected_threshold->SetInputData(scene.boundary); + scene.boundary_selected_threshold->SetInputArrayToProcess( + 0, + 0, + 0, + vtkDataObject::FIELD_ASSOCIATION_CELLS, + "selected"); + scene.boundary_selected_threshold->SetThresholdFunction( + vtkThreshold::THRESHOLD_BETWEEN); + scene.boundary_selected_threshold->SetLowerThreshold(0.5); + scene.boundary_selected_threshold->SetUpperThreshold(1.5); + + scene.boundary_selected_geom->SetInputConnection( + scene.boundary_selected_threshold->GetOutputPort()); + scene.boundary_selected_mapper->SetInputConnection( + scene.boundary_selected_geom->GetOutputPort()); + scene.boundary_selected_mapper->SetResolveCoincidentTopologyToPolygonOffset(); + scene.boundary_selected_mapper->SetRelativeCoincidentTopologyPolygonOffsetParameters( + 1.0, 1.0); + scene.boundary_selected_actor->SetMapper(scene.boundary_selected_mapper); + scene.boundary_selected_actor->GetProperty()->SetColor(0.98, 0.30, 0.95); + scene.boundary_selected_actor->GetProperty()->SetLineWidth(12.0); + scene.boundary_selected_actor->GetProperty()->SetRenderLinesAsTubes(true); + scene.boundary_selected_actor->GetProperty()->LightingOff(); + scene.boundary_selected_actor->SetPosition(0.0, 0.0, 0.008); + scene.boundary_selected_actor->PickableOff(); + + scene.boundary_hover_threshold->SetInputData(scene.boundary); + scene.boundary_hover_threshold->SetInputArrayToProcess( + 0, + 0, + 0, + vtkDataObject::FIELD_ASSOCIATION_CELLS, + "hovered"); + scene.boundary_hover_threshold->SetThresholdFunction( + vtkThreshold::THRESHOLD_BETWEEN); + scene.boundary_hover_threshold->SetLowerThreshold(0.5); + scene.boundary_hover_threshold->SetUpperThreshold(1.5); + scene.boundary_hover_geom->SetInputConnection( + scene.boundary_hover_threshold->GetOutputPort()); + scene.boundary_hover_mapper->SetInputConnection( + scene.boundary_hover_geom->GetOutputPort()); + scene.boundary_hover_mapper->SetResolveCoincidentTopologyToPolygonOffset(); + scene.boundary_hover_mapper->SetRelativeCoincidentTopologyPolygonOffsetParameters( + 1.0, 1.0); + scene.boundary_hover_actor->SetMapper(scene.boundary_hover_mapper); + scene.boundary_hover_actor->GetProperty()->SetColor(0.95, 0.85, 0.20); + scene.boundary_hover_actor->GetProperty()->SetLineWidth(10.0); + scene.boundary_hover_actor->GetProperty()->SetRenderLinesAsTubes(true); + scene.boundary_hover_actor->GetProperty()->LightingOff(); + scene.boundary_hover_actor->SetPosition(0.0, 0.0, 0.010); + scene.boundary_hover_actor->PickableOff(); + + scene.renderer->AddActor(scene.cell_actor); + scene.renderer->AddActor(scene.cell_selected_fill_actor); + scene.renderer->AddActor(scene.cell_selected_actor); + scene.renderer->AddActor(scene.cell_hover_actor); + scene.renderer->AddActor(scene.boundary_actor); + scene.renderer->AddActor(scene.boundary_selected_actor); + scene.renderer->AddActor(scene.boundary_hover_actor); + + scene.picker->SetTolerance(1e-2); + scene.picker->PickFromListOn(); + + scene.renderer->ResetCamera(); + scene.render_window->Render(); + + UpdateVtkSelectionArrays(state, scene); + UpdateVtkHoverArrays(state, scene); +} + +static void +RebuildScene(GLFWwindow *glfw_window, + MeshState &state, + VtkScene &scene, + const bool reset_camera) +{ + (void)glfw_window; + BuildVtkMeshesFromTriangulation(state, scene); + RebuildCounts(state); + UpdateLutForMaterialIds(state, scene); + UpdateLutForBoundaryIds(state, scene); + UpdateVtkSelectionArrays(state, scene); + UpdateVtkHoverArrays(state, scene); + if (reset_camera) + scene.renderer->ResetCamera(); + scene.renderer->ResetCameraClippingRange(); + scene.render_window->Render(); +} + +static bool +PickCellOrBoundary(const MeshState &state, + VtkScene &scene, + const int x, + const int y, + unsigned int &out_cell_index, + unsigned int &out_face_no, + unsigned int &out_boundary_id, + unsigned int &out_material_id) +{ + scene.picker->InitializePickList(); + if (state.mode == MeshState::Mode::cell) + scene.picker->AddPickList(scene.cell_actor); + else + scene.picker->AddPickList(scene.boundary_actor); + + if (!scene.picker->Pick(x, y, 0.0, scene.renderer)) + return false; + + vtkDataSet *ds = vtkDataSet::SafeDownCast(scene.picker->GetDataSet()); + if (!ds) + return false; + + const vtkIdType cid = scene.picker->GetCellId(); + if (cid < 0) + return false; + + if (state.mode == MeshState::Mode::cell) + { + vtkIntArray *active_cell_index = GetCellArray(ds, "active_cell_index"); + vtkIntArray *material_id = GetCellArray(ds, "material_id"); + if (!active_cell_index || !material_id) + return false; + out_cell_index = + static_cast(active_cell_index->GetValue(cid)); + out_material_id = + static_cast(material_id->GetValue(cid)); + out_face_no = 0; + out_boundary_id = 0; + return true; + } + else + { + vtkIntArray *cell_index = GetCellArray(ds, "cell_index"); + vtkIntArray *face_no = GetCellArray(ds, "face_no"); + vtkIntArray *boundary_id = GetCellArray(ds, "boundary_id"); + if (!cell_index || !face_no || !boundary_id) + return false; + out_cell_index = + static_cast(cell_index->GetValue(cid)); + out_face_no = + static_cast(face_no->GetValue(cid)); + out_boundary_id = + static_cast(boundary_id->GetValue(cid)); + out_material_id = 0; + return true; + } +} + +static void +ApplySelectionFromPick(MeshState &state, + const unsigned int cell_index, + const unsigned int face_no, + const unsigned int boundary_id, + const unsigned int material_id, + const bool add_mode, + const bool subtract_mode) +{ + const bool replace_mode = !add_mode && !subtract_mode; + + if (state.mode == MeshState::Mode::cell) + { + state.selected_material_id = static_cast(material_id); + if (replace_mode) + state.selected_cells.clear(); + + if (subtract_mode) + state.selected_cells.erase(cell_index); + else if (add_mode && state.selected_cells.count(cell_index) > 0) + state.selected_cells.erase(cell_index); + else + state.selected_cells.insert(cell_index); + } + else + { + state.selected_boundary_id = static_cast(boundary_id); + const auto key = std::make_pair(cell_index, face_no); + if (replace_mode) + state.selected_boundary_faces.clear(); + + if (subtract_mode) + state.selected_boundary_faces.erase(key); + else if (add_mode && state.selected_boundary_faces.count(key) > 0) + state.selected_boundary_faces.erase(key); + else + state.selected_boundary_faces.insert(key); + } +} + +static std::string +ToString(const std::exception &e) +{ + return e.what(); +} + +static bool +LoadTriangulationFromFile(MeshState &state, + const std::filesystem::path &path, + std::string &error) +{ + try + { + state.tria.clear(); + dealii::GridIn<2, 2> grid_in; + grid_in.attach_triangulation(state.tria); + + std::ifstream in(path); + if (!in) + { + error = "Could not open file: " + path.string(); + return false; + } + + const auto ext = path.extension().string(); + if (ext == ".vtk") + grid_in.read_vtk(in); + else if (ext == ".vtu") + grid_in.read_vtu(in); + else + { + error = "Unsupported extension: " + ext + " (supported: .vtk, .vtu)"; + return false; + } + + state.current_label = path.filename().string(); + RebuildCellIndexMap(state); + RebuildCounts(state); + state.selected_boundary_faces.clear(); + state.selected_cells.clear(); + state.selected_boundary_id = -1; + state.selected_material_id = -1; + return true; + } + catch (const std::exception &e) + { + error = ToString(e); + return false; + } +} + +static bool +GenerateTriangulation(MeshState &state, + const std::string &name, + const std::string &args, + const int refinements, + std::string &error) +{ + try + { + state.tria.clear(); + dealii::GridGenerator::generate_from_name_and_arguments(state.tria, + name, + args); + if (refinements > 0) + state.tria.refine_global(static_cast(refinements)); + + state.current_label = name; + RebuildCellIndexMap(state); + RebuildCounts(state); + state.selected_boundary_faces.clear(); + state.selected_cells.clear(); + state.selected_boundary_id = -1; + state.selected_material_id = -1; + return true; + } + catch (const std::exception &e) + { + error = ToString(e); + return false; + } +} + +static bool +SaveTriangulationToFile(const MeshState &state, + const std::filesystem::path &path, + const int format, + std::string &error) +{ + try + { + std::ofstream out(path); + if (!out) + { + error = "Could not open file for writing: " + path.string(); + return false; + } + + dealii::GridOut grid_out; + if (format == 0) + grid_out.write_vtk(state.tria, out); + else + grid_out.write_vtu(state.tria, out); + return true; + } + catch (const std::exception &e) + { + error = ToString(e); + return false; + } +} + +static void +DrawSelectionPanel(MeshState &state, VtkScene &scene) +{ + TallButtonStyle tall_buttons; + constexpr float id_input_width = 90.0f; // ~4 digits + + ImGui::TextUnformatted("Boundary IDs"); + ImGui::Separator(); + bool boundary_colors_changed = false; + for (const auto &[bid, count] : state.boundary_counts) + { + const bool selected = state.selected_boundary_id == static_cast(bid); + ImGui::PushID(static_cast(bid)); + ImVec4 &col = state.boundary_colors[bid]; + if (ImGui::ColorEdit3("##c", &col.x, ImGuiColorEditFlags_NoInputs)) + boundary_colors_changed = true; + ImGui::SameLine(); + char label[128]; + std::snprintf(label, sizeof(label), "%u (%u)", bid, count); + if (ImGui::Selectable(label, selected)) + { + state.selected_boundary_id = static_cast(bid); + state.mode = MeshState::Mode::boundary; + } + ImGui::PopID(); + } + if (boundary_colors_changed) + { + UpdateLutForBoundaryIds(state, scene); + scene.render_window->Render(); + } + + ImGui::SetNextItemWidth(id_input_width); + ImGui::InputScalar("Assign boundary id", + ImGuiDataType_U32, + &state.assign_boundary_id); + { + ImGui::PushID("boundary_actions"); + const auto lay = GetButtonRowLayout(3); + if (ImGui::Button("Select all", ImVec2(lay.w, lay.h)) && + state.selected_boundary_id >= 0) + { + state.mode = MeshState::Mode::boundary; + state.selected_boundary_faces.clear(); + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); + ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + for (unsigned int f = 0; f < cell->n_faces(); ++f) + { + const auto face = cell->face(f); + if (!face->at_boundary()) + continue; + if (static_cast(face->boundary_id()) != + state.selected_boundary_id) + continue; + state.selected_boundary_faces.insert({cell_index, f}); + } + } + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Clear", ImVec2(lay.w, lay.h))) + { + state.selected_boundary_faces.clear(); + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Apply", ImVec2(lay.w, lay.h))) + { + for (const auto &pair : state.selected_boundary_faces) + { + const unsigned int cell_index = pair.first; + const unsigned int face_no = pair.second; + auto it = state.cell_by_index.find(cell_index); + if (it == state.cell_by_index.end()) + continue; + auto face = it->second->face(face_no); + if (face->at_boundary()) + face->set_boundary_id(state.assign_boundary_id); + } + RebuildCounts(state); + RebuildScene(nullptr, state, scene, false); + } + ImGui::PopID(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Material IDs"); + ImGui::Separator(); + bool material_colors_changed = false; + for (const auto &[mid, count] : state.material_counts) + { + const bool selected = state.selected_material_id == static_cast(mid); + ImGui::PushID(static_cast(mid) + 100000); + ImVec4 &col = state.material_colors[mid]; + if (ImGui::ColorEdit3("##c", &col.x, ImGuiColorEditFlags_NoInputs)) + material_colors_changed = true; + ImGui::SameLine(); + char label[128]; + std::snprintf(label, sizeof(label), "%u (%u)", mid, count); + if (ImGui::Selectable(label, selected)) + { + state.selected_material_id = static_cast(mid); + state.mode = MeshState::Mode::cell; + } + ImGui::PopID(); + } + if (material_colors_changed) + { + UpdateLutForMaterialIds(state, scene); + scene.render_window->Render(); + } + + ImGui::SetNextItemWidth(id_input_width); + ImGui::InputScalar("Assign material id", + ImGuiDataType_U32, + &state.assign_material_id); + { + ImGui::PushID("material_actions"); + const auto lay = GetButtonRowLayout(3); + if (ImGui::Button("Select all", ImVec2(lay.w, lay.h)) && + state.selected_material_id >= 0) + { + state.mode = MeshState::Mode::cell; + state.selected_cells.clear(); + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); + ++cell) + { + if (static_cast(cell->material_id()) != state.selected_material_id) + continue; + state.selected_cells.insert( + static_cast(cell->active_cell_index())); + } + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Clear", ImVec2(lay.w, lay.h))) + { + state.selected_cells.clear(); + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Apply", ImVec2(lay.w, lay.h))) + { + for (const auto cell_index : state.selected_cells) + { + auto it = state.cell_by_index.find(cell_index); + if (it == state.cell_by_index.end()) + continue; + it->second->set_material_id(state.assign_material_id); + } + RebuildCounts(state); + RebuildScene(nullptr, state, scene, false); + } + ImGui::PopID(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Face manifold ids"); + ImGui::Separator(); + for (const auto &[mid, count] : state.face_manifold_counts) + { + const int shown_mid = IsFlatManifoldId(mid) ? -1 : static_cast(mid); + const bool selected = state.selected_face_manifold_id == shown_mid; + char label[128]; + if (shown_mid < 0) + std::snprintf(label, sizeof(label), "-1 (%u)", count); + else + std::snprintf(label, sizeof(label), "%u (%u)", mid, count); + if (ImGui::Selectable(label, selected)) + { + state.selected_face_manifold_id = shown_mid; + state.mode = MeshState::Mode::boundary; + } + } + ImGui::SetNextItemWidth(id_input_width); + ImGui::InputScalar("Assign face manifold id", + ImGuiDataType_U32, + &state.assign_face_manifold_id); + { + ImGui::PushID("face_manifold_actions"); + const auto lay = GetButtonRowLayout(3); + if (ImGui::Button("Select all", ImVec2(lay.w, lay.h)) && + state.selected_face_manifold_id >= 0) + { + state.mode = MeshState::Mode::boundary; + state.selected_boundary_faces.clear(); + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + { + const unsigned int cell_index = + static_cast(cell->active_cell_index()); + for (unsigned int f = 0; f < cell->n_faces(); ++f) + { + const auto face = cell->face(f); + if (!face->at_boundary()) + continue; + if (!ManifoldIdMatches(face->manifold_id(), + state.selected_face_manifold_id)) + continue; + state.selected_boundary_faces.insert({cell_index, f}); + } + } + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Clear", ImVec2(lay.w, lay.h))) + { + state.selected_boundary_faces.clear(); + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Apply", ImVec2(lay.w, lay.h))) + { + for (const auto &pair : state.selected_boundary_faces) + { + const unsigned int cell_index = pair.first; + const unsigned int face_no = pair.second; + auto it = state.cell_by_index.find(cell_index); + if (it == state.cell_by_index.end()) + continue; + auto face = it->second->face(face_no); + if (face->at_boundary()) + face->set_manifold_id( + static_cast(state.assign_face_manifold_id)); + } + RebuildCounts(state); + RebuildScene(nullptr, state, scene, false); + } + ImGui::PopID(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Cell manifold ids"); + ImGui::Separator(); + for (const auto &[mid, count] : state.cell_manifold_counts) + { + const int shown_mid = IsFlatManifoldId(mid) ? -1 : static_cast(mid); + const bool selected = state.selected_cell_manifold_id == shown_mid; + char label[128]; + if (shown_mid < 0) + std::snprintf(label, sizeof(label), "-1 (%u)", count); + else + std::snprintf(label, sizeof(label), "%u (%u)", mid, count); + if (ImGui::Selectable(label, selected)) + { + state.selected_cell_manifold_id = shown_mid; + state.mode = MeshState::Mode::cell; + } + } + ImGui::SetNextItemWidth(id_input_width); + ImGui::InputScalar("Assign cell manifold id", + ImGuiDataType_U32, + &state.assign_cell_manifold_id); + { + ImGui::PushID("cell_manifold_actions"); + const auto lay = GetButtonRowLayout(3); + if (ImGui::Button("Select all", ImVec2(lay.w, lay.h)) && + state.selected_cell_manifold_id >= 0) + { + state.mode = MeshState::Mode::cell; + state.selected_cells.clear(); + for (auto cell = state.tria.begin_active(); cell != state.tria.end(); ++cell) + { + if (!ManifoldIdMatches(cell->manifold_id(), + state.selected_cell_manifold_id)) + continue; + state.selected_cells.insert( + static_cast(cell->active_cell_index())); + } + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Clear", ImVec2(lay.w, lay.h))) + { + state.selected_cells.clear(); + UpdateVtkSelectionArrays(state, scene); + } + ImGui::SameLine(); + if (ImGui::Button("Apply", ImVec2(lay.w, lay.h))) + { + for (const auto cell_index : state.selected_cells) + { + auto it = state.cell_by_index.find(cell_index); + if (it == state.cell_by_index.end()) + continue; + it->second->set_manifold_id( + static_cast(state.assign_cell_manifold_id)); + } + RebuildCounts(state); + RebuildScene(nullptr, state, scene, false); + } + ImGui::PopID(); + } +} + +} // namespace + +struct ManipulatorApp::Impl +{ + MeshState mesh; + VtkScene vtk; + FilePicker open_picker; + NewGridDialog new_grid; + SaveDialog save_dialog; + GLFWwindow *glfw_window = nullptr; + + double last_cursor_x = 0.0; + double last_cursor_y = 0.0; + bool camera_button_down = false; // RMB mapped to VTK LMB + + bool lmb_pan_down = false; // LMB mapped to VTK MMB when off-grid + bool box_select_active = false; + bool mouse_over_grid_any = false; + int last_mods = 0; + + ImVec2 box_start_screen = ImVec2(0.0f, 0.0f); + ImVec2 box_end_screen = ImVec2(0.0f, 0.0f); + int box_start_x = 0; // VTK coords (origin bottom-left) + int box_start_y = 0; + int box_end_x = 0; + int box_end_y = 0; + + float pick_tolerance = 1e-2f; + float cell_edge_width = 1.5f; + + int fb_width = 1; + int fb_height = 1; + + std::string status; + + bool initialized = false; + float left_panel_width = 380.0f; + + bool create_default_grid() + { + std::string error; + if (!GenerateTriangulation(mesh, "hyper_cube", "0 : 1 : true", 3, error)) + { + status = "Default grid failed: " + error; + return false; + } + status.clear(); + return true; + } + + void ensure_glad_ready() + { + // VTK's vtkOpenGLState::Reset() can run before vtkOpenGLRenderWindow::OpenGLInit() + // so ensure VTK's bundled GLAD is initialized for the current context. + if (vtk_gladLoadGLUserPtr(&GlfwGladGetProcAddress, glfw_window) == 0) + { + status = "VTK GLAD init failed (gl function pointers missing)"; + } + } + + std::pair + to_vtk_xy(GLFWwindow *win, const double xpos, const double ypos) const + { + int win_w = 1; + int win_h = 1; + glfwGetWindowSize(win, &win_w, &win_h); + win_w = std::max(1, win_w); + win_h = std::max(1, win_h); + + const double sx = + static_cast(fb_width) / static_cast(win_w); + const double sy = + static_cast(fb_height) / static_cast(win_h); + + const int px = static_cast(xpos * sx); + const int py = static_cast(ypos * sy); + return {px, py}; + } +}; + +ManipulatorApp::ManipulatorApp() : impl(std::make_unique()) {} +ManipulatorApp::~ManipulatorApp() = default; + +void +ManipulatorApp::initialize(GLFWwindow *glfw_window) +{ + impl->glfw_window = glfw_window; + glfwMakeContextCurrent(glfw_window); + impl->ensure_glad_ready(); + if (!impl->create_default_grid()) + return; + InitScene(glfw_window, impl->mesh, impl->vtk); + impl->vtk.cell_actor->GetProperty()->SetLineWidth(impl->cell_edge_width); + impl->vtk.picker->SetTolerance(impl->pick_tolerance); + impl->initialized = true; +} + +void +ManipulatorApp::set_framebuffer_size(int width, int height) +{ + impl->fb_width = std::max(1, width); + impl->fb_height = std::max(1, height); + impl->vtk.viewport_w = impl->fb_width; + impl->vtk.viewport_h = impl->fb_height; + impl->vtk.render_window->SetSize(impl->fb_width, impl->fb_height); +} + +void +ManipulatorApp::on_glfw_cursor_pos(GLFWwindow *glfw_window, + double xpos, + double ypos) +{ + if (!impl->initialized) + return; + + impl->last_cursor_x = xpos; + impl->last_cursor_y = ypos; + + const ImGuiIO &imgui_io = ImGui::GetIO(); + if (imgui_io.WantCaptureMouse) + return; + + const auto [px, py] = impl->to_vtk_xy(glfw_window, xpos, ypos); + const bool ctrl = false; + const bool shift = false; + impl->vtk.interactor->SetEventInformationFlipY(px, py, ctrl, shift); + impl->vtk.interactor->InvokeEvent(vtkCommand::MouseMoveEvent, nullptr); + + const int vtk_x = px; + const int vtk_y = impl->fb_height - py; // bottom-left + impl->mouse_over_grid_any = HitOnAnyGeometry(impl->vtk, vtk_x, vtk_y); + + if (impl->box_select_active) + { + impl->box_end_screen = ImGui::GetIO().MousePos; + impl->box_end_x = vtk_x; + impl->box_end_y = vtk_y; + return; + } + + if (impl->camera_button_down || impl->lmb_pan_down || impl->box_select_active) + return; + + unsigned int cell_index = 0; + unsigned int face_no = 0; + unsigned int boundary_id = 0; + unsigned int material_id = 0; + const bool hit = PickCellOrBoundary(impl->mesh, + impl->vtk, + vtk_x, + vtk_y, + cell_index, + face_no, + boundary_id, + material_id); + + bool changed = false; + if (hit) + { + if (impl->mesh.mode == MeshState::Mode::cell) + { + if (!impl->mesh.hovered_cell || *impl->mesh.hovered_cell != cell_index) + changed = true; + impl->mesh.hovered_cell = cell_index; + if (impl->mesh.hovered_boundary_face) + changed = true; + impl->mesh.hovered_boundary_face.reset(); + } + else + { + const std::pair key{cell_index, face_no}; + if (!impl->mesh.hovered_boundary_face || + *impl->mesh.hovered_boundary_face != key) + changed = true; + impl->mesh.hovered_boundary_face = key; + if (impl->mesh.hovered_cell) + changed = true; + impl->mesh.hovered_cell.reset(); + } + } + else + { + if (impl->mesh.hovered_cell || impl->mesh.hovered_boundary_face) + changed = true; + impl->mesh.hovered_cell.reset(); + impl->mesh.hovered_boundary_face.reset(); + } + + if (changed) + UpdateVtkHoverArrays(impl->mesh, impl->vtk); +} + +void +ManipulatorApp::on_glfw_mouse_button(GLFWwindow *glfw_window, + int button, + int action, + int mods) +{ + if (!impl->initialized) + return; + + const ImGuiIO &imgui_io = ImGui::GetIO(); + if (imgui_io.WantCaptureMouse) + return; + + impl->last_mods = mods; + + const auto [px_top, py_top] = + impl->to_vtk_xy(glfw_window, impl->last_cursor_x, impl->last_cursor_y); + const int vtk_x = px_top; + const int vtk_y = impl->fb_height - py_top; // bottom-left + + // RMB -> VTK LMB for camera rotate. + if (button == GLFW_MOUSE_BUTTON_RIGHT) + { + const bool ctrl = (mods & GLFW_MOD_CONTROL) != 0; + const bool shift = (mods & GLFW_MOD_SHIFT) != 0; + impl->vtk.interactor->SetEventInformationFlipY(vtk_x, + py_top, + ctrl, + shift); + + if (action == GLFW_PRESS) + { + impl->camera_button_down = true; + impl->vtk.interactor->InvokeEvent(vtkCommand::LeftButtonPressEvent, + nullptr); + } + else if (action == GLFW_RELEASE) + { + impl->camera_button_down = false; + impl->vtk.interactor->InvokeEvent(vtkCommand::LeftButtonReleaseEvent, + nullptr); + } + return; + } + + // LMB: drag is either box-select (over grid) or pan camera (off grid). + if (button != GLFW_MOUSE_BUTTON_LEFT) + return; + + if (action == GLFW_PRESS) + { + impl->box_start_screen = ImGui::GetIO().MousePos; + impl->box_end_screen = impl->box_start_screen; + impl->box_start_x = vtk_x; + impl->box_start_y = vtk_y; + impl->box_end_x = vtk_x; + impl->box_end_y = vtk_y; + + // Decide mode at press time. + const bool over_grid = HitOnAnyGeometry(impl->vtk, vtk_x, vtk_y); + impl->mouse_over_grid_any = over_grid; + if (over_grid) + { + impl->box_select_active = true; + impl->lmb_pan_down = false; + } + else + { + impl->box_select_active = false; + impl->lmb_pan_down = true; + + const bool ctrl = (mods & GLFW_MOD_CONTROL) != 0; + const bool shift = (mods & GLFW_MOD_SHIFT) != 0; + impl->vtk.interactor->SetEventInformationFlipY(vtk_x, + py_top, + ctrl, + shift); + impl->vtk.interactor->InvokeEvent(vtkCommand::MiddleButtonPressEvent, + nullptr); + } + return; + } + + if (action == GLFW_RELEASE) + { + if (impl->lmb_pan_down) + { + impl->lmb_pan_down = false; + const bool ctrl = (mods & GLFW_MOD_CONTROL) != 0; + const bool shift = (mods & GLFW_MOD_SHIFT) != 0; + impl->vtk.interactor->SetEventInformationFlipY(vtk_x, + py_top, + ctrl, + shift); + impl->vtk.interactor->InvokeEvent( + vtkCommand::MiddleButtonReleaseEvent, nullptr); + return; + } + + if (impl->box_select_active) + { + impl->box_end_screen = ImGui::GetIO().MousePos; + impl->box_end_x = vtk_x; + impl->box_end_y = vtk_y; + + const float dx = + std::abs(impl->box_end_screen.x - impl->box_start_screen.x); + const float dy = + std::abs(impl->box_end_screen.y - impl->box_start_screen.y); + + const bool add_mode = (mods & GLFW_MOD_SHIFT) != 0; + const bool subtract_mode = (mods & GLFW_MOD_ALT) != 0; + + if (dx < 3.0f && dy < 3.0f) + { + unsigned int cell_index = 0; + unsigned int face_no = 0; + unsigned int boundary_id = 0; + unsigned int material_id = 0; + if (PickCellOrBoundary(impl->mesh, + impl->vtk, + vtk_x, + vtk_y, + cell_index, + face_no, + boundary_id, + material_id)) + { + ApplySelectionFromPick(impl->mesh, + cell_index, + face_no, + boundary_id, + material_id, + add_mode, + subtract_mode); + UpdateVtkSelectionArrays(impl->mesh, impl->vtk); + } + } + else + { + ApplyBoxSelection(impl->mesh, + impl->vtk, + impl->box_start_x, + impl->box_start_y, + impl->box_end_x, + impl->box_end_y, + add_mode, + subtract_mode); + } + + impl->box_select_active = false; + return; + } + } + + // Other buttons are ignored. +} + +void +ManipulatorApp::on_glfw_scroll(GLFWwindow *glfw_window, + double xoffset, + double yoffset) +{ + (void)xoffset; + if (!impl->initialized) + return; + + const ImGuiIO &imgui_io = ImGui::GetIO(); + if (imgui_io.WantCaptureMouse) + return; + + const auto [px, py] = + impl->to_vtk_xy(glfw_window, impl->last_cursor_x, impl->last_cursor_y); + const bool ctrl = false; + const bool shift = false; + impl->vtk.interactor->SetEventInformationFlipY(px, py, ctrl, shift); + + const int steps = RoundScrollSteps(yoffset); + if (steps > 0) + for (int i = 0; i < steps; ++i) + impl->vtk.interactor->InvokeEvent(vtkCommand::MouseWheelForwardEvent, + nullptr); + else if (steps < 0) + for (int i = 0; i < -steps; ++i) + impl->vtk.interactor->InvokeEvent(vtkCommand::MouseWheelBackwardEvent, + nullptr); +} + +void +ManipulatorApp::process_input(GLFWwindow *glfw_window) +{ + if (!impl->initialized) + return; + + (void)glfw_window; + // Selection/camera interactions are handled via GLFW callbacks. +} + +void +ManipulatorApp::render_frame() +{ + if (!impl->initialized) + return; + impl->vtk.render_window->Render(); +} + +void +ManipulatorApp::draw_ui() +{ + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Open...")) + { + const auto start_dir = std::filesystem::current_path().string(); + impl->open_picker.open_modal("Open VTK", start_dir, ".vtk"); + } + if (ImGui::MenuItem("New...")) + impl->new_grid.open_modal(); + if (ImGui::MenuItem("Save...")) + { + const auto start_dir = std::filesystem::current_path().string(); + impl->save_dialog.open_modal(start_dir); + } + if (ImGui::MenuItem("Quit")) + glfwSetWindowShouldClose(impl->glfw_window, GLFW_TRUE); + ImGui::EndMenu(); + } + + if (!impl->mesh.current_label.empty()) + { + ImGui::Separator(); + ImGui::TextUnformatted(impl->mesh.current_label.c_str()); + } + + ImGui::EndMainMenuBar(); + } + + // Layout: left tools panel (ImGui window) + splitter (ImGui window) + + // instructions overlay (ImGui window without inputs/background). The mesh is + // rendered directly by VTK to the whole framebuffer behind these windows. + const ImGuiViewport *vp = ImGui::GetMainViewport(); + const ImVec2 root_pos = vp ? vp->WorkPos : ImVec2(0, 0); + const ImVec2 root_size = vp ? vp->WorkSize : ImGui::GetIO().DisplaySize; + const ImVec2 vp_pos = vp ? vp->Pos : ImVec2(0, 0); + const ImVec2 vp_size = vp ? vp->Size : ImVec2(impl->fb_width, impl->fb_height); + + const float splitter_width = 6.0f; + const float min_left = 260.0f; + const float min_right = 240.0f; + const float full_width = root_size.x; + const float max_left = + std::max(min_left, full_width - splitter_width - min_right); + impl->left_panel_width = + std::max(min_left, std::min(max_left, impl->left_panel_width)); + + // Constrain the VTK renderer viewport to the right panel (and exclude the + // menu bar/work-area offset) so ResetCamera centers in the visible area. + if (vp && impl->fb_width > 1 && impl->fb_height > 1) + { + const double sx = + static_cast(impl->fb_width) / std::max(1.0f, vp_size.x); + const double sy = + static_cast(impl->fb_height) / std::max(1.0f, vp_size.y); + + const double work_off_x = static_cast(root_pos.x - vp_pos.x); + const double work_off_y = static_cast(root_pos.y - vp_pos.y); + + const double x0_fb = (work_off_x + impl->left_panel_width + splitter_width) * sx; + const double ymax_fb = static_cast(impl->fb_height) - (work_off_y * sy); + + const double xmin = x0_fb / static_cast(impl->fb_width); + const double xmax = 1.0; + const double ymin = 0.0; + const double ymax = ymax_fb / static_cast(impl->fb_height); + UpdateRendererViewport(impl->vtk, xmin, ymin, xmax, ymax); + } + + ImGui::SetNextWindowPos(root_pos); + ImGui::SetNextWindowSize(ImVec2(impl->left_panel_width, root_size.y)); + const ImGuiWindowFlags left_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; + ImGui::Begin("##coral_manipulator_left", nullptr, left_flags); + + if (ImGui::Button("Save...")) + { + const auto start_dir = std::filesystem::current_path().string(); + impl->save_dialog.open_modal(start_dir); + } + + ImGui::SameLine(); + ImGui::TextUnformatted("Pick tolerance"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(140.0f); + if (ImGui::DragFloat("##pick_tol", + &impl->pick_tolerance, + 0.0005f, + 0.0001f, + 0.05f, + "%.4f")) + { + impl->pick_tolerance = + std::max(0.0001f, std::min(0.05f, impl->pick_tolerance)); + impl->vtk.picker->SetTolerance(impl->pick_tolerance); + } + + ImGui::SameLine(); + ImGui::TextUnformatted("Cell edge width"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::DragFloat("##cell_edge_width", + &impl->cell_edge_width, + 0.1f, + 0.5f, + 12.0f, + "%.1f")) + { + impl->cell_edge_width = std::max(0.5f, std::min(12.0f, impl->cell_edge_width)); + impl->vtk.cell_actor->GetProperty()->SetLineWidth(impl->cell_edge_width); + impl->vtk.render_window->Render(); + } + + ImGui::TextUnformatted("Selection"); + ImGui::Separator(); + + if (auto selected = impl->open_picker.draw()) + { + std::string error; + if (!LoadTriangulationFromFile(impl->mesh, *selected, error)) + { + impl->status = "Open failed: " + error; + } + else + { + impl->status.clear(); + RebuildScene(impl->glfw_window, impl->mesh, impl->vtk, true); + } + } + + if (auto save = impl->save_dialog.draw()) + { + std::string error; + if (!SaveTriangulationToFile(impl->mesh, save->path, save->format, error)) + { + impl->status = "Save failed: " + error; + } + else + { + impl->status = "Saved: " + save->path.string(); + impl->mesh.current_label = save->path.filename().string(); + } + } + + if (impl->new_grid.open) + { + const ImGuiWindowFlags flags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings; + if (impl->new_grid.pending_open_popup) + { + ImGui::OpenPopup("New Grid"); + impl->new_grid.pending_open_popup = false; + } + if (ImGui::BeginPopupModal("New Grid", &impl->new_grid.open, flags)) + { + ImGui::TextUnformatted("GridGenerator::generate_from_name_and_arguments"); + ImGui::Separator(); + + ImGui::InputText("Name", &impl->new_grid.name); + ImGui::InputText("Arguments", &impl->new_grid.arguments); + ImGui::InputInt("N refinements", &impl->new_grid.refinements); + impl->new_grid.refinements = std::max(0, impl->new_grid.refinements); + + ImGui::TextUnformatted("Example:"); + ImGui::TextUnformatted(" name: hyper_ball"); + ImGui::TextUnformatted(" args: 0.0, 0.0 : 1 : false"); + + if (!impl->new_grid.error.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", impl->new_grid.error.c_str()); + } + + ImGui::Separator(); + if (ImGui::Button("Create")) + { + std::string error; + if (!GenerateTriangulation(impl->mesh, + impl->new_grid.name, + impl->new_grid.arguments, + impl->new_grid.refinements, + error)) + { + impl->new_grid.error = error; + } + else + { + impl->status.clear(); + impl->new_grid.open = false; + ImGui::CloseCurrentPopup(); + RebuildScene(impl->glfw_window, impl->mesh, impl->vtk, true); + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + impl->new_grid.open = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + } + + int mode_int = static_cast(impl->mesh.mode); + if (ImGui::RadioButton("Boundary faces", mode_int == 0)) + impl->mesh.mode = MeshState::Mode::boundary; + ImGui::SameLine(); + if (ImGui::RadioButton("Cells", mode_int == 1)) + impl->mesh.mode = MeshState::Mode::cell; + + ImGui::Separator(); + DrawSelectionPanel(impl->mesh, impl->vtk); + + if (!impl->status.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", impl->status.c_str()); + } + + ImGui::End(); + + ImGui::SetNextWindowPos(ImVec2(root_pos.x + impl->left_panel_width, root_pos.y)); + ImGui::SetNextWindowSize(ImVec2(splitter_width, root_size.y)); + const ImGuiWindowFlags splitter_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; + ImGui::Begin("##coral_manipulator_splitter", nullptr, splitter_flags); + const float splitter_height = root_size.y; + ImGui::InvisibleButton("##coral_manipulator_splitter_btn", + ImVec2(splitter_width, splitter_height)); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (ImGui::IsItemActive()) + { + impl->left_panel_width += ImGui::GetIO().MouseDelta.x; + impl->left_panel_width = + std::max(min_left, std::min(max_left, impl->left_panel_width)); + } + ImGui::End(); + + ImGui::SetNextWindowPos(ImVec2(root_pos.x + impl->left_panel_width + splitter_width, + root_pos.y)); + ImGui::SetNextWindowSize( + ImVec2(std::max(0.0f, root_size.x - impl->left_panel_width - splitter_width), + root_size.y)); + const ImGuiWindowFlags overlay_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + ImGui::Begin("##coral_manipulator_view_overlay", nullptr, overlay_flags); + + ImDrawList *dl = ImGui::GetWindowDrawList(); + const ImVec2 p0 = ImGui::GetCursorScreenPos(); + const ImVec2 pad(8.0f, 6.0f); + const char *help = impl->mouse_over_grid_any ? + "LMB drag: box select | LMB click: select | Shift add | Alt subtract | RMB drag: rotate | Wheel: zoom" : + "LMB drag: pan camera | RMB drag: rotate | Wheel: zoom"; + const ImVec2 text_sz = ImGui::CalcTextSize(help); + const ImVec2 bg_min(p0.x + pad.x, p0.y + pad.y); + const ImVec2 bg_max(bg_min.x + text_sz.x + 2.0f * pad.x, + bg_min.y + text_sz.y + 2.0f * pad.y); + dl->AddRectFilled(bg_min, + bg_max, + ImGui::GetColorU32(ImVec4(0.05f, 0.05f, 0.06f, 0.65f)), + 6.0f); + dl->AddText(ImVec2(bg_min.x + pad.x, bg_min.y + pad.y), + ImGui::GetColorU32(ImVec4(1, 1, 1, 0.90f)), + help); + + if (impl->box_select_active) + { + ImDrawList *fg = ImGui::GetForegroundDrawList(); + const ImU32 fill_col = ImGui::GetColorU32(ImVec4(0.95f, 0.85f, 0.20f, 0.10f)); + const ImU32 line_col = ImGui::GetColorU32(ImVec4(0.95f, 0.85f, 0.20f, 0.90f)); + const ImVec2 a = impl->box_start_screen; + const ImVec2 b = impl->box_end_screen; + const ImVec2 rmin(std::min(a.x, b.x), std::min(a.y, b.y)); + const ImVec2 rmax(std::max(a.x, b.x), std::max(a.y, b.y)); + fg->AddRectFilled(rmin, rmax, fill_col, 0.0f); + fg->AddRect(rmin, rmax, line_col, 0.0f, 0, 2.0f); + } + + ImGui::End(); +} + +std::string +ManipulatorApp::status_text() const +{ + return impl->status; +} + +} // namespace coral::manipulator diff --git a/include/type_name.h b/include/type_name.h deleted file mode 100644 index fc2dadc..0000000 --- a/include/type_name.h +++ /dev/null @@ -1,1487 +0,0 @@ -#ifndef BOOST_CORE_TYPE_NAME_HPP_INCLUDED -#define BOOST_CORE_TYPE_NAME_HPP_INCLUDED - -// MS compatible compilers support #pragma once - -#if defined(_MSC_VER) && (_MSC_VER >= 1020) -# pragma once -#endif - -// std::string boost::core::type_name() -// -// Copyright 2021 Peter Dimov -// Distributed under the Boost Software License, Version 1.0. -// https://www.boost.org/LICENSE_1_0.txt - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#if !defined(BOOST_NO_CXX17_HDR_STRING_VIEW) -# include -#endif - -namespace boost -{ - namespace core - { - namespace detail - { - - // tn_identity - - template - struct tn_identity - { - typedef T type; - }; - - // tn_remove_prefix - - inline bool - tn_remove_prefix(std::string &str, const char *prefix) - { - std::size_t n = std::strlen(prefix); - - if (str.substr(0, n) == prefix) - { - str = str.substr(n); - return true; - } - else - { - return false; - } - } - -#if !defined(BOOST_NO_TYPEID) - - // typeid_name - - inline std::string - fix_typeid_name(const char *n) - { - std::string r = boost::core::demangle(n); - -# if defined(_MSC_VER) - - tn_remove_prefix(r, "class "); - tn_remove_prefix(r, "struct "); - tn_remove_prefix(r, "enum "); - -# endif - - // libc++ inline namespace - - if (tn_remove_prefix(r, "std::__1::")) - { - r = "std::" + r; - } - - // libstdc++ inline namespace - - if (tn_remove_prefix(r, "std::__cxx11::")) - { - r = "std::" + r; - } - -# if defined(BOOST_MSVC) && BOOST_MSVC == 1600 - - // msvc-10.0 puts TR1 things in std::tr1 - - if (tn_remove_prefix(r, "std::tr1::")) - { - r = "std::" + r; - } - -# endif - - return r; - } - - - - template - std::string - typeid_name() - { - return fix_typeid_name(typeid(T).name()); - } - - // template names - - template - std::string - class_template_name() - { -# if defined(BOOST_GCC) - - std::string r = typeid_name(); - -# else - - std::string r = typeid_name(); - -# endif - return r.substr(0, r.find('<')); - } - - - - template - std::string - sequence_template_name() - { - return detail::class_template_name(); - } - - - - template - std::string - set_template_name() - { - return detail::class_template_name(); - } - - - - template - std::string - map_template_name() - { - return detail::class_template_name(); - } - - - - template - std::string - array_template_name() - { - return detail::class_template_name(); - } - -#else // #if !defined(BOOST_NO_TYPEID) - - template - std::string - typeid_name() - { - return "_Tp"; - } - - - - template - std::string - class_template_name() - { - return "_Tm"; - } - - - - template - std::string - sequence_template_name() - { - return "_Sq"; - } - - - - template - std::string - set_template_name() - { - return "_St"; - } - - - - template - std::string - map_template_name() - { - return "_Mp"; - } - - - - template - std::string - array_template_name() - { - return "_Ar"; - } - -#endif - - // tn_to_string - -#if defined(BOOST_MSVC) -# pragma warning(push) -# pragma warning(disable : 4996) -#endif - - inline std::string - tn_to_string(std::size_t n) - { - char buffer[32]; - const auto written = std::snprintf(buffer, - sizeof(buffer), - "%lu", - static_cast(n)); - - if (written < 0) - return {}; - - return std::string(buffer, - static_cast(std::min( - written, static_cast(sizeof(buffer) - 1)))); - } - -#if defined(BOOST_MSVC) -# pragma warning(pop) -#endif - - // tn_holder - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return typeid_name() + suffix; - } - }; - - // integrals - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "bool" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "char" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "signed char" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "unsigned char" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "short" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "unsigned short" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "int" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "unsigned" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "long" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "unsigned long" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "long long" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "unsigned long long" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "wchar_t" + suffix; - } - }; - -#if !defined(BOOST_NO_CXX11_CHAR16_T) - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "char16_t" + suffix; - } - }; - -#endif - -#if !defined(BOOST_NO_CXX11_CHAR32_T) - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "char32_t" + suffix; - } - }; - -#endif - -#if defined(__cpp_char8_t) && __cpp_char8_t >= 201811L - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "char8_t" + suffix; - } - }; - -#endif - -#if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "std::byte" + suffix; - } - }; - -#endif - - // floating point - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "float" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "double" + suffix; - } - }; - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "long double" + suffix; - } - }; - - // void - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "void" + suffix; - } - }; - - // nullptr_t - -#if !defined(BOOST_NO_CXX11_NULLPTR) - - template <> - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return "std::nullptr_t" + suffix; - } - }; - -#endif - - // cv - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name(" const" + suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name(" volatile" + suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name(" const volatile" + suffix); - } - }; - - // refs - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name("&" + suffix); - } - }; - -#if !defined(BOOST_NO_CXX11_RVALUE_REFERENCES) - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name("&&" + suffix); - } - }; - -#endif - - // function types - -#if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) - - // tn_add_each - - template - int - tn_add_each_impl(std::string &st) - { - if (!st.empty()) - st += ", "; - st += tn_holder::type_name(""); - return 0; - } - - - - template - std::string - tn_add_each() - { - std::string st; - - typedef int A[sizeof...(T) + 1]; - (void)A{0, tn_add_each_impl(st)...}; - - return st; - } - - - - template - std::string - function_type_name(tn_identity, - const std::string &trailer, - const std::string &suffix) - { - std::string r = tn_holder::type_name(""); - - if (!suffix.empty()) - { - r += '('; - - if (suffix[0] == ' ') - { - r += suffix.substr(1); - } - else - { - r += suffix; - } - - r += ')'; - } - - r += '(' + tn_add_each() + ')'; - r += trailer; - - return r; - } - - - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), "", suffix); - } - }; - -# if !defined(BOOST_MSVC) || BOOST_MSVC >= 1900 - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile", - suffix); - } - }; - -# endif - -# if !defined(BOOST_NO_CXX11_REF_QUALIFIERS) - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " &", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const &", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile &", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile &", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " &&", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const &&", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile &&", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile &&", - suffix); - } - }; - -# endif - -# if defined(__cpp_noexcept_function_type) || \ - defined(_NOEXCEPT_TYPES_SUPPORTED) - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " & noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const & noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile & noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile & noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " && noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const && noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile && noexcept", - suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile && noexcept", - suffix); - } - }; - -# endif - -#endif // #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) - - // pointers - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name("*" + suffix); - } - }; - - // arrays - - template - std::pair - array_prefix_suffix(tn_identity) - { - return std::pair(tn_holder::type_name(""), - ""); - } - - - - template - std::pair - array_prefix_suffix(tn_identity) - { - std::pair r = - detail::array_prefix_suffix(tn_identity()); - - r.second = '[' + tn_to_string(N) + ']' + r.second; - - return r; - } - - - - template - std::string - array_type_name(tn_identity, const std::string &suffix) - { - std::pair r = - detail::array_prefix_suffix(tn_identity()); - - if (suffix.empty()) - { - return r.first + "[]" + r.second; - } - else - { - return r.first + '(' + suffix + ")[]" + r.second; - } - } - - - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } // namespace detail - }; // namespace core - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } // namespace boost - }; - - template - struct tn_holder{ - static std::string type_name(const std::string &suffix){ - return detail::array_type_name(tn_identity(), - suffix); - } // namespace detail - }; // namespace core - - template - std::string - array_type_name(tn_identity, const std::string &suffix) - { - std::pair r = - detail::array_prefix_suffix(tn_identity()); - - if (suffix.empty()) - { - return r.first + r.second; - } - else - { - return r.first + '(' + suffix + ")" + r.second; - } - } - - - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::array_type_name(tn_identity(), suffix); - } - }; - - template - struct tn_holder { - static std::string type_name(std::string const &suffix) - { - return detail::array_type_name(tn_identity(), - suffix); - } - }; - - // pointers to members - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return tn_holder::type_name(' ' + tn_holder::type_name("") + "::*" + - suffix); - } - }; - -#if defined(BOOST_MSVC) && BOOST_MSVC < 1900 && \ - !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - "", - ' ' + tn_holder::type_name("") + - "::*" + suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const", - ' ' + tn_holder::type_name("") + - "::*" + suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " volatile", - ' ' + tn_holder::type_name("") + - "::*" + suffix); - } - }; - - template - struct tn_holder - { - static std::string - type_name(const std::string &suffix) - { - return detail::function_type_name(tn_identity(), - " const volatile", - ' ' + tn_holder::type_name("") + - "::*" + suffix); - } - }; - -#endif - - // strings - - template