Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
128 changes: 91 additions & 37 deletions app/main_window.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string, AddNodeMenuTree> children;
std::vector<piper::NodeType const*> 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<piper::NodeType const*> 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;
Expand Down Expand Up @@ -560,44 +648,10 @@ namespace piper::studio

if (ImGui::BeginMenu("Add node"))
{
std::map<std::string, std::vector<piper::NodeType const*>> 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();
}
Expand Down
8 changes: 7 additions & 1 deletion cmake/flags.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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()
11 changes: 11 additions & 0 deletions docs/type_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/v2_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
Loading