diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..81b3845 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: [master] + tags: ["*"] + pull_request: + branches: [master] + +jobs: + # Build the full tree and run ctest on each OS. + build-test: + name: Build & test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Conan + run: pip install "conan>=2.10.0" + + - name: Install GLFW system dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + xorg-dev libgl1-mesa-dev \ + libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules + + - name: Configure, build, test (Unix) + if: runner.os != 'Windows' + run: | + ./setup_build.sh build + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel + ctest --test-dir build --output-on-failure + shell: bash + + # MinGW gcc so find_package re-rooting works like the Unix path. + - name: Configure, build, test (Windows) + if: runner.os == 'Windows' + run: | + cat > win_profile.txt <<'EOF' + [settings] + os=Windows + arch=x86_64 + compiler=gcc + compiler.version=14 + compiler.libcxx=libstdc++11 + compiler.cppstd=17 + build_type=Release + [buildenv] + CC=gcc + CXX=g++ + EOF + conan install conan/conanfile.txt -of=build -pr:a win_profile.txt --build=missing + cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel + ctest --test-dir build --output-on-failure + shell: bash + + # Build the nanobind wheel. Windows excluded: the SKBUILD path shells + # out to setup_build.sh (bash). + wheels: + name: Build wheel (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v3.1.0 + env: + # Conan comes from [build-system].requires. + CIBW_ARCHS: native + + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index fd9e563..6f0d6eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,9 @@ if (SKBUILD AND NOT CMAKE_CI_BUILD) endif() list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_BINARY_DIR}) +# CMAKE_FIND_ROOT_PATH re-rooting doesn't reach the binary dir on native +# Windows; a plain prefix does, on every platform. +list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(flags) diff --git a/app/main_window.cc b/app/main_window.cc index e6a21bb..5ee8674 100644 --- a/app/main_window.cc +++ b/app/main_window.cc @@ -434,6 +434,94 @@ namespace piper::studio return IM_COL32(r, g, b, a); } + // The "Add node" menu tree. A node type's `category` is a '/'- + // delimited path, so users grouping their own node packs (e.g. + // "hal/motor") get nested submenus. Empty category -> the type + // sits at the root level. std::map keeps submenus name-sorted. + struct AddNodeMenuTree + { + std::map children; + std::vector types; + }; + + void sort_add_node_tree(AddNodeMenuTree& tree) + { + std::sort(tree.types.begin(), tree.types.end(), + [](piper::NodeType const* a, piper::NodeType const* b) + { + return a->type < b->type; + }); + for (auto& kv : tree.children) + { + sort_add_node_tree(kv.second); + } + } + + AddNodeMenuTree build_add_node_tree( + std::vector const& types) + { + AddNodeMenuTree root; + for (auto const* nt : types) + { + AddNodeMenuTree* cur = &root; + std::string_view const cat = nt->category; + std::size_t start = 0; + while (start < cat.size()) + { + std::size_t const slash = cat.find('/', start); + std::size_t end = cat.size(); + if (slash != std::string_view::npos) + { + end = slash; + } + std::string_view const seg = cat.substr(start, end - start); + if (not seg.empty()) + { + cur = &cur->children[std::string(seg)]; + } + if (slash == std::string_view::npos) + { + break; + } + start = slash + 1; + } + cur->types.push_back(nt); + } + sort_add_node_tree(root); + return root; + } + + // Returns the type the user clicked this frame, or nullptr. The + // caller owns node creation so this stays free of Document state. + piper::NodeType const* draw_add_node_tree(AddNodeMenuTree const& tree) + { + piper::NodeType const* chosen = nullptr; + for (auto const* nt : tree.types) + { + if (ImGui::MenuItem(nt->type.c_str())) + { + chosen = nt; + } + } + if (not tree.types.empty() and not tree.children.empty()) + { + ImGui::Separator(); + } + for (auto const& kv : tree.children) + { + if (ImGui::BeginMenu(kv.first.c_str())) + { + piper::NodeType const* const c = draw_add_node_tree(kv.second); + if (c != nullptr) + { + chosen = c; + } + ImGui::EndMenu(); + } + } + return chosen; + } + void MainWindow::wire_document_callbacks(Document& doc) { Document* dp = &doc; @@ -560,44 +648,10 @@ namespace piper::studio if (ImGui::BeginMenu("Add node")) { - std::map> by_cat; - for (auto const* nt : registry_.all()) - { - by_cat[nt->category].push_back(nt); - } - auto const draw_type_item = [&](piper::NodeType const* nt) - { - if (ImGui::MenuItem(nt->type.c_str())) - { - add_node_at(*dp, *nt, canvas_pos); - } - }; - auto const it_uncat = by_cat.find(""); - if (it_uncat != by_cat.end()) - { - for (auto const* nt : it_uncat->second) - { - draw_type_item(nt); - } - if (by_cat.size() > 1) - { - ImGui::Separator(); - } - } - for (auto const& kv : by_cat) + AddNodeMenuTree const tree = build_add_node_tree(registry_.all()); + if (piper::NodeType const* nt = draw_add_node_tree(tree)) { - if (kv.first.empty()) - { - continue; - } - if (ImGui::BeginMenu(kv.first.c_str())) - { - for (auto const* nt : kv.second) - { - draw_type_item(nt); - } - ImGui::EndMenu(); - } + add_node_at(*dp, *nt, canvas_pos); } ImGui::EndMenu(); } diff --git a/cmake/flags.cmake b/cmake/flags.cmake index 43bb7f1..c307907 100644 --- a/cmake/flags.cmake +++ b/cmake/flags.cmake @@ -19,4 +19,10 @@ else() message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}. Only GCC, Clang, and AppleClang are supported: please add your definitions here.") endif() -set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +if(MINGW) + # Statically link the gcc/libstdc++/winpthread runtimes so the binary + # can't load a shadowing libstdc++-6.dll from another toolchain on + # PATH -- a mismatched one corrupts libstdc++'s locale/iostream state + # and crashes std::ifstream while leaving simpler ops working. + add_link_options(-static) +endif() diff --git a/docs/type_system.md b/docs/type_system.md index ff9c030..0f169b3 100644 --- a/docs/type_system.md +++ b/docs/type_system.md @@ -115,6 +115,17 @@ reg.add(nt); For external catalogs, write the same `NodeType` shape in JSON and load via `v2::deserialize_registry` -- see `docs/v2_format.md`. +`category` is a `/`-delimited path that lays out the "Add node" +context menu. A single segment (`"control"`) is one submenu; nesting +segments (`"hal/motor"`) builds the submenu tree, letting a project +group its own node pack however it likes: + +```cpp +nt.category = "my_project/hal/motor"; // Add node > my_project > hal > motor +``` + +An empty `category` puts the type at the menu root. + ## Drift detection When a graph saved against an older registry is loaded against a newer diff --git a/docs/v2_format.md b/docs/v2_format.md index 2d2307b..19ec6b9 100644 --- a/docs/v2_format.md +++ b/docs/v2_format.md @@ -302,7 +302,7 @@ Each type entry: |--- |--- |--- |--- | | `type` | string | yes | Unique within `types`. Duplicate names -> `DuplicateTypeName` diagnostic; first entry wins. | | `library` | string | optional | Free-form tag for grouping in palettes (e.g. `"math"`, `"control"`). | -| `category` | string | optional | Free-form tag (e.g. `"filter"`, `"generator"`). | +| `category` | string | optional | `/`-delimited path grouping the type in the "Add node" menu (e.g. `"filter"`, or `"hal/motor"` for a nested submenu). Empty -> the type sits at the menu root. | | `help` | string | optional | One-line description. | | `attributes` | array | optional | Each entry is an `AttributeSpec`: `name`, `data_type`, `role` (required) plus optional `default_value`. Same shape as graph-file attributes minus `value` and `stages`. | diff --git a/pyproject.toml b/pyproject.toml index da6164a..9d1bcf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["scikit-build-core >=0.10", "nanobind >=2.0"] +requires = ["scikit-build-core >=0.10", "nanobind >=2.0", "conan >=2.10.0"] build-backend = "scikit_build_core.build" [project]