Author SHA1 Message Date
Martin Felis 5ac22ebac0 Added graph design ramblings. 2025-11-21 13:17:47 +01:00
Martin Felis 64cdddea96 Minor serialization refactoring. 2025-04-24 19:01:24 +02:00
Martin Felis 65c7a9aaaf AnimLibraries do not load animations by default. 2025-04-13 22:35:06 +02:00
Martin Felis 5d01dfcca2 Started working on AnimationLibrary. 2025-04-11 19:03:13 +02:00
Martin Felis 89fedce539 Cleanup. 2025-04-11 13:08:19 +02:00
Martin Felis f6f7e92cea Added name to AnimationResource. 2025-04-11 13:05:50 +02:00
Martin Felis d32c247cc0 Cleaned up includes. 2025-04-11 13:03:38 +02:00
Martin Felis 36c7f7a11e Refactor/cleanup of SyncTrack and added serialization to/from json. 2025-04-11 12:22:03 +02:00
Martin Felis 3b5537fc9d Minor cleanup. 2025-04-11 12:22:03 +02:00
Martin Felis c173707a18 Minor cleanup and refactoring. 2025-03-30 22:35:49 +02:00
Martin Felis 887131af37 Added AnimationFileResource that combine animations with sync tracks. 2025-03-20 16:16:16 +01:00
Martin Felis 86ea476881 Renamed AnimData to Pose. 2025-03-19 21:43:01 +01:00
Martin Felis f52b19a8d2 Further cleanup and added comments. 2025-03-19 21:12:27 +01:00
Martin Felis 283306f225 Minor cleanup and added comments. 2025-03-18 22:20:50 +01:00
Martin Felis b4eda31242 AnimGraphEvalTests now properly evaluates. 2025-03-17 22:23:12 +01:00
Martin Felis 1870a9d214 Added custom imgui-node-editor changes. 2025-03-16 22:59:05 +01:00
Martin Felis 07d02a2e42 AnimGraphEvalTests now compiles again. 2025-03-16 22:58:14 +01:00
Martin Felis 2ea2c56bbb Refactored BlendTreeResources to be a subclass of AnimGraphResource.
The latter is used to abstract saving/loading for both BlendTrees and StateMachines.
2025-03-16 18:15:31 +01:00
Martin Felis f351939d61 Tiny cleanup. 2025-03-14 12:40:08 +01:00
Martin Felis a977698336 Fixed crash when starting up and no proper application config was available. 2025-03-14 12:36:22 +01:00
Martin Felis 0f9f9d6283 Added file dialog to graph editor. 2025-03-11 23:02:11 +01:00
Martin Felis 6d9a6fca56 Added https://github.com/btzy/nativefiledialog-extended at revision a1a401062819beb8c3da84518ab1fe7de88632db 2025-03-11 22:58:39 +01:00
Martin Felis 9298e5ad0e Fixed memory leak in editor. 2025-03-11 22:16:03 +01:00
Martin Felis a1c4630ee7 Nodes can now be deleted in the blend tree editor. 2025-03-02 19:30:26 +01:00
Martin Felis acbe3a4ed5 Fixed adding links to output node of blend tree. 2025-03-02 12:56:47 +01:00
Martin Felis 25776f2b2d Add a suffix when creating blend tree input or output sockets. 2025-03-02 12:49:45 +01:00
Martin Felis a2e8de0b70 Added NodeConnectionDebug tool. 2025-03-02 12:48:39 +01:00
Martin Felis 55bcd9cd99 BlendTree node names are now always unique. 2025-03-02 12:46:26 +01:00
Martin Felis feb5f57a86 Refactored construction of AnimNodeResources and AnimGraphResources by using factory methods more consistently. 2025-03-02 12:45:55 +01:00
84 changed files with 10585 additions and 2063 deletions
+1 -2
View File
@@ -9,5 +9,4 @@ BinPackParameters: 'false'
BreakBeforeBinaryOperators: NonAssignment BreakBeforeBinaryOperators: NonAssignment
ExperimentalAutoDetectBinPacking: 'false' ExperimentalAutoDetectBinPacking: 'false'
ReflowComments: 'false' ReflowComments: 'false'
DerivePointerAlignment: false
...
@@ -929,7 +929,8 @@ struct Example:
{ {
auto cursorTopLeft = ImGui::GetCursorScreenPos(); auto cursorTopLeft = ImGui::GetCursorScreenPos();
util::BlueprintNodeBuilder builder(m_HeaderBackground, GetTextureWidth(m_HeaderBackground), GetTextureHeight(m_HeaderBackground)); // util::BlueprintNodeBuilder builder(m_HeaderBackground, GetTextureWidth(m_HeaderBackground), GetTextureHeight(m_HeaderBackground));
util::BlueprintNodeBuilder builder;
for (auto& node : m_Nodes) for (auto& node : m_Nodes)
{ {
@@ -11,7 +11,7 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
# include <imgui_node_editor.h> # include "3rdparty/imgui-node-editor/imgui_node_editor.h"
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+8 -5
View File
@@ -1256,6 +1256,9 @@ void ed::EditorContext::End()
auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge() auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge()
//auto& editorStyle = GetStyle(); //auto& editorStyle = GetStyle();
// martin.felis, 2024-05-01, Start: expose Hot Node
m_HotNode = control.HotNode ? control.HotNode->m_ID : 0;
// martin.felis, 2024-05-01, End: expose Hot Node
m_HoveredNode = control.HotNode && m_CurrentAction == nullptr ? control.HotNode->m_ID : 0; m_HoveredNode = control.HotNode && m_CurrentAction == nullptr ? control.HotNode->m_ID : 0;
m_HoveredPin = control.HotPin && m_CurrentAction == nullptr ? control.HotPin->m_ID : 0; m_HoveredPin = control.HotPin && m_CurrentAction == nullptr ? control.HotPin->m_ID : 0;
m_HoveredLink = control.HotLink && m_CurrentAction == nullptr ? control.HotLink->m_ID : 0; m_HoveredLink = control.HotLink && m_CurrentAction == nullptr ? control.HotLink->m_ID : 0;
@@ -4391,15 +4394,15 @@ ed::EditorAction::AcceptResult ed::ShortcutAction::Accept(const Control& control
Action candidateAction = None; Action candidateAction = None;
auto& io = ImGui::GetIO(); auto& io = ImGui::GetIO();
if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X))) if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_X))
candidateAction = Cut; candidateAction = Cut;
if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_C))
candidateAction = Copy; candidateAction = Copy;
if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V))) if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_V))
candidateAction = Paste; candidateAction = Paste;
if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(GetKeyIndexForD())) if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(GetKeyIndexForD()))
candidateAction = Duplicate; candidateAction = Duplicate;
if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space))) if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_Space))
candidateAction = CreateNode; candidateAction = CreateNode;
if (candidateAction != None) if (candidateAction != None)
@@ -4953,7 +4956,7 @@ ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept(const Control& cont
return False; return False;
auto& io = ImGui::GetIO(); auto& io = ImGui::GetIO();
if (Editor->CanAcceptUserInput() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)) && Editor->AreShortcutsEnabled()) if (Editor->CanAcceptUserInput() && ImGui::IsKeyPressed(ImGuiKey_Delete) && Editor->AreShortcutsEnabled())
{ {
auto& selection = Editor->GetSelectedObjects(); auto& selection = Editor->GetSelectedObjects();
if (!selection.empty()) if (!selection.empty())
+3
View File
@@ -409,6 +409,9 @@ IMGUI_NODE_EDITOR_API void EndShortcut();
IMGUI_NODE_EDITOR_API float GetCurrentZoom(); IMGUI_NODE_EDITOR_API float GetCurrentZoom();
// martin.felis, 2024-05-01, Start: expose Hot Node
IMGUI_NODE_EDITOR_API NodeId GetHotNode();
// martin.felis, 2024-05-01, Stop: expose Hot Node
IMGUI_NODE_EDITOR_API NodeId GetHoveredNode(); IMGUI_NODE_EDITOR_API NodeId GetHoveredNode();
IMGUI_NODE_EDITOR_API PinId GetHoveredPin(); IMGUI_NODE_EDITOR_API PinId GetHoveredPin();
IMGUI_NODE_EDITOR_API LinkId GetHoveredLink(); IMGUI_NODE_EDITOR_API LinkId GetHoveredLink();
+7
View File
@@ -667,6 +667,13 @@ float ax::NodeEditor::GetCurrentZoom()
return s_Editor->GetView().InvScale; return s_Editor->GetView().InvScale;
} }
// martin.felis, 2024-05-01, Start: expose Hot Node
ax::NodeEditor::NodeId ax::NodeEditor::GetHotNode()
{
return s_Editor->GetHotNode();
}
// martin.felis, 2024-05-01, End: expose Hot Node
ax::NodeEditor::NodeId ax::NodeEditor::GetHoveredNode() ax::NodeEditor::NodeId ax::NodeEditor::GetHoveredNode()
{ {
return s_Editor->GetHoveredNode(); return s_Editor->GetHoveredNode();
@@ -1447,6 +1447,9 @@ struct EditorContext
void EnableShortcuts(bool enable); void EnableShortcuts(bool enable);
bool AreShortcutsEnabled(); bool AreShortcutsEnabled();
// martin.felis, 2024-05-01, Start: expose Hot Node
NodeId GetHotNode() const { return m_HotNode; }
// martin.felis, 2024-05-01, End: expose Hot Node
NodeId GetHoveredNode() const { return m_HoveredNode; } NodeId GetHoveredNode() const { return m_HoveredNode; }
PinId GetHoveredPin() const { return m_HoveredPin; } PinId GetHoveredPin() const { return m_HoveredPin; }
LinkId GetHoveredLink() const { return m_HoveredLink; } LinkId GetHoveredLink() const { return m_HoveredLink; }
@@ -1528,6 +1531,9 @@ private:
vector<AnimationController*> m_AnimationControllers; vector<AnimationController*> m_AnimationControllers;
FlowAnimationController m_FlowAnimationController; FlowAnimationController m_FlowAnimationController;
// martin.felis, 2024-05-01, Start: expose Hot Node
NodeId m_HotNode;
// martin.felis, 2024-05-01, End: expose Hot Node
NodeId m_HoveredNode; NodeId m_HoveredNode;
PinId m_HoveredPin; PinId m_HoveredPin;
LinkId m_HoveredLink; LinkId m_HoveredLink;
+11
View File
@@ -0,0 +1,11 @@
---
BasedOnStyle: Chromium
IndentWidth: 4
BinPackArguments: false
ColumnLimit: 100
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLoopsOnASingleLine: true
---
Language: Cpp
---
Language: ObjC
@@ -0,0 +1,7 @@
# VS CMake default output
/.vs/
/out/
/CMakeSettings.json
# Mac OS X rubbish
.DS_Store
+52
View File
@@ -0,0 +1,52 @@
cmake_minimum_required(VERSION 3.10)
project(nativefiledialog-extended VERSION 1.2.1)
set(nfd_ROOT_PROJECT OFF)
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(nfd_ROOT_PROJECT ON)
endif ()
option(BUILD_SHARED_LIBS "Build a shared library instead of static" OFF)
option(NFD_BUILD_TESTS "Build tests for nfd" ${nfd_ROOT_PROJECT})
option(NFD_BUILD_SDL2_TESTS "Build SDL2 tests for nfd" OFF)
option(NFD_INSTALL "Generate install target for nfd" ${nfd_ROOT_PROJECT})
set(nfd_PLATFORM Undefined)
if(WIN32)
set(nfd_PLATFORM PLATFORM_WIN32)
elseif(APPLE)
set(nfd_PLATFORM PLATFORM_MACOS)
elseif(UNIX AND NOT APPLE)
set(nfd_PLATFORM PLATFORM_LINUX)
endif()
message("nfd Platform: ${nfd_PLATFORM}")
set(nfd_COMPILER Undefined)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")
# This is clang-cl, which has different compiler options
set(nfd_COMPILER COMPILER_CLANGCL)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(nfd_COMPILER COMPILER_MSVC)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(nfd_COMPILER COMPILER_GNU)
endif()
message("nfd Compiler: ${nfd_COMPILER}")
# Use latest C++ by default (should be the best one), but let user override it
if(NOT DEFINED CMAKE_CXX_STANDARD)
if(CMAKE_VERSION VERSION_LESS "3.12")
set (CMAKE_CXX_STANDARD 17)
elseif(CMAKE_VERSION VERSION_LESS "3.20")
set (CMAKE_CXX_STANDARD 20)
else()
set (CMAKE_CXX_STANDARD 23)
endif()
endif()
add_subdirectory(src)
if(${NFD_BUILD_TESTS} OR ${NFD_BUILD_SDL2_TESTS})
add_subdirectory(test)
endif()
+16
View File
@@ -0,0 +1,16 @@
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
+395
View File
@@ -0,0 +1,395 @@
# Native File Dialog Extended
![GitHub Actions](https://github.com/btzy/nativefiledialog-extended/workflows/build/badge.svg)
A small C library that portably invokes native file open, folder select and file save dialogs. Write dialog code once and have it pop up native dialogs on all supported platforms. Avoid linking large dependencies like wxWidgets and Qt.
This library is based on Michael Labbe's Native File Dialog ([mlabbe/nativefiledialog](https://github.com/mlabbe/nativefiledialog)).
Features:
- Lean C API, static library &mdash; no C++/ObjC runtime needed
- Supports Windows (MSVC, MinGW, Clang), macOS (Clang), and Linux (GTK, portal) (GCC, Clang)
- Zlib licensed
- Friendly names for filters (e.g. `C/C++ Source files (*.c;*.cpp)` instead of `(*.c;*.cpp)`) on platforms that support it
- Automatically append file extension on platforms where users expect it
- Support for setting a default folder path
- Support for setting a default file name (e.g. `Untitled.c`)
- Consistent UTF-8 support on all platforms
- Native character set (UTF-16 `wchar_t`) support on Windows
- Initialization and de-initialization of platform library (e.g. COM (Windows) / GTK (Linux GTK) / D-Bus (Linux portal)) decoupled from dialog functions, so applications can choose when to initialize/de-initialize
- Multiple selection support (for file open and folder select dialogs)
- Support for Vista's modern `IFileDialog` on Windows
- No third party dependencies
- Modern CMake build system
- Works alongside [SDL2](http://www.libsdl.org) on all platforms
- Optional C++ wrapper with `unique_ptr` auto-freeing semantics and optional parameters, for those using this library from C++
**Comparison with original Native File Dialog:**
The friendly names feature is the primary reason for breaking API compatibility with Michael Labbe's library (and hence this library probably will never be merged with it). There are also a number of tweaks that cause observable differences in this library.
Features added in Native File Dialog Extended:
- Friendly names for filters
- Automatically appending file extensions
- Support for setting a default file name
- Native character set (UTF-16 `wchar_t`) support on Windows
- xdg-desktop-portal support on Linux that opens the "native" file chooser (see "Usage" section below)
- Multiple folder selection support
- Initialization and de-initialization of platform library decoupled from file dialog functions
- Modern CMake build system
- Optional C++ wrapper with `unique_ptr` auto-freeing semantics and optional parameters
There is also significant code refractoring, especially for the Windows implementation.
The [wiki](https://github.com/btzy/nativefiledialog-extended/wiki) keeps track of known language bindings and known popular projects that depend on this library.
# Basic Usage
```C
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
NFD_Init();
nfdu8char_t *outPath;
nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } };
nfdopendialogu8args_t args = {0};
args.filterList = filters;
args.filterCount = 2;
nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);
if (result == NFD_OKAY)
{
puts("Success!");
puts(outPath);
NFD_FreePathU8(outPath);
}
else if (result == NFD_CANCEL)
{
puts("User pressed cancel.");
}
else
{
printf("Error: %s\n", NFD_GetError());
}
NFD_Quit();
return 0;
}
```
The `U8`/`u8` in NFDe refer to the API for UTF-8 characters (`char`), which most consumers probably want. An `N`/`n` version is also available, which uses the native character type (`wchar_t` on Windows and `char` on other platforms).
For the full list of arguments that you can set on the `args` struct, see the "All Options" section below.
If you are using a platform abstraction framework such as SDL or GLFW, also see the "Usage with a Platform Abstraction Framework" section below.
# Screenshots #
![Windows 10](screens/open_win10.png?raw=true#gh-light-mode-only)
![Windows 10](screens/open_win10_dark.png?raw=true#gh-dark-mode-only)
![macOS 10.13](screens/open_macos_11.0.png?raw=true#gh-light-mode-only)
![macOS 10.13](screens/open_macos_11.0_dark.png?raw=true#gh-dark-mode-only)
![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true#gh-light-mode-only)
![GTK3 on Ubuntu 20.04](screens/open_gtk3_dark.png?raw=true#gh-dark-mode-only)
# Building
## CMake Projects
If your project uses CMake,
simply add the following lines to your CMakeLists.txt:
```
add_subdirectory(path/to/nativefiledialog-extended)
target_link_libraries(MyProgram PRIVATE nfd)
```
Make sure that you also have the needed [dependencies](#dependencies).
When included as a subproject, sample programs are not built and the install target is disabled by default.
Add `-DNFD_BUILD_TESTS=ON` to build sample programs and `-DNFD_INSTALL=ON` to enable the install target.
## Standalone Library
If you want to build the standalone static library,
execute the following commands (starting from the project root directory):
For GCC and Clang:
```
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
```
For MSVC:
```
mkdir build
cd build
cmake ..
cmake --build . --config Release
```
The above commands will make a `build` directory,
and build the project (in release mode) there.
If you are developing NFDe, you may want to do `-DCMAKE_BUILD_TYPE=Debug`/`--config Debug`
to build a debug version of the library instead.
When building as a standalone library, sample programs are built and the install target is enabled by default.
Add `-DNFD_BUILD_TESTS=OFF` to disable building sample programs and `-DNFD_INSTALL=OFF` to disable the install target.
On Linux, if you want to use the Flatpak desktop portal instead of GTK, add `-DNFD_PORTAL=ON`. (Otherwise, GTK will be used.) See the "Usage" section below for more information.
See the [CI build file](.github/workflows/cmake.yml) for some example build commands.
### Visual Studio on Windows
Recent versions of Visual Studio have CMake support built into the IDE.
You should be able to "Open Folder" in the project root directory,
and Visual Studio will recognize and configure the project appropriately.
From there, you will be able to set configurations for Debug vs Release,
and for x86 vs x64.
For more information, see [the Microsoft Docs page]([https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=vs-2019](https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=vs-2019)).
This has been tested to work on Visual Studio 2019,
and it probably works on Visual Studio 2017 too.
### Compiling Your Programs
1. Add `src/include` to your include search path.
2. Add `nfd.lib` or `nfd_d.lib` to the list of static libraries to link against (for release or debug, respectively).
3. Add `build/<debug|release>/<arch>` to the library search path.
## Dependencies
### Linux
#### GTK (default)
Make sure `libgtk-3-dev` is installed on your system.
#### Portal
Make sure `libdbus-1-dev` is installed on your system.
### macOS
On macOS, add `AppKit` and `UniformTypeIdentifiers` to the list of frameworks.
### Windows
On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib`, `uuid.lib`, and `shell32.lib`.
# Usage
## All Options
To open a dialog, you set options on a struct and then pass that struct to an NFDe function, e.g.:
```C
nfdopendialogu8args_t args = {0};
args.filterList = filters;
args.filterCount = 2;
nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);
```
All options are optional and may be set individually (zero initialization sets all options to reasonable defaults), except for `filterList` and `filterCount` which must be either both set or both left unset.
**Future versions of NFDe may add additional options to the end of the arguments struct without bumping the major version number, so to ensure backward API compatibility, you should not assume that the struct has a specific length or number of fields.** You may assume that zero-initialization of the struct will continue to set all options to reasonable defaults, so assigning `{0}` to the struct is acceptable. For those building shared libraries of NFDe, backward ABI compatibility is ensured by an internal version index (`NFD_INTERFACE_VERSION`), which is expected to be transparent to consumers.
**OpenDialog**/**OpenDialogMultiple**:
```C
typedef struct {
const nfdu8filteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdu8char_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdopendialogu8args_t;
```
**SaveDialog**:
```C
typedef struct {
const nfdu8filteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdu8char_t* defaultPath;
const nfdu8char_t* defaultName;
nfdwindowhandle_t parentWindow;
} nfdsavedialogu8args_t;
```
**PickFolder**/**PickFolderMultiple**:
```C
typedef struct {
const nfdu8char_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdpickfolderu8args_t;
```
- `filterList` and `filterCount`: Set these to customize the file filter (it appears as a dropdown menu on Windows and Linux, but simply hides files on macOS). Set `filterList` to a pointer to the start of the array of filter items and `filterCount` to the number of filter items in that array. See the "File Filter Syntax" section below for details.
- `defaultPath`: Set this to the default folder that the dialog should open to (on Windows, if there is a recently used folder, it opens to that folder instead of the folder you pass, unless the `NFD_OVERRIDE_RECENT_WITH_DEFAULT` build option is set to ON).
- `defaultName`: (For SaveDialog only) Set this to the file name that should be pre-filled on the dialog.
- `parentWindow`: Set this to the native window handle of the parent of this dialog. See the "Usage with a Platform Abstraction Framework" section for details. It is also possible to pass a handle even if you do not use a platform abstraction framework.
## Examples
See the `test` directory for example code (both C and C++).
If you turned on the option to build the `test` directory (`-DNFD_BUILD_TESTS=ON`), then `build/bin` will contain the compiled test programs.
There is also an SDL2 example, which needs to be enabled separately with `-DNFD_BUILD_SDL2_TESTS=ON`. It requires SDL2 to be installed on your machine.
Compiled examples (including the SDL2 example) are also uploaded as artefacts to GitHub Actions, and may be downloaded from there.
## File Filter Syntax
Files can be filtered by file extension groups:
```C
nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } };
```
A file filter is a pair of strings comprising the friendly name and the specification (multiple file extensions are comma-separated).
A list of file filters can be passed as an argument when invoking the library.
A wildcard filter is always added to every dialog.
*Note: On macOS, the file dialogs do not have friendly names and there is no way to switch between filters, so the filter specifications are combined (e.g. "c,cpp,cc,h,hpp"). The filter specification is also never explicitly shown to the user. This is usual macOS behaviour and users expect it.*
*Note 2: You must ensure that the specification string is non-empty and that every file extension has at least one character. Otherwise, bad things might ensue (i.e. undefined behaviour).*
*Note 3: On Linux, the file extension is appended (if missing) when the user presses down the "Save" button. The appended file extension will remain visible to the user, even if an overwrite prompt is shown and the user then presses "Cancel".*
*Note 4: On Windows, the default folder parameter is only used if there is no recently used folder available, unless the `NFD_OVERRIDE_RECENT_WITH_DEFAULT` build option is set to ON. Otherwise, the default folder will be the folder that was last used. Internally, the Windows implementation calls [IFileDialog::SetDefaultFolder(IShellItem)](https://docs.microsoft.com/en-us/windows/desktop/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setdefaultfolder). This is usual Windows behaviour and users expect it.*
*Note 5: Linux is designed for case-sensitive file filters, but this is perhaps not what most users expect. A simple hack is used to make filters case-insensitive. To get case-sensitive filtering, set the `NFD_CASE_SENSITIVE_FILTER` build option to ON.*
## Iterating Over PathSets
A file open dialog that supports multiple selection produces a PathSet, which is a thin abstraction over the platform-specific collection. There are two ways to iterate over a PathSet:
### Accessing by index
This method does array-like access on the PathSet, and is the easiest to use.
However, on certain platforms (Linux, and possibly Windows),
it takes O(N<sup>2</sup>) time in total to iterate the entire PathSet,
because the underlying platform-specific implementation uses a linked list.
See [test_opendialogmultiple.c](test/test_opendialogmultiple.c).
### Using an enumerator (experimental)
This method uses an enumerator object to iterate the paths in the PathSet.
It is guaranteed to take O(N) time in total to iterate the entire PathSet.
See [test_opendialogmultiple_enum.c](test/test_opendialogmultiple_enum.c).
This API is experimental, and subject to change.
## Customization Macros
You can define the following macros *before* including `nfd.h`/`nfd.hpp`:
- `NFD_NATIVE`: Define this before including `nfd.h` to make non-suffixed function names and typedefs (e.g. `NFD_OpenDialog`) aliases for the native functions (e.g. `NFD_OpenDialogN`) instead of aliases for the UTF-8 functions (e.g. `NFD_OpenDialogU8`). This macro does not affect the C++ wrapper `nfd.hpp`.
- `NFD_THROWS_EXCEPTIONS`: (C++ only) Define this before including `nfd.hpp` to make `NFD::Guard` construction throw `std::runtime_error` if `NFD_Init` fails. Otherwise, there is no way to detect failure in `NFD::Guard` construction.
Macros that might be defined by `nfd.h`:
- `NFD_DIFFERENT_NATIVE_FUNCTIONS`: Defined if the native and UTF-8 versions of functions are different (i.e. compiling for Windows); not defined otherwise. If `NFD_DIFFERENT_NATIVE_FUNCTIONS` is not defined, then the UTF-8 versions of functions are aliases for the native versions. This might be useful if you are writing a function that wants to provide overloads depending on whether the native functions and UTF-8 functions are the same. (Native is UTF-16 (`wchar_t`) for Windows and UTF-8 (`char`) for Mac/Linux.)
## Usage with a Platform Abstraction Framework
NFDe is known to work with SDL2 and GLFW, and should also work with other platform abstraction framworks. This section explains how to use NFDe properly with such frameworks.
### Parent window handle
The `parentWindow` argument allows the user to give the dialog a parent.
If using SDL2, include `<nfd_sdl2.h>` and call the following function to set the parent window handle:
```C
NFD_GetNativeWindowFromSDLWindow(sdlWindow /* SDL_Window* */, &args.parentWindow);
```
If using GLFW3, define the appropriate `GLFW_EXPOSE_NATIVE_*` macros described on the [GLFW native access page](https://www.glfw.org/docs/latest/group__native.html), and then include `<nfd_glfw3.h>` and call the following function to set the parent window handle:
```C
NFD_GetNativeWindowFromGLFWWindow(glfwWindow /* GLFWwindow* */, &args.parentWindow);
```
If you are using another platform abstraction framework, or not using any such framework, you can set `args.parentWindow` manually.
Win32 (Windows), Cocoa (macOS), and X11 (Linux) windows are supported. Passing a Wayland (Linux) window currently does nothing (i.e. the dialog acts as if it has no parent), but support is likely to be added in the future.
#### Why pass a parent window handle?
To make a window (in this case the file dialog) stay above another window, we need to declare the bottom window as the parent of the top window. This keeps the dialog window from disappearing behind the parent window if the user clicks on the parent window while the dialog is open. Keeping the dialog above the window that invoked it is the expected behaviour on all supported operating systems, and so passing the parent window handle is recommended if possible.
### Initialization order
You should initialize NFDe _after_ initializing the framework, and probably should deinitialize NFDe _before_ deinitializing the framework. This is because some frameworks expect to be initialized on a "clean slate", and they may configure the system in a different way from NFDe. `NFD_Init` is generally very careful not to disrupt the existing configuration unless necessary, and `NFD_Quit` restores the configuration back exactly to what it was before initialization.
An example with SDL2:
```
// Initialize SDL2 first
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
// display some error here
}
// Then initialize NFDe
if (NFD_Init() != NFD_OKAY) {
// display some error here
}
/*
Your main program goes here
*/
NFD_Quit(); // deinitialize NFDe first
SDL_Quit(); // Then deinitialize SDL2
```
## Using xdg-desktop-portal on Linux
On Linux, you can use the portal implementation instead of GTK, which will open the "native" file chooser selected by the OS or customized by the user. The user must have `xdg-desktop-portal` and a suitable backend installed (this comes pre-installed with most common desktop distros), otherwise `NFD_ERROR` will be returned.
To use the portal implementation, add `-DNFD_PORTAL=ON` to the build command.
*Note: The folder picker is only supported on org.freedesktop.portal.FileChooser interface version >= 3, which corresponds to xdg-desktop-portal version >= 1.7.1. `NFD_PickFolder()` will query the interface version at runtime, and return `NFD_ERROR` if the version is too low.
### What is a portal?
Unlike Windows and macOS, Linux does not have a file chooser baked into the operating system. Linux applications that want a file chooser usually link with a library that provides one (such as GTK, as in the Linux screenshot above). This is a mostly acceptable solution that many applications use, but may make the file chooser look foreign on non-GTK distros.
Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently three known backends with file chooser support: GTK, KDE, and LXQt; Gnome and Xapp backends depend on the GTK one for this functionality. The Xapp backend has been designed for Cinnamon, MATE, and XFCE. Other desktop environments do not seem to currently have a portal backend.
## Platform-specific Quirks
### macOS
- If the macOS deployment target is ≥ 11.0, the [allowedContentTypes](https://developer.apple.com/documentation/appkit/nssavepanel/3566857-allowedcontenttypes?language=objc) property of NSSavePanel is used instead of the deprecated [allowedFileTypes](https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes?language=objc) property for file filters. Thus, if you are filtering by a custom file extension specific to your application, you will need to define the data type in your `Info.plist` file as per the [Apple documentation](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app). (It is possible to force NFDe to use allowedFileTypes by adding `-DNFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE=OFF` to your CMake build command, but this is not recommended. If you need to support older macOS versions, you should be setting the correct deployment target instead.)
# Known Limitations #
- No support for Windows XP's legacy dialogs such as `GetOpenFileName`. (There are no plans to support this; you shouldn't be still using Windows XP anyway.)
- No Emscripten (WebAssembly) bindings. (This might get implemented if I decide to port Circuit Sandbox for the web, but I don't think there is any way to implement a web-based folder picker.)
- GTK dialogs don't set the existing window as parent, so if users click the existing window while the dialog is open then the dialog will go behind it. GTK writes a warning to stdout or stderr about this.
- This library is not compatible with the original Native File Dialog library. Things might break if you use both in the same project. (There are no plans to support this; you have to use one or the other.)
- This library does not explicitly dispatch calls to the UI thread. This may lead to crashes if you call functions from other threads when the platform does not support it (e.g. macOS). Users are generally expected to call NFDe from an appropriate UI thread (i.e. the thread performing the UI event loop).
# Reporting Bugs #
Please use the GitHub issue tracker to report bugs or to contribute to this repository. Feel free to submit bug reports of any kind.
# Credit #
Bernard Teo (me) and other contributors for everything that wasn't from Michael Labbe's [Native File Dialog](https://github.com/mlabbe/nativefiledialog).
[Michael Labbe](https://github.com/mlabbe) for his awesome Native File Dialog library, and the other contributors to that library.
Much of this README has also been copied from the README of original Native File Dialog repository.
## License ##
Everything in this repository is distributed under the ZLib license, as is the original Native File Dialog library.
## Support ##
I don't provide any paid support. [Michael Labbe](https://github.com/mlabbe) appears to provide paid support for his [library](https://github.com/mlabbe/nativefiledialog) at the time of writing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+149
View File
@@ -0,0 +1,149 @@
set(TARGET_NAME nfd)
set(PUBLIC_HEADER_FILES
include/nfd.h
include/nfd.hpp
include/nfd_sdl2.h
include/nfd_glfw3.h)
set(SOURCE_FILES ${PUBLIC_HEADER_FILES})
if(nfd_PLATFORM STREQUAL PLATFORM_WIN32)
list(APPEND SOURCE_FILES nfd_win.cpp)
endif()
if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
find_package(PkgConfig REQUIRED)
# for Linux, we support GTK3 and xdg-desktop-portal
option(NFD_PORTAL "Use xdg-desktop-portal instead of GTK" OFF)
if(NOT NFD_PORTAL)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
message("Using GTK version: ${GTK3_VERSION}")
list(APPEND SOURCE_FILES nfd_gtk.cpp)
else()
pkg_check_modules(DBUS REQUIRED dbus-1)
message("Using DBUS version: ${DBUS_VERSION}")
list(APPEND SOURCE_FILES nfd_portal.cpp)
endif()
endif()
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)
# For setting the filter list, macOS introduced allowedContentTypes in version 11.0 and deprecated allowedFileTypes in 12.0.
# By default (set to ON), NFDe will use allowedContentTypes when targeting macOS >= 11.0.
# Set this option to OFF to always use allowedFileTypes regardless of the target macOS version.
# This is mainly needed for applications that are built on macOS >= 11.0 but should be able to run on lower versions
# and should not be used otherwise.
option(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE "Use allowedContentTypes for filter lists on macOS >= 11.0" ON)
find_library(APPKIT_LIBRARY AppKit)
if(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles(
"
#include <Availability.h>
#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0
static_assert(false);
#endif
int main() { return 0; }
"
NFD_USE_ALLOWEDCONTENTTYPES
)
if(NFD_USE_ALLOWEDCONTENTTYPES)
find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers)
if(NOT UNIFORMTYPEIDENTIFIERS_LIBRARY)
message(FATAL_ERROR "UniformTypeIdentifiers framework is not available even though we are targeting macOS >= 11.0")
endif()
endif()
endif()
list(APPEND SOURCE_FILES nfd_cocoa.m)
endif()
# Define the library
add_library(${TARGET_NAME} ${SOURCE_FILES})
# Define alias library to fail early in dependent projects
add_library(${TARGET_NAME}::${TARGET_NAME} ALIAS ${TARGET_NAME})
if (BUILD_SHARED_LIBS)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_EXPORT INTERFACE NFD_SHARED)
endif ()
# Allow includes from include/
target_include_directories(${TARGET_NAME}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
if(NOT NFD_PORTAL)
target_include_directories(${TARGET_NAME}
PRIVATE ${GTK3_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME}
PRIVATE ${GTK3_LINK_LIBRARIES})
else()
target_include_directories(${TARGET_NAME}
PRIVATE ${DBUS_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME}
PRIVATE ${DBUS_LINK_LIBRARIES})
target_compile_definitions(${TARGET_NAME}
PUBLIC NFD_PORTAL)
endif()
option(NFD_APPEND_EXTENSION "Automatically append file extension to an extensionless selection in SaveDialog()" OFF)
if(NFD_APPEND_EXTENSION)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION)
endif()
option(NFD_CASE_SENSITIVE_FILTER "Make filters case sensitive" OFF)
if(NFD_CASE_SENSITIVE_FILTER)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_CASE_SENSITIVE_FILTER)
endif()
endif()
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)
if(NFD_USE_ALLOWEDCONTENTTYPES)
target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY} ${UNIFORMTYPEIDENTIFIERS_LIBRARY})
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=1)
else()
target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY})
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=0)
endif()
endif()
if(nfd_COMPILER STREQUAL COMPILER_MSVC)
string(REPLACE "/EHsc" "/EHs-c-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
string(REPLACE "/GR" "/GR-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY STATIC_LIBRARY_OPTIONS /NODEFAULTLIB)
endif()
if(nfd_COMPILER STREQUAL COMPILER_CLANGCL)
string(REPLACE "/EHsc" "/EHs-c-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
string(REPLACE "/GR" "/GR-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
endif()
if(nfd_COMPILER STREQUAL COMPILER_GNU)
target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions -fno-rtti)
endif()
set_target_properties(${TARGET_NAME} PROPERTIES
PUBLIC_HEADER "${PUBLIC_HEADER_FILES}"
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR})
if (NFD_INSTALL)
include(GNUInstallDirs)
install(TARGETS ${TARGET_NAME} EXPORT ${TARGET_NAME}-export
LIBRARY DESTINATION ${LIB_INSTALL_DIR} ARCHIVE DESTINATION ${LIB_INSTALL_DIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT ${TARGET_NAME}-export
DESTINATION lib/cmake/${TARGET_NAME}
NAMESPACE ${TARGET_NAME}::
FILE ${TARGET_NAME}-config.cmake
)
endif()
option(NFD_OVERRIDE_RECENT_WITH_DEFAULT "Use defaultPath instead of recent folder on Windows" OFF)
if (NFD_OVERRIDE_RECENT_WITH_DEFAULT)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_OVERRIDE_RECENT_WITH_DEFAULT)
endif()
+564
View File
@@ -0,0 +1,564 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo, Michael Labbe
This header contains the functions that can be called by user code.
*/
#ifndef _NFD_H
#define _NFD_H
#if defined(_WIN32)
#if defined(NFD_EXPORT)
#define NFD_API __declspec(dllexport)
#elif defined(NFD_SHARED)
#define NFD_API __declspec(dllimport)
#endif
#else
#if defined(NFD_EXPORT) || defined(NFD_SHARED)
#if defined(__GNUC__) || defined(__clang__)
#define NFD_API __attribute__((visibility("default")))
#endif
#endif
#endif
#ifndef NFD_API
#define NFD_API
#endif
#ifdef __cplusplus
extern "C" {
#define NFD_INLINE inline
#else
#define NFD_INLINE static inline
#endif // __cplusplus
#include <stddef.h>
typedef char nfdu8char_t;
#ifdef _WIN32
/** @typedef UTF-16 character */
typedef wchar_t nfdnchar_t;
#else
/** @typedef UTF-8 character */
typedef nfdu8char_t nfdnchar_t;
#endif // _WIN32
/** @typedef Opaque data structure -- see NFD_PathSet_* */
typedef void nfdpathset_t;
#ifndef NFD_PORTAL
typedef struct {
void* ptr;
} nfdpathsetenum_t;
#else
typedef struct {
void* d1;
void* d2;
unsigned int d3;
int d4;
int d5;
int d6;
int d7;
int d8;
int d9;
int d10;
int d11;
int p1;
void* p2;
void* p3;
} nfdpathsetenum_t;
#endif
typedef unsigned int nfdfiltersize_t;
typedef enum {
NFD_ERROR, /**< Programmatic error */
NFD_OKAY, /**< User pressed okay, or successful return */
NFD_CANCEL /**< User pressed cancel */
} nfdresult_t;
/** @typedef UTF-8 Filter Item */
typedef struct {
const nfdu8char_t* name;
const nfdu8char_t* spec;
} nfdu8filteritem_t;
#ifdef _WIN32
/** @typedef UTF-16 Filter Item */
typedef struct {
const nfdnchar_t* name;
const nfdnchar_t* spec;
} nfdnfilteritem_t;
#else
/** @typedef UTF-8 Filter Item */
typedef nfdu8filteritem_t nfdnfilteritem_t;
#endif // _WIN32
// The native window handle type.
enum {
NFD_WINDOW_HANDLE_TYPE_UNSET = 0,
// Windows: handle is HWND (the Windows API typedefs this to void*)
NFD_WINDOW_HANDLE_TYPE_WINDOWS = 1,
// Cocoa: handle is NSWindow*
NFD_WINDOW_HANDLE_TYPE_COCOA = 2,
// X11: handle is Window
NFD_WINDOW_HANDLE_TYPE_X11 = 3,
// Wayland support will be implemented separately in the future
};
// The native window handle. If using a platform abstraction framework (e.g. SDL2), this should be
// obtained using the corresponding NFD glue header (e.g. nfd_sdl2.h).
typedef struct {
size_t type; // this is one of the values of the enum above
void* handle;
} nfdwindowhandle_t;
typedef size_t nfdversion_t;
typedef struct {
const nfdu8filteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdu8char_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdopendialogu8args_t;
#ifdef _WIN32
typedef struct {
const nfdnfilteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdnchar_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdopendialognargs_t;
#else
typedef nfdopendialogu8args_t nfdopendialognargs_t;
#endif // _WIN32
typedef struct {
const nfdu8filteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdu8char_t* defaultPath;
const nfdu8char_t* defaultName;
nfdwindowhandle_t parentWindow;
} nfdsavedialogu8args_t;
#ifdef _WIN32
typedef struct {
const nfdnfilteritem_t* filterList;
nfdfiltersize_t filterCount;
const nfdnchar_t* defaultPath;
const nfdnchar_t* defaultName;
nfdwindowhandle_t parentWindow;
} nfdsavedialognargs_t;
#else
typedef nfdsavedialogu8args_t nfdsavedialognargs_t;
#endif // _WIN32
typedef struct {
const nfdu8char_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdpickfolderu8args_t;
#ifdef _WIN32
typedef struct {
const nfdnchar_t* defaultPath;
nfdwindowhandle_t parentWindow;
} nfdpickfoldernargs_t;
#else
typedef nfdpickfolderu8args_t nfdpickfoldernargs_t;
#endif // _WIN32
// This is a unique identifier tagged to all the NFD_*With() function calls, for backward
// compatibility purposes. There is usually no need to use this directly, unless you want to use
// NFD differently depending on the version you're building with.
#define NFD_INTERFACE_VERSION 1
/** Free a file path that was returned by the dialogs.
*
* Note: use NFD_PathSet_FreePathN() to free path from pathset instead of this function. */
NFD_API void NFD_FreePathN(nfdnchar_t* filePath);
/** Free a file path that was returned by the dialogs.
*
* Note: use NFD_PathSet_FreePathU8() to free path from pathset instead of this function. */
NFD_API void NFD_FreePathU8(nfdu8char_t* filePath);
/** Initialize NFD. Call this for every thread that might use NFD, before calling any other NFD
* functions on that thread. */
NFD_API nfdresult_t NFD_Init(void);
/** Call this to de-initialize NFD, if NFD_Init returned NFD_OKAY. */
NFD_API void NFD_Quit(void);
/** Single file open dialog
*
* It's the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY.
* @param[out] outPath
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath);
/** Single file open dialog
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY.
* @param[out] outPath
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath);
/** This function is a library implementation detail. Please use NFD_OpenDialogN_With() instead. */
NFD_API nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdopendialognargs_t* args);
/** Single file open dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function
* returns NFD_OKAY. See documentation of nfdopendialognargs_t for details. */
NFD_INLINE nfdresult_t NFD_OpenDialogN_With(nfdnchar_t** outPath,
const nfdopendialognargs_t* args) {
return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** This function is a library implementation detail. Please use NFD_OpenDialogU8_With() instead.
*/
NFD_API nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdopendialogu8args_t* args);
/** Single file open dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY. See documentation of nfdopendialogu8args_t for details. */
NFD_INLINE nfdresult_t NFD_OpenDialogU8_With(nfdu8char_t** outPath,
const nfdopendialogu8args_t* args) {
return NFD_OpenDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** Multiple file open dialog
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function
* returns NFD_OKAY.
* @param[out] outPaths
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath);
/** Multiple file open dialog
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function
* returns NFD_OKAY.
* @param[out] outPaths
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath);
/** This function is a library implementation detail. Please use NFD_OpenDialogMultipleN_With()
* instead. */
NFD_API nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialognargs_t* args);
/** Multiple file open dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function
* returns NFD_OKAY. See documentation of nfdopendialognargs_t for details. */
NFD_INLINE nfdresult_t NFD_OpenDialogMultipleN_With(const nfdpathset_t** outPaths,
const nfdopendialognargs_t* args) {
return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, args);
}
/** This function is a library implementation detail. Please use NFD_OpenDialogU8_With() instead.
*/
NFD_API nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialogu8args_t* args);
/** Multiple file open dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function
* returns NFD_OKAY. See documentation of nfdopendialogu8args_t for details. */
NFD_INLINE nfdresult_t NFD_OpenDialogMultipleU8_With(const nfdpathset_t** outPaths,
const nfdopendialogu8args_t* args) {
return NFD_OpenDialogMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, args);
}
/** Save dialog
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY.
* @param[out] outPath
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName);
/** Save dialog
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY.
* @param[out] outPath
* @param filterCount If zero, filterList is ignored (you can use null).
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath,
const nfdu8char_t* defaultName);
/** This function is a library implementation detail. Please use NFD_SaveDialogN_With() instead. */
NFD_API nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdsavedialognargs_t* args);
/** Single file save dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function
* returns NFD_OKAY. See documentation of nfdsavedialognargs_t for details. */
NFD_INLINE nfdresult_t NFD_SaveDialogN_With(nfdnchar_t** outPath,
const nfdsavedialognargs_t* args) {
return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** This function is a library implementation detail. Please use NFD_SaveDialogU8_With() instead.
*/
NFD_API nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdsavedialogu8args_t* args);
/** Single file save dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY. See documentation of nfdsavedialogu8args_t for details. */
NFD_INLINE nfdresult_t NFD_SaveDialogU8_With(nfdu8char_t** outPath,
const nfdsavedialogu8args_t* args) {
return NFD_SaveDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** Select single folder dialog
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY.
* @param[out] outPath
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath);
/** Select single folder dialog
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY.
* @param[out] outPath
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath);
/** This function is a library implementation detail. Please use NFD_PickFolderN_With() instead. */
NFD_API nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdpickfoldernargs_t* args);
/** Select single folder dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function
* returns NFD_OKAY. See documentation of nfdpickfoldernargs_t for details. */
NFD_INLINE nfdresult_t NFD_PickFolderN_With(nfdnchar_t** outPath,
const nfdpickfoldernargs_t* args) {
return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** This function is a library implementation detail. Please use NFD_PickFolderU8_With() instead.
*/
NFD_API nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdpickfolderu8args_t* args);
/** Select single folder dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function
* returns NFD_OKAY. See documentation of nfdpickfolderu8args_t for details. */
NFD_INLINE nfdresult_t NFD_PickFolderU8_With(nfdu8char_t** outPath,
const nfdpickfolderu8args_t* args) {
return NFD_PickFolderU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args);
}
/** Select multiple folder dialog
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function
* returns NFD_OKAY.
* @param[out] outPaths
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths,
const nfdnchar_t* defaultPath);
/** Select multiple folder dialog
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function
* returns NFD_OKAY.
* @param[out] outPaths
* @param defaultPath If null, the operating system will decide. */
NFD_API nfdresult_t NFD_PickFolderMultipleU8(const nfdpathset_t** outPaths,
const nfdu8char_t* defaultPath);
/** This function is a library implementation detail. Please use NFD_PickFolderMultipleN_With()
* instead. */
NFD_API nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfoldernargs_t* args);
/** Select multiple folder dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function
* returns NFD_OKAY. See documentation of nfdopendialogargs_t for details. */
NFD_INLINE nfdresult_t NFD_PickFolderMultipleN_With(const nfdpathset_t** outPaths,
const nfdpickfoldernargs_t* args) {
return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, args);
}
/** This function is a library implementation detail. Please use NFD_PickFolderMultipleU8_With()
* instead.
*/
NFD_API nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfolderu8args_t* args);
/** Select multiple folder dialog, with additional parameters.
*
* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function
* returns NFD_OKAY. See documentation of nfdpickfolderargs_t for details. */
NFD_INLINE nfdresult_t NFD_PickFolderMultipleU8_With(const nfdpathset_t** outPaths,
const nfdpickfolderu8args_t* args) {
return NFD_PickFolderMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, args);
}
/** Get the last error
*
* This is set when a function returns NFD_ERROR.
* The memory is owned by NFD and should not be freed by user code.
* This is *always* ASCII printable characters, so it can be interpreted as UTF-8 without any
* conversion.
* @return The last error that was set, or null if there is no error. */
NFD_API const char* NFD_GetError(void);
/** Clear the error. */
NFD_API void NFD_ClearError(void);
/* path set operations */
#ifdef _WIN32
typedef unsigned long nfdpathsetsize_t;
#elif __APPLE__
typedef unsigned long nfdpathsetsize_t;
#else
typedef unsigned int nfdpathsetsize_t;
#endif // _WIN32, __APPLE__
/** Get the number of entries stored in pathSet.
*
* Note: some paths might be invalid (NFD_ERROR will be returned by NFD_PathSet_GetPath),
* so we might not actually have this number of usable paths. */
NFD_API nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count);
/** Get the UTF-8 path at offset index.
*
* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathN() if this function
* returns NFD_OKAY. */
NFD_API nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdnchar_t** outPath);
/** Get the native path at offset index.
*
* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathU8() if this
* function returns NFD_OKAY. */
NFD_API nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdu8char_t** outPath);
/** Free the path gotten by NFD_PathSet_GetPathN(). */
NFD_API void NFD_PathSet_FreePathN(const nfdnchar_t* filePath);
/** Free the path gotten by NFD_PathSet_GetPathU8(). */
NFD_API void NFD_PathSet_FreePathU8(const nfdu8char_t* filePath);
/** Gets an enumerator of the path set.
*
* It is the caller's responsibility to free `enumerator` via NFD_PathSet_FreeEnum()
* if this function returns NFD_OKAY, and it should be freed before freeing the pathset. */
NFD_API nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet,
nfdpathsetenum_t* outEnumerator);
/** Frees an enumerator of the path set. */
NFD_API void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator);
/** Gets the next item from the path set enumerator.
*
* If there are no more items, then *outPaths will be set to null.
* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathN()
* if this function returns NFD_OKAY and `*outPath` is not null. */
NFD_API nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath);
/** Gets the next item from the path set enumerator.
*
* If there are no more items, then *outPaths will be set to null.
* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathU8()
* if this function returns NFD_OKAY and `*outPath` is not null. */
NFD_API nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath);
/** Free the pathSet */
NFD_API void NFD_PathSet_Free(const nfdpathset_t* pathSet);
#ifdef _WIN32
/* say that the U8 versions of functions are not just __attribute__((alias(""))) to the native
* versions */
#define NFD_DIFFERENT_NATIVE_FUNCTIONS
#endif // _WIN32
#ifdef NFD_NATIVE
typedef nfdnchar_t nfdchar_t;
typedef nfdnfilteritem_t nfdfilteritem_t;
#define NFD_FreePath NFD_FreePathN
#define NFD_OpenDialog NFD_OpenDialogN
#define NFD_OpenDialogMultiple NFD_OpenDialogMultipleN
#define NFD_SaveDialog NFD_SaveDialogN
#define NFD_PickFolder NFD_PickFolderN
#define NFD_PickFolderMultiple NFD_PickFolderMultipleN
#define NFD_PathSet_GetPath NFD_PathSet_GetPathN
#define NFD_PathSet_FreePath NFD_PathSet_FreePathN
#define NFD_PathSet_EnumNext NFD_PathSet_EnumNextN
#else
typedef nfdu8char_t nfdchar_t;
typedef nfdu8filteritem_t nfdfilteritem_t;
#define NFD_FreePath NFD_FreePathU8
#define NFD_OpenDialog NFD_OpenDialogU8
#define NFD_OpenDialogMultiple NFD_OpenDialogMultipleU8
#define NFD_SaveDialog NFD_SaveDialogU8
#define NFD_PickFolder NFD_PickFolderU8
#define NFD_PickFolderMultiple NFD_PickFolderMultipleU8
#define NFD_PathSet_GetPath NFD_PathSet_GetPathU8
#define NFD_PathSet_FreePath NFD_PathSet_FreePathU8
#define NFD_PathSet_EnumNext NFD_PathSet_EnumNextU8
#endif // NFD_NATIVE
#undef NFD_INLINE
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // _NFD_H
+375
View File
@@ -0,0 +1,375 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Author: Bernard Teo
This header is a thin C++ wrapper for nfd.h.
C++ projects can choose to use this header instead of nfd.h directly.
Refer to documentation on nfd.h for instructions on how to use these functions.
*/
#ifndef _NFD_HPP
#define _NFD_HPP
#include <nfd.h>
#include <cstddef> // for std::size_t
#include <memory> // for std::unique_ptr
#ifdef NFD_THROWS_EXCEPTIONS
#include <stdexcept>
#endif
namespace NFD {
inline nfdresult_t Init() noexcept {
return ::NFD_Init();
}
inline void Quit() noexcept {
::NFD_Quit();
}
inline void FreePath(nfdnchar_t* outPath) noexcept {
::NFD_FreePathN(outPath);
}
inline nfdresult_t OpenDialog(nfdnchar_t*& outPath,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdopendialognargs_t args{filterList, filterCount, defaultPath, parentWindow};
return ::NFD_OpenDialogN_With(&outPath, &args);
}
inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdopendialognargs_t args{filterList, filterCount, defaultPath, parentWindow};
return ::NFD_OpenDialogMultipleN_With(&outPaths, &args);
}
inline nfdresult_t SaveDialog(nfdnchar_t*& outPath,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
const nfdnchar_t* defaultName = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdsavedialognargs_t args{
filterList, filterCount, defaultPath, defaultName, parentWindow};
return ::NFD_SaveDialogN_With(&outPath, &args);
}
inline nfdresult_t PickFolder(nfdnchar_t*& outPath,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpickfoldernargs_t args{defaultPath, parentWindow};
return ::NFD_PickFolderN_With(&outPath, &args);
}
inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpickfoldernargs_t args{defaultPath, parentWindow};
return ::NFD_PickFolderMultipleN_With(&outPaths, &args);
}
inline const char* GetError() noexcept {
return ::NFD_GetError();
}
inline void ClearError() noexcept {
::NFD_ClearError();
}
namespace PathSet {
inline nfdresult_t Count(const nfdpathset_t* pathSet, nfdpathsetsize_t& count) noexcept {
return ::NFD_PathSet_GetCount(pathSet, &count);
}
inline nfdresult_t GetPath(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdnchar_t*& outPath) noexcept {
return ::NFD_PathSet_GetPathN(pathSet, index, &outPath);
}
inline void FreePath(nfdnchar_t* filePath) noexcept {
::NFD_PathSet_FreePathN(filePath);
}
inline void Free(const nfdpathset_t* pathSet) noexcept {
::NFD_PathSet_Free(pathSet);
}
} // namespace PathSet
#ifdef NFD_DIFFERENT_NATIVE_FUNCTIONS
/* we need the C++ bindings for the UTF-8 functions as well, because there are different functions
* for them */
inline void FreePath(nfdu8char_t* outPath) noexcept {
::NFD_FreePathU8(outPath);
}
inline nfdresult_t OpenDialog(nfdu8char_t*& outPath,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdopendialogu8args_t args{filterList, filterCount, defaultPath, parentWindow};
return ::NFD_OpenDialogU8_With(&outPath, &args);
}
inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdopendialogu8args_t args{filterList, filterCount, defaultPath, parentWindow};
return ::NFD_OpenDialogMultipleU8_With(&outPaths, &args);
}
inline nfdresult_t SaveDialog(nfdu8char_t*& outPath,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
const nfdu8char_t* defaultName = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdsavedialogu8args_t args{
filterList, filterCount, defaultPath, defaultName, parentWindow};
return ::NFD_SaveDialogU8_With(&outPath, &args);
}
inline nfdresult_t PickFolder(nfdu8char_t*& outPath,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpickfolderu8args_t args{defaultPath, parentWindow};
return ::NFD_PickFolderU8_With(&outPath, &args);
}
inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpickfolderu8args_t args{defaultPath, parentWindow};
return ::NFD_PickFolderMultipleU8_With(&outPaths, &args);
}
namespace PathSet {
inline nfdresult_t GetPath(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdu8char_t*& outPath) noexcept {
return ::NFD_PathSet_GetPathU8(pathSet, index, &outPath);
}
inline void FreePath(nfdu8char_t* filePath) noexcept {
::NFD_PathSet_FreePathU8(filePath);
}
} // namespace PathSet
#endif
// smart objects
class Guard {
public:
#ifndef NFD_THROWS_EXCEPTIONS
inline Guard() noexcept {
Init(); // always assume that initialization succeeds
}
#else
inline Guard() {
if (!Init()) {
throw std::runtime_error(GetError());
}
}
#endif
inline ~Guard() noexcept { Quit(); }
// Not allowed to copy or move this class
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
};
template <typename T>
struct PathDeleter {
inline void operator()(T* ptr) const noexcept { FreePath(ptr); }
};
typedef std::unique_ptr<nfdchar_t, PathDeleter<nfdchar_t>> UniquePath;
typedef std::unique_ptr<nfdnchar_t, PathDeleter<nfdnchar_t>> UniquePathN;
typedef std::unique_ptr<nfdu8char_t, PathDeleter<nfdu8char_t>> UniquePathU8;
struct PathSetDeleter {
inline void operator()(const nfdpathset_t* ptr) const noexcept { PathSet::Free(ptr); }
};
typedef std::unique_ptr<const nfdpathset_t, PathSetDeleter> UniquePathSet;
template <typename T>
struct PathSetPathDeleter {
inline void operator()(T* ptr) const noexcept { PathSet::FreePath(ptr); }
};
typedef std::unique_ptr<nfdchar_t, PathSetPathDeleter<nfdchar_t>> UniquePathSetPath;
typedef std::unique_ptr<nfdnchar_t, PathSetPathDeleter<nfdnchar_t>> UniquePathSetPathN;
typedef std::unique_ptr<nfdu8char_t, PathSetPathDeleter<nfdu8char_t>> UniquePathSetPathU8;
inline nfdresult_t OpenDialog(UniquePathN& outPath,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdnchar_t* out;
nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t OpenDialogMultiple(UniquePathSet& outPaths,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpathset_t* out;
nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPaths.reset(out);
}
return res;
}
inline nfdresult_t SaveDialog(UniquePathN& outPath,
const nfdnfilteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdnchar_t* defaultPath = nullptr,
const nfdnchar_t* defaultName = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdnchar_t* out;
nfdresult_t res =
SaveDialog(out, filterList, filterCount, defaultPath, defaultName, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t PickFolder(UniquePathN& outPath,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdnchar_t* out;
nfdresult_t res = PickFolder(out, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths,
const nfdnchar_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpathset_t* out;
nfdresult_t res = PickFolderMultiple(out, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPaths.reset(out);
}
return res;
}
#ifdef NFD_DIFFERENT_NATIVE_FUNCTIONS
inline nfdresult_t OpenDialog(UniquePathU8& outPath,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdu8char_t* out;
nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t OpenDialogMultiple(UniquePathSet& outPaths,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpathset_t* out;
nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPaths.reset(out);
}
return res;
}
inline nfdresult_t SaveDialog(UniquePathU8& outPath,
const nfdu8filteritem_t* filterList = nullptr,
nfdfiltersize_t filterCount = 0,
const nfdu8char_t* defaultPath = nullptr,
const nfdu8char_t* defaultName = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdu8char_t* out;
nfdresult_t res =
SaveDialog(out, filterList, filterCount, defaultPath, defaultName, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t PickFolder(UniquePathU8& outPath,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
nfdu8char_t* out;
nfdresult_t res = PickFolder(out, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths,
const nfdu8char_t* defaultPath = nullptr,
nfdwindowhandle_t parentWindow = {}) noexcept {
const nfdpathset_t* out;
nfdresult_t res = PickFolderMultiple(out, defaultPath, parentWindow);
if (res == NFD_OKAY) {
outPaths.reset(out);
}
return res;
}
#endif
namespace PathSet {
inline nfdresult_t Count(const UniquePathSet& uniquePathSet, nfdpathsetsize_t& count) noexcept {
return Count(uniquePathSet.get(), count);
}
inline nfdresult_t GetPath(const UniquePathSet& uniquePathSet,
nfdpathsetsize_t index,
UniquePathSetPathN& outPath) noexcept {
nfdnchar_t* out;
nfdresult_t res = GetPath(uniquePathSet.get(), index, out);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
#ifdef NFD_DIFFERENT_NATIVE_FUNCTIONS
inline nfdresult_t GetPath(const UniquePathSet& uniquePathSet,
nfdpathsetsize_t index,
UniquePathSetPathU8& outPath) noexcept {
nfdu8char_t* out;
nfdresult_t res = GetPath(uniquePathSet.get(), index, out);
if (res == NFD_OKAY) {
outPath.reset(out);
}
return res;
}
#endif
} // namespace PathSet
} // namespace NFD
#endif
@@ -0,0 +1,85 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo
This header contains a function to convert a GLFW window handle to a native window handle for
passing to NFDe.
*/
#ifndef _NFD_GLFW3_H
#define _NFD_GLFW3_H
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include <nfd.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#define NFD_INLINE inline
#else
#define NFD_INLINE static inline
#endif // __cplusplus
/**
* Converts a GLFW window handle to a native window handle that can be passed to NFDe.
* @param sdlWindow The GLFW window handle.
* @param[out] nativeWindow The output native window handle, populated if and only if this function
* returns true.
* @return Either true to indicate success, or false to indicate failure. It is intended that
* users ignore the error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this
* function fails. */
NFD_INLINE bool NFD_GetNativeWindowFromGLFWWindow(GLFWwindow* glfwWindow,
nfdwindowhandle_t* nativeWindow) {
GLFWerrorfun oldCallback = glfwSetErrorCallback(NULL);
bool success = false;
#if defined(GLFW_EXPOSE_NATIVE_WIN32)
if (!success) {
const HWND hwnd = glfwGetWin32Window(glfwWindow);
if (hwnd) {
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS;
nativeWindow->handle = (void*)hwnd;
success = true;
}
}
#endif
#if defined(GLFW_EXPOSE_NATIVE_COCOA)
if (!success) {
const id cocoa_window = glfwGetCocoaWindow(glfwWindow);
if (cocoa_window) {
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA;
nativeWindow->handle = (void*)cocoa_window;
success = true;
}
}
#endif
#if defined(GLFW_EXPOSE_NATIVE_X11)
if (!success) {
const Window x11_window = glfwGetX11Window(glfwWindow);
if (x11_window != None) {
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11;
nativeWindow->handle = (void*)x11_window;
success = true;
}
}
#endif
#if defined(GLFW_EXPOSE_NATIVE_WAYLAND)
// For now we don't support Wayland, but we intend to support it eventually.
// Silence the warnings.
{
(void)glfwWindow;
(void)nativeWindow;
}
#endif
glfwSetErrorCallback(oldCallback);
return success;
}
#undef NFD_INLINE
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // _NFD_GLFW3_H
@@ -0,0 +1,76 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo
This header contains a function to convert an SDL window handle to a native window handle for
passing to NFDe.
This is meant to be used with SDL2, but if there are incompatibilities with future SDL versions,
we can conditionally compile based on SDL_MAJOR_VERSION.
*/
#ifndef _NFD_SDL2_H
#define _NFD_SDL2_H
#include <SDL_error.h>
#include <SDL_syswm.h>
#include <nfd.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#define NFD_INLINE inline
#else
#define NFD_INLINE static inline
#endif // __cplusplus
/**
* Converts an SDL window handle to a native window handle that can be passed to NFDe.
* @param sdlWindow The SDL window handle.
* @param[out] nativeWindow The output native window handle, populated if and only if this function
* returns true.
* @return Either true to indicate success, or false to indicate failure. If false is returned,
* you can call SDL_GetError() for more information. However, it is intended that users ignore the
* error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this function fails. */
NFD_INLINE bool NFD_GetNativeWindowFromSDLWindow(SDL_Window* sdlWindow,
nfdwindowhandle_t* nativeWindow) {
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(sdlWindow, &info)) {
return false;
}
switch (info.subsystem) {
#if defined(SDL_VIDEO_DRIVER_WINDOWS)
case SDL_SYSWM_WINDOWS:
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS;
nativeWindow->handle = (void*)info.info.win.window;
return true;
#endif
#if defined(SDL_VIDEO_DRIVER_COCOA)
case SDL_SYSWM_COCOA:
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA;
nativeWindow->handle = (void*)info.info.cocoa.window;
return true;
#endif
#if defined(SDL_VIDEO_DRIVER_X11)
case SDL_SYSWM_X11:
nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11;
nativeWindow->handle = (void*)info.info.x11.window;
return true;
#endif
default:
// Silence the warning in case we are not using a supported backend.
(void)nativeWindow;
SDL_SetError("Unsupported native window type.");
return false;
}
}
#undef NFD_INLINE
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // _NFD_SDL2_H
+615
View File
@@ -0,0 +1,615 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo, Michael Labbe
*/
#include <AppKit/AppKit.h>
#include <Availability.h>
#include "nfd.h"
// MacOS is deprecating the allowedFileTypes property in favour of allowedContentTypes, so we have
// to introduce this breaking change. Define NFD_MACOS_ALLOWEDCONTENTTYPES to 1 to have it set the
// allowedContentTypes property of the SavePanel or OpenPanel. Define
// NFD_MACOS_ALLOWEDCONTENTTYPES to 0 to have it set the allowedFileTypes property of the SavePanel
// or OpenPanel. If NFD_MACOS_ALLOWEDCONTENTTYPES is undefined, then it will set it to 1 if
// __MAC_OS_X_VERSION_MIN_REQUIRED >= 11.0, and 0 otherwise.
#if !defined(NFD_MACOS_ALLOWEDCONTENTTYPES)
#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || \
__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0
#define NFD_MACOS_ALLOWEDCONTENTTYPES 0
#else
#define NFD_MACOS_ALLOWEDCONTENTTYPES 1
#endif
#endif
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif
static const char* g_errorstr = NULL;
static void NFDi_SetError(const char* msg) {
g_errorstr = msg;
}
static void* NFDi_Malloc(size_t bytes) {
void* ptr = malloc(bytes);
if (!ptr) NFDi_SetError("NFDi_Malloc failed.");
return ptr;
}
static void NFDi_Free(void* ptr) {
assert(ptr);
free(ptr);
}
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
// Returns an NSArray of UTType representing the content types.
static NSArray* BuildAllowedContentTypes(const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
NSMutableArray* buildFilterList = [[NSMutableArray alloc] init];
for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) {
// this is the spec to parse (we don't use the friendly name on OS X)
const nfdnchar_t* filterSpec = filterList[filterIndex].spec;
const nfdnchar_t* p_currentFilterBegin = filterSpec;
for (const nfdnchar_t* p_filterSpec = filterSpec; *p_filterSpec; ++p_filterSpec) {
if (*p_filterSpec == ',') {
// add the extension to the array
NSString* filterStr = [[NSString alloc]
initWithBytes:(const void*)p_currentFilterBegin
length:(sizeof(nfdnchar_t) * (p_filterSpec - p_currentFilterBegin))
encoding:NSUTF8StringEncoding];
UTType* filterType = [UTType typeWithFilenameExtension:filterStr
conformingToType:UTTypeData];
[filterStr release];
if (filterType) [buildFilterList addObject:filterType];
p_currentFilterBegin = p_filterSpec + 1;
}
}
// add the extension to the array
NSString* filterStr = [[NSString alloc] initWithUTF8String:p_currentFilterBegin];
UTType* filterType = [UTType typeWithFilenameExtension:filterStr
conformingToType:UTTypeData];
[filterStr release];
if (filterType) [buildFilterList addObject:filterType];
}
NSArray* returnArray = [NSArray arrayWithArray:buildFilterList];
[buildFilterList release];
assert([returnArray count] != 0);
return returnArray;
}
#else
// Returns an NSArray of NSString representing the file types.
static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
NSMutableArray* buildFilterList = [[NSMutableArray alloc] init];
for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) {
// this is the spec to parse (we don't use the friendly name on OS X)
const nfdnchar_t* filterSpec = filterList[filterIndex].spec;
const nfdnchar_t* p_currentFilterBegin = filterSpec;
for (const nfdnchar_t* p_filterSpec = filterSpec; *p_filterSpec; ++p_filterSpec) {
if (*p_filterSpec == ',') {
// add the extension to the array
NSString* filterStr = [[[NSString alloc]
initWithBytes:(const void*)p_currentFilterBegin
length:(sizeof(nfdnchar_t) * (p_filterSpec - p_currentFilterBegin))
encoding:NSUTF8StringEncoding] autorelease];
[buildFilterList addObject:filterStr];
p_currentFilterBegin = p_filterSpec + 1;
}
}
// add the extension to the array
NSString* filterStr = [NSString stringWithUTF8String:p_currentFilterBegin];
[buildFilterList addObject:filterStr];
}
NSArray* returnArray = [NSArray arrayWithArray:buildFilterList];
[buildFilterList release];
assert([returnArray count] != 0);
return returnArray;
}
#endif
static void AddFilterListToDialog(NSSavePanel* dialog,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
// note: NSOpenPanel inherits from NSSavePanel.
if (!filterCount) return;
assert(filterList);
// Make NSArray of file types and set it on the dialog
// We use setAllowedFileTypes or setAllowedContentTypes depending on the deployment target
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
NSArray* allowedContentTypes = BuildAllowedContentTypes(filterList, filterCount);
[dialog setAllowedContentTypes:allowedContentTypes];
#else
NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount);
[dialog setAllowedFileTypes:allowedFileTypes];
#endif
}
static void SetDefaultPath(NSSavePanel* dialog, const nfdnchar_t* defaultPath) {
if (!defaultPath || !*defaultPath) return;
NSString* defaultPathString = [NSString stringWithUTF8String:defaultPath];
NSURL* url = [NSURL fileURLWithPath:defaultPathString isDirectory:YES];
[dialog setDirectoryURL:url];
}
static void SetDefaultName(NSSavePanel* dialog, const nfdnchar_t* defaultName) {
if (!defaultName || !*defaultName) return;
NSString* defaultNameString = [NSString stringWithUTF8String:defaultName];
[dialog setNameFieldStringValue:defaultNameString];
}
static nfdresult_t CopyUtf8String(const char* utf8Str, nfdnchar_t** out) {
// byte count, not char count
size_t len = strlen(utf8Str);
// Too bad we have to use additional memory for all the result paths,
// because we cannot reconstitute an NSString from a char* to release it properly.
*out = (nfdnchar_t*)NFDi_Malloc(len + 1);
if (*out) {
strcpy(*out, utf8Str);
return NFD_OKAY;
}
return NFD_ERROR;
}
static NSWindow* GetNativeWindowHandle(const nfdwindowhandle_t* parentWindow) {
if (parentWindow->type != NFD_WINDOW_HANDLE_TYPE_COCOA) {
return NULL;
}
return (NSWindow*)parentWindow->handle;
}
/* public */
const char* NFD_GetError(void) {
return g_errorstr;
}
void NFD_ClearError(void) {
NFDi_SetError(NULL);
}
void NFD_FreePathN(nfdnchar_t* filePath) {
NFDi_Free((void*)filePath);
}
void NFD_FreePathU8(nfdu8char_t* filePath) {
NFD_FreePathN(filePath);
}
static NSApplicationActivationPolicy old_app_policy;
nfdresult_t NFD_Init(void) {
NSApplication* app = [NSApplication sharedApplication];
old_app_policy = [app activationPolicy];
if (old_app_policy == NSApplicationActivationPolicyProhibited) {
if (![app setActivationPolicy:NSApplicationActivationPolicyAccessory]) {
NFDi_SetError("Failed to set activation policy.");
return NFD_ERROR;
}
}
return NFD_OKAY;
}
/* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */
void NFD_Quit(void) {
[[NSApplication sharedApplication] setActivationPolicy:old_app_policy];
}
nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
nfdopendialognargs_t args = {0};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdopendialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
nfdresult_t result = NFD_CANCEL;
@autoreleasepool {
NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow);
if (keyWindow) {
[keyWindow makeKeyAndOrderFront:nil];
} else {
keyWindow = [[NSApplication sharedApplication] keyWindow];
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
[dialog setAllowsMultipleSelection:NO];
// Build the filter list
AddFilterListToDialog(dialog, args->filterList, args->filterCount);
// Set the starting directory
SetDefaultPath(dialog, args->defaultPath);
if ([dialog runModal] == NSModalResponseOK) {
const NSURL* url = [dialog URL];
const char* utf8Path = [[url path] UTF8String];
result = CopyUtf8String(utf8Path, outPath);
}
// return focus to the key window (i.e. main window)
[keyWindow makeKeyAndOrderFront:nil];
}
return result;
}
nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath) {
return NFD_OpenDialogN(outPath, filterList, filterCount, defaultPath);
}
nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdopendialogu8args_t* args) {
return NFD_OpenDialogN_With_Impl(version, outPath, args);
}
nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
nfdopendialognargs_t args = {0};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args);
}
nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
nfdresult_t result = NFD_CANCEL;
@autoreleasepool {
NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow);
if (keyWindow) {
[keyWindow makeKeyAndOrderFront:nil];
} else {
keyWindow = [[NSApplication sharedApplication] keyWindow];
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
[dialog setAllowsMultipleSelection:YES];
// Build the filter list
AddFilterListToDialog(dialog, args->filterList, args->filterCount);
// Set the starting directory
SetDefaultPath(dialog, args->defaultPath);
if ([dialog runModal] == NSModalResponseOK) {
const NSArray* urls = [dialog URLs];
if ([urls count] > 0) {
// have at least one URL, we return this NSArray
[urls retain];
*outPaths = (const nfdpathset_t*)urls;
result = NFD_OKAY;
}
}
// return focus to the key window (i.e. main window)
[keyWindow makeKeyAndOrderFront:nil];
}
return result;
}
nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath) {
return NFD_OpenDialogMultipleN(outPaths, filterList, filterCount, defaultPath);
}
nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialogu8args_t* args) {
return NFD_OpenDialogMultipleN_With_Impl(version, outPaths, args);
}
nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName) {
nfdsavedialognargs_t args = {0};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
args.defaultName = defaultName;
return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdsavedialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
nfdresult_t result = NFD_CANCEL;
@autoreleasepool {
NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow);
if (keyWindow) {
[keyWindow makeKeyAndOrderFront:nil];
} else {
keyWindow = [[NSApplication sharedApplication] keyWindow];
}
NSSavePanel* dialog = [NSSavePanel savePanel];
[dialog setExtensionHidden:NO];
// allow other file types, to give the user an escape hatch since you can't select "*.*" on
// Mac
[dialog setAllowsOtherFileTypes:TRUE];
// Build the filter list
AddFilterListToDialog(dialog, args->filterList, args->filterCount);
// Set the starting directory
SetDefaultPath(dialog, args->defaultPath);
// Set the default file name
SetDefaultName(dialog, args->defaultName);
if ([dialog runModal] == NSModalResponseOK) {
const NSURL* url = [dialog URL];
const char* utf8Path = [[url path] UTF8String];
result = CopyUtf8String(utf8Path, outPath);
}
// return focus to the key window (i.e. main window)
[keyWindow makeKeyAndOrderFront:nil];
}
return result;
}
nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath,
const nfdu8char_t* defaultName) {
return NFD_SaveDialogN(outPath, filterList, filterCount, defaultPath, defaultName);
}
nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdsavedialogu8args_t* args) {
return NFD_SaveDialogN_With_Impl(version, outPath, args);
}
nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) {
nfdpickfoldernargs_t args = {0};
args.defaultPath = defaultPath;
return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdpickfoldernargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
nfdresult_t result = NFD_CANCEL;
@autoreleasepool {
NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow);
if (keyWindow) {
[keyWindow makeKeyAndOrderFront:nil];
} else {
keyWindow = [[NSApplication sharedApplication] keyWindow];
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
[dialog setAllowsMultipleSelection:NO];
[dialog setCanChooseDirectories:YES];
[dialog setCanCreateDirectories:YES];
[dialog setCanChooseFiles:NO];
// Set the starting directory
SetDefaultPath(dialog, args->defaultPath);
if ([dialog runModal] == NSModalResponseOK) {
const NSURL* url = [dialog URL];
const char* utf8Path = [[url path] UTF8String];
result = CopyUtf8String(utf8Path, outPath);
}
// return focus to the key window (i.e. main window)
[keyWindow makeKeyAndOrderFront:nil];
}
return result;
}
nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) {
return NFD_PickFolderN(outPath, defaultPath);
}
nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdpickfolderu8args_t* args) {
return NFD_PickFolderN_With_Impl(version, outPath, args);
}
nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) {
nfdpickfoldernargs_t args = {0};
args.defaultPath = defaultPath;
return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args);
}
nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfoldernargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
nfdresult_t result = NFD_CANCEL;
@autoreleasepool {
NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow);
if (keyWindow) {
[keyWindow makeKeyAndOrderFront:nil];
} else {
keyWindow = [[NSApplication sharedApplication] keyWindow];
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
[dialog setAllowsMultipleSelection:YES];
[dialog setCanChooseDirectories:YES];
[dialog setCanCreateDirectories:YES];
[dialog setCanChooseFiles:NO];
// Set the starting directory
SetDefaultPath(dialog, args->defaultPath);
if ([dialog runModal] == NSModalResponseOK) {
const NSArray* urls = [dialog URLs];
if ([urls count] > 0) {
// have at least one URL, we return this NSArray
[urls retain];
*outPaths = (const nfdpathset_t*)urls;
result = NFD_OKAY;
}
}
// return focus to the key window (i.e. main window)
[keyWindow makeKeyAndOrderFront:nil];
}
return result;
}
nfdresult_t NFD_PickFolderMultipleU8(const nfdpathset_t** outPaths,
const nfdu8char_t* defaultPath) {
return NFD_PickFolderMultipleN(outPaths, defaultPath);
}
nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfolderu8args_t* args) {
return NFD_PickFolderMultipleN_With_Impl(version, outPaths, args);
}
nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) {
const NSArray* urls = (const NSArray*)pathSet;
*count = [urls count];
return NFD_OKAY;
}
nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdnchar_t** outPath) {
const NSArray* urls = (const NSArray*)pathSet;
@autoreleasepool {
// autoreleasepool needed because UTF8String method might use the pool
const NSURL* url = [urls objectAtIndex:index];
const char* utf8Path = [[url path] UTF8String];
return CopyUtf8String(utf8Path, outPath);
}
}
nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdu8char_t** outPath) {
return NFD_PathSet_GetPathN(pathSet, index, outPath);
}
void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) {
// const_cast not supported on Mac
union {
const nfdnchar_t* constPath;
nfdnchar_t* nonConstPath;
} pathUnion;
pathUnion.constPath = filePath;
NFD_FreePathN(pathUnion.nonConstPath);
}
void NFD_PathSet_FreePathU8(const nfdu8char_t* filePath) {
// const_cast not supported on Mac
union {
const nfdu8char_t* constPath;
nfdu8char_t* nonConstPath;
} pathUnion;
pathUnion.constPath = filePath;
NFD_FreePathU8(pathUnion.nonConstPath);
}
void NFD_PathSet_Free(const nfdpathset_t* pathSet) {
const NSArray* urls = (const NSArray*)pathSet;
[urls release];
}
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) {
const NSArray* urls = (const NSArray*)pathSet;
@autoreleasepool {
// autoreleasepool needed because NSEnumerator uses it
NSEnumerator* enumerator = [urls objectEnumerator];
[enumerator retain];
outEnumerator->ptr = (void*)enumerator;
}
return NFD_OKAY;
}
void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) {
NSEnumerator* real_enum = (NSEnumerator*)enumerator->ptr;
[real_enum release];
}
nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) {
NSEnumerator* real_enum = (NSEnumerator*)enumerator->ptr;
@autoreleasepool {
// autoreleasepool needed because NSURL uses it
const NSURL* url = [real_enum nextObject];
if (url) {
const char* utf8Path = [[url path] UTF8String];
return CopyUtf8String(utf8Path, outPath);
} else {
*outPath = NULL;
return NFD_OKAY;
}
}
}
nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath) {
return NFD_PathSet_EnumNextN(enumerator, outPath);
}
+974
View File
@@ -0,0 +1,974 @@
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo, Michael Labbe
Note: We do not check for malloc failure on Linux - Linux overcommits memory!
*/
#include <assert.h>
#include <gtk/gtk.h>
#if defined(GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nfd.h"
/*
Define NFD_CASE_SENSITIVE_FILTER if you want file filters to be case-sensitive. The default
is case-insensitive. While Linux uses a case-sensitive filesystem and is designed for
case-sensitive file extensions, perhaps in the vast majority of cases users actually expect the file
filters to be case-insensitive.
*/
namespace {
template <typename T>
struct Free_Guard {
T* data;
Free_Guard(T* freeable) noexcept : data(freeable) {}
~Free_Guard() { NFDi_Free(data); }
};
template <typename T>
struct FreeCheck_Guard {
T* data;
FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {}
~FreeCheck_Guard() {
if (data) NFDi_Free(data);
}
};
/* current error */
const char* g_errorstr = nullptr;
void NFDi_SetError(const char* msg) {
g_errorstr = msg;
}
template <typename T = void>
T* NFDi_Malloc(size_t bytes) {
void* ptr = malloc(bytes);
if (!ptr) NFDi_SetError("NFDi_Malloc failed.");
return static_cast<T*>(ptr);
}
template <typename T>
void NFDi_Free(T* ptr) {
assert(ptr);
free(static_cast<void*>(ptr));
}
template <typename T>
T* copy(const T* begin, const T* end, T* out) {
for (; begin != end; ++begin) {
*out++ = *begin;
}
return out;
}
#ifndef NFD_CASE_SENSITIVE_FILTER
nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin,
const nfdnchar_t* end,
nfdnchar_t* out) {
// this code will only make regular Latin characters case-insensitive; other
// characters remain case sensitive
for (; begin != end; ++begin) {
if ((*begin >= 'A' && *begin <= 'Z') || (*begin >= 'a' && *begin <= 'z')) {
*out++ = '[';
*out++ = *begin;
// invert the case of the original character
*out++ = *begin ^ static_cast<nfdnchar_t>(0x20);
*out++ = ']';
} else {
*out++ = *begin;
}
}
return out;
}
#endif
// Does not own the filter and extension.
struct Pair_GtkFileFilter_FileExtension {
GtkFileFilter* filter;
const nfdnchar_t* extensionBegin;
const nfdnchar_t* extensionEnd;
};
struct ButtonClickedArgs {
Pair_GtkFileFilter_FileExtension* map;
GtkFileChooser* chooser;
};
void AddFiltersToDialog(GtkFileChooser* chooser,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
if (filterCount) {
assert(filterList);
// we have filters to add ... format and add them
for (nfdfiltersize_t index = 0; index != filterCount; ++index) {
GtkFileFilter* filter = gtk_file_filter_new();
// count number of file extensions
size_t sep = 1;
for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) {
if (*p_spec == ',') {
++sep;
}
}
// friendly name conversions: "png,jpg" -> "Image files
// (png, jpg)"
// calculate space needed (including the trailing '\0')
size_t nameSize =
sep + strlen(filterList[index].spec) + 3 + strlen(filterList[index].name);
// malloc the required memory
nfdnchar_t* nameBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) * nameSize);
nfdnchar_t* p_nameBuf = nameBuf;
for (const nfdnchar_t* p_filterName = filterList[index].name; *p_filterName;
++p_filterName) {
*p_nameBuf++ = *p_filterName;
}
*p_nameBuf++ = ' ';
*p_nameBuf++ = '(';
const nfdnchar_t* p_extensionStart = filterList[index].spec;
for (const nfdnchar_t* p_spec = filterList[index].spec; true; ++p_spec) {
if (*p_spec == ',' || !*p_spec) {
if (*p_spec == ',') {
*p_nameBuf++ = ',';
*p_nameBuf++ = ' ';
}
#ifdef NFD_CASE_SENSITIVE_FILTER
// +1 for the trailing '\0'
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) *
(p_spec - p_extensionStart + 3));
nfdnchar_t* p_extnBufEnd = extnBuf;
*p_extnBufEnd++ = '*';
*p_extnBufEnd++ = '.';
p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd);
*p_extnBufEnd++ = '\0';
gtk_file_filter_add_pattern(filter, extnBuf);
NFDi_Free(extnBuf);
#else
// Each character in the Latin alphabet is converted into 4 characters. E.g.
// 'a' is converted into "[Aa]". Other characters are preserved. Then we +1
// for the trailing '\0'.
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(
sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3));
nfdnchar_t* p_extnBufEnd = extnBuf;
*p_extnBufEnd++ = '*';
*p_extnBufEnd++ = '.';
p_extnBufEnd =
emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd);
*p_extnBufEnd++ = '\0';
gtk_file_filter_add_pattern(filter, extnBuf);
NFDi_Free(extnBuf);
#endif
if (*p_spec) {
// update the extension start point
p_extensionStart = p_spec + 1;
} else {
// reached the '\0' character
break;
}
} else {
*p_nameBuf++ = *p_spec;
}
}
*p_nameBuf++ = ')';
*p_nameBuf++ = '\0';
assert((size_t)(p_nameBuf - nameBuf) == sizeof(nfdnchar_t) * nameSize);
// add to the filter
gtk_file_filter_set_name(filter, nameBuf);
// free the memory
NFDi_Free(nameBuf);
// add filter to chooser
gtk_file_chooser_add_filter(chooser, filter);
}
}
/* always append a wildcard option to the end*/
GtkFileFilter* filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, "All files");
gtk_file_filter_add_pattern(filter, "*");
gtk_file_chooser_add_filter(chooser, filter);
}
// returns null-terminated map (trailing .filter is null)
Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* chooser,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
Pair_GtkFileFilter_FileExtension* map = NFDi_Malloc<Pair_GtkFileFilter_FileExtension>(
sizeof(Pair_GtkFileFilter_FileExtension) * (filterCount + 1));
if (filterCount) {
assert(filterList);
// we have filters to add ... format and add them
for (nfdfiltersize_t index = 0; index != filterCount; ++index) {
GtkFileFilter* filter = gtk_file_filter_new();
// store filter in map
map[index].filter = filter;
map[index].extensionBegin = filterList[index].spec;
map[index].extensionEnd = nullptr;
// count number of file extensions
size_t sep = 1;
for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) {
if (*p_spec == ',') {
++sep;
}
}
// friendly name conversions: "png,jpg" -> "Image files
// (png, jpg)"
// calculate space needed (including the trailing '\0')
size_t nameSize =
sep + strlen(filterList[index].spec) + 3 + strlen(filterList[index].name);
// malloc the required memory
nfdnchar_t* nameBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) * nameSize);
nfdnchar_t* p_nameBuf = nameBuf;
for (const nfdnchar_t* p_filterName = filterList[index].name; *p_filterName;
++p_filterName) {
*p_nameBuf++ = *p_filterName;
}
*p_nameBuf++ = ' ';
*p_nameBuf++ = '(';
const nfdnchar_t* p_extensionStart = filterList[index].spec;
for (const nfdnchar_t* p_spec = filterList[index].spec; true; ++p_spec) {
if (*p_spec == ',' || !*p_spec) {
if (*p_spec == ',') {
*p_nameBuf++ = ',';
*p_nameBuf++ = ' ';
}
#ifdef NFD_CASE_SENSITIVE_FILTER
// +1 for the trailing '\0'
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) *
(p_spec - p_extensionStart + 3));
nfdnchar_t* p_extnBufEnd = extnBuf;
*p_extnBufEnd++ = '*';
*p_extnBufEnd++ = '.';
p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd);
*p_extnBufEnd++ = '\0';
gtk_file_filter_add_pattern(filter, extnBuf);
NFDi_Free(extnBuf);
#else
// Each character in the Latin alphabet is converted into 4 characters. E.g.
// 'a' is converted into "[Aa]". Other characters are preserved. Then we +1
// for the trailing '\0'.
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(
sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3));
nfdnchar_t* p_extnBufEnd = extnBuf;
*p_extnBufEnd++ = '*';
*p_extnBufEnd++ = '.';
p_extnBufEnd =
emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd);
*p_extnBufEnd++ = '\0';
gtk_file_filter_add_pattern(filter, extnBuf);
NFDi_Free(extnBuf);
#endif
// store current pointer in map (if it's
// the first one)
if (map[index].extensionEnd == nullptr) {
map[index].extensionEnd = p_spec;
}
if (*p_spec) {
// update the extension start point
p_extensionStart = p_spec + 1;
} else {
// reached the '\0' character
break;
}
} else {
*p_nameBuf++ = *p_spec;
}
}
*p_nameBuf++ = ')';
*p_nameBuf++ = '\0';
assert((size_t)(p_nameBuf - nameBuf) == sizeof(nfdnchar_t) * nameSize);
// add to the filter
gtk_file_filter_set_name(filter, nameBuf);
// free the memory
NFDi_Free(nameBuf);
// add filter to chooser
gtk_file_chooser_add_filter(chooser, filter);
}
}
// set trailing map index to null
map[filterCount].filter = nullptr;
/* always append a wildcard option to the end*/
GtkFileFilter* filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, "All files");
gtk_file_filter_add_pattern(filter, "*");
gtk_file_chooser_add_filter(chooser, filter);
return map;
}
void SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) {
if (!defaultPath || !*defaultPath) return;
/* GTK+ manual recommends not specifically setting the default path.
We do it anyway in order to be consistent across platforms.
If consistency with the native OS is preferred, this is the line
to comment out. -ml */
gtk_file_chooser_set_current_folder(chooser, defaultPath);
}
void SetDefaultName(GtkFileChooser* chooser, const char* defaultName) {
if (!defaultName || !*defaultName) return;
gtk_file_chooser_set_current_name(chooser, defaultName);
}
void WaitForCleanup() {
while (gtk_events_pending()) gtk_main_iteration();
}
struct Widget_Guard {
GtkWidget* data;
Widget_Guard(GtkWidget* widget) : data(widget) {}
~Widget_Guard() {
WaitForCleanup();
gtk_widget_destroy(data);
WaitForCleanup();
}
};
void FileActivatedSignalHandler(GtkButton* saveButton, void* userdata) {
(void)saveButton; // silence the unused arg warning
ButtonClickedArgs* args = static_cast<ButtonClickedArgs*>(userdata);
GtkFileChooser* chooser = args->chooser;
char* currentFileName = gtk_file_chooser_get_current_name(chooser);
if (*currentFileName) { // string is not empty
// find a '.' in the file name
const char* p_period = currentFileName;
for (; *p_period; ++p_period) {
if (*p_period == '.') {
break;
}
}
if (!*p_period) { // there is no '.', so append the default extension
Pair_GtkFileFilter_FileExtension* filterMap =
static_cast<Pair_GtkFileFilter_FileExtension*>(args->map);
GtkFileFilter* currentFilter = gtk_file_chooser_get_filter(chooser);
if (currentFilter) {
for (; filterMap->filter; ++filterMap) {
if (filterMap->filter == currentFilter) break;
}
}
if (filterMap->filter) {
// memory for appended string (including '.' and
// trailing '\0')
char* appendedFileName = NFDi_Malloc<char>(
sizeof(char) * ((p_period - currentFileName) +
(filterMap->extensionEnd - filterMap->extensionBegin) + 2));
char* p_fileName = copy(currentFileName, p_period, appendedFileName);
*p_fileName++ = '.';
p_fileName = copy(filterMap->extensionBegin, filterMap->extensionEnd, p_fileName);
*p_fileName++ = '\0';
assert(p_fileName - appendedFileName ==
(p_period - currentFileName) +
(filterMap->extensionEnd - filterMap->extensionBegin) + 2);
// set the appended file name
gtk_file_chooser_set_current_name(chooser, appendedFileName);
// free the memory
NFDi_Free(appendedFileName);
}
}
}
// free the memory
g_free(currentFileName);
}
// wrapper for gtk_dialog_run() that brings the dialog to the front
// see issues at:
// https://github.com/btzy/nativefiledialog-extended/issues/31
// https://github.com/mlabbe/nativefiledialog/pull/92
// https://github.com/guillaumechereau/noc/pull/11
gint RunDialogWithFocus(GtkDialog* dialog) {
#if defined(GDK_WINDOWING_X11)
gtk_widget_show_all(GTK_WIDGET(dialog)); // show the dialog so that it gets a display
if (GDK_IS_X11_DISPLAY(gtk_widget_get_display(GTK_WIDGET(dialog)))) {
GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(dialog));
gdk_window_set_events(
window,
static_cast<GdkEventMask>(gdk_window_get_events(window) | GDK_PROPERTY_CHANGE_MASK));
gtk_window_present_with_time(GTK_WINDOW(dialog), gdk_x11_get_server_time(window));
}
#endif
return gtk_dialog_run(dialog);
}
// Gets the GdkWindow from the given window handle. This function might fail even if parentWindow
// is set correctly, since it calls some failable GDK functions. If it fails, it will return
// nullptr. The caller is responsible for freeing ths returned GdkWindow, if not nullptr.
GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow) {
switch (parentWindow.type) {
#if defined(GDK_WINDOWING_X11)
case NFD_WINDOW_HANDLE_TYPE_X11: {
const Window x11_handle = reinterpret_cast<Window>(parentWindow.handle);
// AFAIK, _any_ X11 display will do, because Windows are not associated to a specific
// Display. Supposedly, a Display is just a connection to the X server.
// This will contain the X11 display we want to use.
GdkDisplay* x11_display = nullptr;
GdkDisplayManager* display_manager = gdk_display_manager_get();
// If we can find an existing X11 display, use it.
GSList* gdk_display_list = gdk_display_manager_list_displays(display_manager);
while (gdk_display_list) {
GSList* node = gdk_display_list;
GdkDisplay* display = GDK_DISPLAY(node->data);
if (GDK_IS_X11_DISPLAY(display)) {
g_slist_free(node);
x11_display = display;
break;
} else {
gdk_display_list = node->next;
g_slist_free_1(node);
}
}
// Otherwise, we have to create our own X11 display.
if (!x11_display) {
// This is not very nice, because we are always resetting the allowed backends
// setting to NULL (which means all backends are allowed), even though we can't be
// sure that the user didn't call gdk_set_allowed_backends() earlier to force a
// specific backend. But well if the user doesn't have an X11 display already open
// and yet is telling us with have an X11 window as parent, they probably don't use
// GTK in their application at all so they probably won't notice this.
//
// There is no way, AFAIK, to get the allowed backends first so we can restore it
// later, and gdk_x11_display_open() is GTK4-only (the GTK3 version is a private
// implementation detail).
//
// Also, we don't close the display we specially opened, since GTK will need it to
// show the dialog. Though it probably doesn't matter very much if we want to free
// up resources and clean it up.
gdk_set_allowed_backends("x11");
x11_display = gdk_display_manager_open_display(display_manager, NULL);
gdk_set_allowed_backends(NULL);
}
if (!x11_display) return nullptr;
GdkWindow* gdk_window = gdk_x11_window_foreign_new_for_display(x11_display, x11_handle);
return gdk_window;
}
#endif
default:
return nullptr;
}
}
void RealizedSignalHandler(GtkWidget* window, void* userdata) {
GdkWindow* const parentWindow = static_cast<GdkWindow*>(userdata);
gdk_window_set_transient_for(gtk_widget_get_window(window), parentWindow);
}
struct NativeWindowParenter {
NativeWindowParenter(GtkWidget* w, const nfdwindowhandle_t& parentWindow) noexcept : widget(w) {
parent = GetAllocNativeWindowHandle(parentWindow);
if (parent) {
// set the handler to the realize signal to set the transient GDK parent
handlerID = g_signal_connect(G_OBJECT(widget),
"realize",
G_CALLBACK(RealizedSignalHandler),
static_cast<void*>(parent));
// make the dialog window use the same GtkScreen as the parent (so that parenting works)
gtk_window_set_screen(GTK_WINDOW(widget), gdk_window_get_screen(parent));
}
}
~NativeWindowParenter() {
if (parent) {
// unset the handler and delete the parent GdkWindow
g_signal_handler_disconnect(G_OBJECT(widget), handlerID);
g_object_unref(parent);
}
}
GtkWidget* const widget;
GdkWindow* parent;
gulong handlerID;
};
} // namespace
const char* NFD_GetError(void) {
return g_errorstr;
}
void NFD_ClearError(void) {
NFDi_SetError(nullptr);
}
/* public */
nfdresult_t NFD_Init(void) {
// Init GTK
if (!gtk_init_check(NULL, NULL)) {
NFDi_SetError("Failed to initialize GTK+ with gtk_init_check.");
return NFD_ERROR;
}
return NFD_OKAY;
}
void NFD_Quit(void) {
// do nothing, GTK cannot be de-initialized
}
void NFD_FreePathN(nfdnchar_t* filePath) {
assert(filePath);
g_free(filePath);
}
void NFD_FreePathU8(nfdu8char_t* filePath) __attribute__((alias("NFD_FreePathN")));
nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
nfdopendialognargs_t args{};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdopendialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
GtkWidget* widget = gtk_file_chooser_dialog_new("Open File",
nullptr,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel",
GTK_RESPONSE_CANCEL,
"_Open",
GTK_RESPONSE_ACCEPT,
nullptr);
// guard to destroy the widget when returning from this function
Widget_Guard widgetGuard(widget);
/* Build the filter list */
AddFiltersToDialog(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount);
/* Set the default path */
SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath);
gint result;
{
/* Parent the window properly */
NativeWindowParenter nativeWindowParenter(widget, args->parentWindow);
/* invoke the dialog (blocks until dialog is closed) */
result = RunDialogWithFocus(GTK_DIALOG(widget));
}
if (result == GTK_RESPONSE_ACCEPT) {
// write out the file name
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
return NFD_OKAY;
} else {
return NFD_CANCEL;
}
}
nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath)
__attribute__((alias("NFD_OpenDialogN")));
nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdopendialogu8args_t* args)
__attribute__((alias("NFD_OpenDialogN_With_Impl")));
nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
nfdopendialognargs_t args{};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args);
}
nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
GtkWidget* widget = gtk_file_chooser_dialog_new("Open Files",
nullptr,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel",
GTK_RESPONSE_CANCEL,
"_Open",
GTK_RESPONSE_ACCEPT,
nullptr);
// guard to destroy the widget when returning from this function
Widget_Guard widgetGuard(widget);
// set select multiple
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(widget), TRUE);
/* Build the filter list */
AddFiltersToDialog(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount);
/* Set the default path */
SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath);
gint result;
{
/* Parent the window properly */
NativeWindowParenter nativeWindowParenter(widget, args->parentWindow);
/* invoke the dialog (blocks until dialog is closed) */
result = RunDialogWithFocus(GTK_DIALOG(widget));
}
if (result == GTK_RESPONSE_ACCEPT) {
// write out the file name
GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget));
*outPaths = static_cast<void*>(fileList);
return NFD_OKAY;
} else {
return NFD_CANCEL;
}
}
nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath)
__attribute__((alias("NFD_OpenDialogMultipleN")));
nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdopendialogu8args_t* args)
__attribute__((alias("NFD_OpenDialogMultipleN_With_Impl")));
nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName) {
nfdsavedialognargs_t args{};
args.filterList = filterList;
args.filterCount = filterCount;
args.defaultPath = defaultPath;
args.defaultName = defaultName;
return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdsavedialognargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
GtkWidget* widget = gtk_file_chooser_dialog_new("Save File",
nullptr,
GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel",
GTK_RESPONSE_CANCEL,
nullptr);
// guard to destroy the widget when returning from this function
Widget_Guard widgetGuard(widget);
GtkWidget* saveButton = gtk_dialog_add_button(GTK_DIALOG(widget), "_Save", GTK_RESPONSE_ACCEPT);
// Prompt on overwrite
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(widget), TRUE);
/* Build the filter list */
ButtonClickedArgs buttonClickedArgs;
buttonClickedArgs.chooser = GTK_FILE_CHOOSER(widget);
buttonClickedArgs.map =
AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount);
/* Set the default path */
SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath);
/* Set the default file name */
SetDefaultName(GTK_FILE_CHOOSER(widget), args->defaultName);
/* set the handler to add file extension */
gulong handlerID = g_signal_connect(G_OBJECT(saveButton),
"pressed",
G_CALLBACK(FileActivatedSignalHandler),
static_cast<void*>(&buttonClickedArgs));
gint result;
{
/* Parent the window properly */
NativeWindowParenter nativeWindowParenter(widget, args->parentWindow);
/* invoke the dialog (blocks until dialog is closed) */
result = RunDialogWithFocus(GTK_DIALOG(widget));
}
/* unset the handler */
g_signal_handler_disconnect(G_OBJECT(saveButton), handlerID);
/* free the filter map */
NFDi_Free(buttonClickedArgs.map);
if (result == GTK_RESPONSE_ACCEPT) {
// write out the file name
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
return NFD_OKAY;
} else {
return NFD_CANCEL;
}
}
nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdu8char_t* defaultPath,
const nfdu8char_t* defaultName)
__attribute__((alias("NFD_SaveDialogN")));
nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdsavedialogu8args_t* args)
__attribute__((alias("NFD_SaveDialogN_With_Impl")));
nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) {
nfdpickfoldernargs_t args{};
args.defaultPath = defaultPath;
return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args);
}
nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version,
nfdnchar_t** outPath,
const nfdpickfoldernargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
GtkWidget* widget = gtk_file_chooser_dialog_new("Select Folder",
nullptr,
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
"_Cancel",
GTK_RESPONSE_CANCEL,
"_Select",
GTK_RESPONSE_ACCEPT,
nullptr);
// guard to destroy the widget when returning from this function
Widget_Guard widgetGuard(widget);
/* Set the default path */
SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath);
gint result;
{
/* Parent the window properly */
NativeWindowParenter nativeWindowParenter(widget, args->parentWindow);
/* invoke the dialog (blocks until dialog is closed) */
result = RunDialogWithFocus(GTK_DIALOG(widget));
}
if (result == GTK_RESPONSE_ACCEPT) {
// write out the file name
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
return NFD_OKAY;
} else {
return NFD_CANCEL;
}
}
nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath)
__attribute__((alias("NFD_PickFolderN")));
nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version,
nfdu8char_t** outPath,
const nfdpickfolderu8args_t* args)
__attribute__((alias("NFD_PickFolderN_With_Impl")));
nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) {
nfdpickfoldernargs_t args{};
args.defaultPath = defaultPath;
return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args);
}
nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfoldernargs_t* args) {
// We haven't needed to bump the interface version yet.
(void)version;
GtkWidget* widget = gtk_file_chooser_dialog_new("Select Folders",
nullptr,
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
"_Cancel",
GTK_RESPONSE_CANCEL,
"_Select",
GTK_RESPONSE_ACCEPT,
nullptr);
// guard to destroy the widget when returning from this function
Widget_Guard widgetGuard(widget);
/* Set the default path */
SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath);
gint result;
{
/* Parent the window properly */
NativeWindowParenter nativeWindowParenter(widget, args->parentWindow);
/* invoke the dialog (blocks until dialog is closed) */
result = RunDialogWithFocus(GTK_DIALOG(widget));
}
if (result == GTK_RESPONSE_ACCEPT) {
// write out the file name
GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget));
*outPaths = static_cast<void*>(fileList);
return NFD_OKAY;
} else {
return NFD_CANCEL;
}
}
nfdresult_t NFD_PickFolderMultipleU8(const nfdpathset_t** outPaths, const nfdu8char_t* defaultPath)
__attribute__((alias("NFD_PickFolderMultipleN")));
nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version,
const nfdpathset_t** outPaths,
const nfdpickfolderu8args_t* args)
__attribute__((alias("NFD_PickFolderMultipleN_With_Impl")));
nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) {
assert(pathSet);
// const_cast because methods on GSList aren't const, but it should act
// like const to the caller
GSList* fileList = const_cast<GSList*>(static_cast<const GSList*>(pathSet));
*count = g_slist_length(fileList);
return NFD_OKAY;
}
nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdnchar_t** outPath) {
assert(pathSet);
// const_cast because methods on GSList aren't const, but it should act
// like const to the caller
GSList* fileList = const_cast<GSList*>(static_cast<const GSList*>(pathSet));
// Note: this takes linear time... but should be good enough
*outPath = static_cast<nfdnchar_t*>(g_slist_nth_data(fileList, index));
return NFD_OKAY;
}
nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdu8char_t** outPath)
__attribute__((alias("NFD_PathSet_GetPathN")));
void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) {
assert(filePath);
(void)filePath; // prevent warning in release build
// no-op, because NFD_PathSet_Free does the freeing for us
}
void NFD_PathSet_FreePathU8(const nfdu8char_t* filePath)
__attribute__((alias("NFD_PathSet_FreePathN")));
void NFD_PathSet_Free(const nfdpathset_t* pathSet) {
assert(pathSet);
// const_cast because methods on GSList aren't const, but it should act
// like const to the caller
GSList* fileList = const_cast<GSList*>(static_cast<const GSList*>(pathSet));
// free all the nodes
for (GSList* node = fileList; node; node = node->next) {
assert(node->data);
g_free(node->data);
}
// free the path set memory
g_slist_free(fileList);
}
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) {
// The pathset (GSList) is already a linked list, so the enumeration is itself
outEnumerator->ptr = const_cast<void*>(pathSet);
return NFD_OKAY;
}
void NFD_PathSet_FreeEnum(nfdpathsetenum_t*) {
// Do nothing, because the enumeration is the pathset itself
}
nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) {
const GSList* fileList = static_cast<const GSList*>(enumerator->ptr);
if (fileList) {
*outPath = static_cast<nfdnchar_t*>(fileList->data);
enumerator->ptr = static_cast<void*>(fileList->next);
} else {
*outPath = nullptr;
}
return NFD_OKAY;
}
nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath)
__attribute__((alias("NFD_PathSet_EnumNextN")));
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
if(${NFD_BUILD_TESTS})
set(TEST_LIST
test_opendialog.c
test_opendialog_cpp.cpp
test_opendialog_native.c
test_opendialog_with.c
test_opendialog_native_with.c
test_opendialogmultiple.c
test_opendialogmultiple_cpp.cpp
test_opendialogmultiple_native.c
test_opendialogmultiple_enum.c
test_opendialogmultiple_enum_native.c
test_pickfolder.c
test_pickfolder_cpp.cpp
test_pickfolder_native.c
test_pickfolder_with.c
test_pickfolder_native_with.c
test_pickfoldermultiple.c
test_pickfoldermultiple_native.c
test_savedialog.c
test_savedialog_native.c
test_savedialog_with.c
test_savedialog_native_with.c)
foreach (TEST ${TEST_LIST})
string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST})
add_executable(${CLEAN_TEST_NAME}
${TEST})
target_link_libraries(${CLEAN_TEST_NAME}
PRIVATE nfd)
endforeach()
endif()
if(${NFD_BUILD_SDL2_TESTS})
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED sdl2 SDL2_ttf)
if(WIN32)
add_executable(test_sdl2 WIN32 test_sdl.c test_sdl.manifest)
else()
add_executable(test_sdl2 test_sdl.c)
endif()
target_link_libraries(test_sdl2 PRIVATE nfd)
target_include_directories(test_sdl2 PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(test_sdl2 PRIVATE ${SDL2_LINK_LIBRARIES})
target_compile_options(test_sdl2 PUBLIC ${SDL2_CFLAGS_OTHER})
endif()
@@ -0,0 +1,36 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
puts(outPath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,29 @@
#include <iostream>
#include "nfd.hpp"
/* this test should compile on all supported platforms */
/* this demonstrates the thin C++ wrapper */
int main() {
// initialize NFD
NFD::Guard nfdGuard;
// auto-freeing memory
NFD::UniquePath outPath;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD::OpenDialog(outPath, filterItem, 2);
if (result == NFD_OKAY) {
std::cout << "Success!" << std::endl << outPath.get() << std::endl;
} else if (result == NFD_CANCEL) {
std::cout << "User pressed cancel." << std::endl;
} else {
std::cout << "Error: " << NFD::GetError() << std::endl;
}
// NFD::Guard will automatically quit NFD.
return 0;
}
@@ -0,0 +1,49 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
// show the dialog
nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(outPath);
#else
fputws(outPath, stdin);
#endif
#else
puts(outPath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,52 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
// show the dialog
nfdopendialognargs_t args = {0};
args.filterList = filterItem;
args.filterCount = 2;
nfdresult_t result = NFD_OpenDialogN_With(&outPath, &args);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(outPath);
#else
fputws(outPath, stdin);
#endif
#else
puts(outPath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,39 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdopendialogu8args_t args = {0};
args.filterList = filterItem;
args.filterCount = 2;
nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);
if (result == NFD_OKAY) {
puts("Success!");
puts(outPath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,50 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD_OpenDialogMultiple(&outPaths, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
nfdpathsetsize_t numPaths;
NFD_PathSet_GetCount(outPaths, &numPaths);
nfdpathsetsize_t i;
for (i = 0; i < numPaths; ++i) {
nfdchar_t* path;
NFD_PathSet_GetPath(outPaths, i, &path);
printf("Path %i: %s\n", (int)i, path);
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,40 @@
#include "nfd.hpp"
#include <iostream>
/* this test should compile on all supported platforms */
/* this demonstrates the thin C++ wrapper */
int main() {
// initialize NFD
NFD::Guard nfdGuard;
// auto-freeing memory
NFD::UniquePathSet outPaths;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD::OpenDialogMultiple(outPaths, filterItem, 2);
if (result == NFD_OKAY) {
std::cout << "Success!" << std::endl;
nfdpathsetsize_t numPaths;
NFD::PathSet::Count(outPaths, numPaths);
nfdpathsetsize_t i;
for (i = 0; i < numPaths; ++i) {
NFD::UniquePathSetPath path;
NFD::PathSet::GetPath(outPaths, i, path);
std::cout << "Path " << i << ": " << path.get() << std::endl;
}
} else if (result == NFD_CANCEL) {
std::cout << "User pressed cancel." << std::endl;
} else {
std::cout << "Error: " << NFD::GetError() << std::endl;
}
// NFD::Guard will automatically quit NFD.
return 0;
}
@@ -0,0 +1,53 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD_OpenDialogMultiple(&outPaths, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
// declare enumerator (not a pointer)
nfdpathsetenum_t enumerator;
NFD_PathSet_GetEnum(outPaths, &enumerator);
nfdchar_t* path;
unsigned i = 0;
while (NFD_PathSet_EnumNext(&enumerator, &path) && path) {
printf("Path %u: %s\n", i++, path);
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset enumerator memory (before freeing the pathset)
NFD_PathSet_FreeEnum(&enumerator);
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,62 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
// show the dialog
nfdresult_t result = NFD_OpenDialogMultiple(&outPaths, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
// declare enumerator (not a pointer)
nfdpathsetenum_t enumerator;
NFD_PathSet_GetEnum(outPaths, &enumerator);
nfdchar_t* path;
unsigned i = 0;
while (NFD_PathSet_EnumNext(&enumerator, &path) && path) {
#ifdef _WIN32
wprintf(L"Path %u: %s\n", i++, path);
#else
printf("Path %u: %s\n", i++, path);
#endif
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset enumerator memory (before freeing the pathset)
NFD_PathSet_FreeEnum(&enumerator);
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,59 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
// show the dialog
nfdresult_t result = NFD_OpenDialogMultiple(&outPaths, filterItem, 2, NULL);
if (result == NFD_OKAY) {
puts("Success!");
nfdpathsetsize_t numPaths;
NFD_PathSet_GetCount(outPaths, &numPaths);
nfdpathsetsize_t i;
for (i = 0; i < numPaths; ++i) {
nfdchar_t* path;
NFD_PathSet_GetPath(outPaths, i, &path);
#ifdef _WIN32
wprintf(L"Path %i: %s\n", (int)i, path);
#else
printf("Path %i: %s\n", (int)i, path);
#endif
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,33 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// show the dialog
nfdresult_t result = NFD_PickFolder(&outPath, NULL);
if (result == NFD_OKAY) {
puts("Success!");
puts(outPath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,27 @@
#include "nfd.hpp"
#include <iostream>
/* this test should compile on all supported platforms */
/* this demonstrates the thin C++ wrapper */
int main() {
// initialize NFD
NFD::Guard nfdGuard;
// auto-freeing memory
NFD::UniquePath outPath;
// show the dialog
nfdresult_t result = NFD::PickFolder(outPath);
if (result == NFD_OKAY) {
std::cout << "Success!" << std::endl << outPath.get() << std::endl;
} else if (result == NFD_CANCEL) {
std::cout << "User pressed cancel." << std::endl;
} else {
std::cout << "Error: " << NFD::GetError() << std::endl;
}
// NFD::Guard will automatically quit NFD.
return 0;
}
@@ -0,0 +1,42 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// show the dialog
nfdresult_t result = NFD_PickFolder(&outPath, NULL);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(outPath);
#else
fputws(outPath, stdin);
#endif
#else
puts(outPath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,43 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// show the dialog
nfdpickfoldernargs_t args = {0};
nfdresult_t result = NFD_PickFolderN_With(&outPath, &args);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(outPath);
#else
fputws(outPath, stdin);
#endif
#else
puts(outPath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,34 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* outPath;
// show the dialog
nfdpickfolderu8args_t args = {0};
nfdresult_t result = NFD_PickFolderU8_With(&outPath, &args);
if (result == NFD_OKAY) {
puts("Success!");
puts(outPath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(outPath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,47 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// show the dialog
nfdresult_t result = NFD_PickFolderMultiple(&outPaths, NULL);
if (result == NFD_OKAY) {
puts("Success!");
nfdpathsetsize_t numPaths;
NFD_PathSet_GetCount(outPaths, &numPaths);
nfdpathsetsize_t i;
for (i = 0; i < numPaths; ++i) {
nfdchar_t* path;
NFD_PathSet_GetPath(outPaths, i, &path);
printf("Path %i: %s\n", (int)i, path);
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,52 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
const nfdpathset_t* outPaths;
// show the dialog
nfdresult_t result = NFD_PickFolderMultiple(&outPaths, NULL);
if (result == NFD_OKAY) {
puts("Success!");
nfdpathsetsize_t numPaths;
NFD_PathSet_GetCount(outPaths, &numPaths);
nfdpathsetsize_t i;
for (i = 0; i < numPaths; ++i) {
nfdchar_t* path;
NFD_PathSet_GetPath(outPaths, i, &path);
#ifdef _WIN32
wprintf(L"Path %i: %s\n", (int)i, path);
#else
printf("Path %i: %s\n", (int)i, path);
#endif
// remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!)
NFD_PathSet_FreePath(path);
}
// remember to free the pathset memory (since NFD_OKAY is returned)
NFD_PathSet_Free(outPaths);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,36 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* savePath;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Header", "h,hpp"}};
// show the dialog
nfdresult_t result = NFD_SaveDialog(&savePath, filterItem, 2, NULL, "Untitled.c");
if (result == NFD_OKAY) {
puts("Success!");
puts(savePath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(savePath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,55 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* savePath;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
#ifdef _WIN32
const wchar_t* defaultPath = L"Untitled.c";
#else
const char* defaultPath = "Untitled.c";
#endif
// show the dialog
nfdresult_t result = NFD_SaveDialog(&savePath, filterItem, 2, NULL, defaultPath);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(savePath);
#else
fputws(savePath, stdin);
#endif
#else
puts(savePath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(savePath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,59 @@
#define NFD_NATIVE
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* savePath;
// prepare filters for the dialog
#ifdef _WIN32
nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}};
#else
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
#endif
#ifdef _WIN32
const wchar_t* defaultPath = L"Untitled.c";
#else
const char* defaultPath = "Untitled.c";
#endif
// show the dialog
nfdsavedialognargs_t args = {0};
args.filterList = filterItem;
args.filterCount = 2;
args.defaultName = defaultPath;
nfdresult_t result = NFD_SaveDialogN_With(&savePath, &args);
if (result == NFD_OKAY) {
puts("Success!");
#ifdef _WIN32
#ifdef _MSC_VER
_putws(savePath);
#else
fputws(savePath, stdin);
#endif
#else
puts(savePath);
#endif
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(savePath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
@@ -0,0 +1,40 @@
#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>
/* this test should compile on all supported platforms */
int main(void) {
// initialize NFD
// either call NFD_Init at the start of your program and NFD_Quit at the end of your program,
// or before/after every time you want to show a file dialog.
NFD_Init();
nfdchar_t* savePath;
// prepare filters for the dialog
nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Header", "h,hpp"}};
// show the dialog
nfdsavedialogu8args_t args = {0};
args.filterList = filterItem;
args.filterCount = 2;
args.defaultName = "Untitled.c";
nfdresult_t result = NFD_SaveDialogU8_With(&savePath, &args);
if (result == NFD_OKAY) {
puts("Success!");
puts(savePath);
// remember to free the memory (since NFD_OKAY is returned)
NFD_FreePath(savePath);
} else if (result == NFD_CANCEL) {
puts("User pressed cancel.");
} else {
printf("Error: %s\n", NFD_GetError());
}
// Quit NFD
NFD_Quit();
return 0;
}
+414
View File
@@ -0,0 +1,414 @@
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include <SDL_ttf.h>
#include <nfd.h>
#include <nfd_sdl2.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
// Small program meant to demonstrate and test nfd_sdl2.h with SDL2. Note that it quits immediately
// when it encounters an error, without calling the opposite destroy/quit function. A real-world
// application should call destroy/quit appropriately.
void show_error(const char* message, SDL_Window* window) {
if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window) != 0) {
printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError());
return;
}
}
void show_path(const char* path, SDL_Window* window) {
if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", path, window) != 0) {
printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError());
return;
}
}
void show_paths(const nfdpathset_t* paths, SDL_Window* window) {
size_t num_chars = 0;
nfdpathsetsize_t num_paths;
if (NFD_PathSet_GetCount(paths, &num_paths) != NFD_OKAY) {
printf("NFD_PathSet_GetCount failed: %s\n", NFD_GetError());
return;
}
nfdpathsetsize_t i;
for (i = 0; i != num_paths; ++i) {
char* path;
if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) {
printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError());
return;
}
num_chars += strlen(path) + 1;
NFD_PathSet_FreePathU8(path);
}
// We should never return NFD_OKAY with zero paths, but GCC doesn't know this and will emit a
// warning that we're trying to malloc with size zero if we write the following line.
if (!num_paths) num_chars = 1;
char* message = malloc(num_chars);
message[0] = '\0';
for (i = 0; i != num_paths; ++i) {
if (i != 0) {
strcat(message, "\n");
}
char* path;
if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) {
printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError());
free(message);
return;
}
strcat(message, path);
NFD_PathSet_FreePathU8(path);
}
if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", message, window) != 0) {
printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError());
free(message);
return;
}
free(message);
}
void set_native_window(SDL_Window* sdlWindow, nfdwindowhandle_t* nativeWindow) {
if (!NFD_GetNativeWindowFromSDLWindow(sdlWindow, nativeWindow)) {
printf("NFD_GetNativeWindowFromSDLWindow failed: %s\n", SDL_GetError());
}
}
void opendialog_handler(SDL_Window* window) {
char* path;
nfdopendialogu8args_t args = {0};
set_native_window(window, &args.parentWindow);
const nfdresult_t res = NFD_OpenDialogU8_With(&path, &args);
switch (res) {
case NFD_OKAY:
show_path(path, window);
NFD_FreePathU8(path);
break;
case NFD_ERROR:
show_error(NFD_GetError(), window);
break;
default:
break;
}
}
void opendialogmultiple_handler(SDL_Window* window) {
const nfdpathset_t* paths;
nfdopendialogu8args_t args = {0};
set_native_window(window, &args.parentWindow);
const nfdresult_t res = NFD_OpenDialogMultipleU8_With(&paths, &args);
switch (res) {
case NFD_OKAY:
show_paths(paths, window);
NFD_PathSet_Free(paths);
break;
case NFD_ERROR:
show_error(NFD_GetError(), window);
break;
default:
break;
}
}
void savedialog_handler(SDL_Window* window) {
char* path;
nfdsavedialogu8args_t args = {0};
set_native_window(window, &args.parentWindow);
const nfdresult_t res = NFD_SaveDialogU8_With(&path, &args);
switch (res) {
case NFD_OKAY:
show_path(path, window);
NFD_FreePathU8(path);
break;
case NFD_ERROR:
show_error(NFD_GetError(), window);
break;
default:
break;
}
}
void pickfolder_handler(SDL_Window* window) {
char* path;
nfdpickfolderu8args_t args = {0};
set_native_window(window, &args.parentWindow);
const nfdresult_t res = NFD_PickFolderU8_With(&path, &args);
switch (res) {
case NFD_OKAY:
show_path(path, window);
NFD_FreePathU8(path);
break;
case NFD_ERROR:
show_error(NFD_GetError(), window);
break;
default:
break;
}
}
void pickfoldermultiple_handler(SDL_Window* window) {
const nfdpathset_t* paths;
nfdpickfolderu8args_t args = {0};
set_native_window(window, &args.parentWindow);
const nfdresult_t res = NFD_PickFolderMultipleU8_With(&paths, &args);
switch (res) {
case NFD_OKAY:
show_paths(paths, window);
NFD_PathSet_Free(paths);
break;
case NFD_ERROR:
show_error(NFD_GetError(), window);
break;
default:
break;
}
}
#if defined(_WIN32)
const char font_file[] = "C:\\Windows\\Fonts\\calibri.ttf";
#elif defined(__APPLE__)
const char font_file[] = "/System/Library/Fonts/SFNS.ttf";
#else
const char font_file[] = "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf";
#endif
#define NUM_STATES 3
#define NUM_BUTTONS 5
const char* button_text[NUM_BUTTONS] = {"Open File",
"Open Files",
"Save File",
"Select Folder",
"Select Folders"};
const int BUTTON_WIDTH = 400;
const int BUTTON_HEIGHT = 40;
void (*button_handler[NUM_BUTTONS])(SDL_Window*) = {&opendialog_handler,
&opendialogmultiple_handler,
&savedialog_handler,
&pickfolder_handler,
&pickfoldermultiple_handler};
#ifdef _WIN32
int WINAPI WinMain(void)
#else
int main(void)
#endif
{
#ifdef _WIN32
// Enable DPI awareness on Windows
SDL_SetHint("SDL_HINT_WINDOWS_DPI_AWARENESS", "permonitorv2");
SDL_SetHint("SDL_HINT_WINDOWS_DPI_SCALING", "1");
#endif
// initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
printf("SDL_Init failed: %s\n", SDL_GetError());
return 0;
}
// initialize SDL_ttf
if (TTF_Init() != 0) {
printf("TTF_Init failed: %s\n", TTF_GetError());
return 0;
}
// initialize NFD
if (NFD_Init() != NFD_OKAY) {
printf("NFD_Init failed: %s\n", NFD_GetError());
return 0;
}
// create window
SDL_Window* const window = SDL_CreateWindow("Welcome",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
BUTTON_WIDTH,
BUTTON_HEIGHT * NUM_BUTTONS,
SDL_WINDOW_ALLOW_HIGHDPI);
if (!window) {
printf("SDL_CreateWindow failed: %s\n", SDL_GetError());
return 0;
}
// create renderer
SDL_Renderer* const renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
printf("SDL_CreateRenderer failed: %s\n", SDL_GetError());
return 0;
}
// prepare the buttons and handlers
SDL_Texture* textures_normal[NUM_BUTTONS][NUM_STATES];
TTF_Font* const font = TTF_OpenFont(font_file, 20);
if (!font) {
printf("TTF_OpenFont failed: %s\n", TTF_GetError());
return 0;
}
const SDL_Color back_color[NUM_STATES] = {{0, 0, 0, SDL_ALPHA_OPAQUE},
{51, 51, 51, SDL_ALPHA_OPAQUE},
{102, 102, 102, SDL_ALPHA_OPAQUE}};
const SDL_Color text_color = {255, 255, 255, SDL_ALPHA_OPAQUE};
const uint8_t text_alpha[NUM_STATES] = {153, 204, 255};
for (size_t i = 0; i != NUM_BUTTONS; ++i) {
SDL_Surface* const text_surface = TTF_RenderUTF8_Blended(font, button_text[i], text_color);
if (!text_surface) {
printf("TTF_RenderUTF8_Blended failed: %s\n", TTF_GetError());
return 0;
}
if (SDL_SetSurfaceBlendMode(text_surface, SDL_BLENDMODE_BLEND) != 0) {
printf("SDL_SetSurfaceBlendMode failed: %s\n", SDL_GetError());
return 0;
}
for (size_t j = 0; j != NUM_STATES; ++j) {
SDL_Surface* button_surface =
SDL_CreateRGBSurface(0, BUTTON_WIDTH, BUTTON_HEIGHT, 32, 0, 0, 0, 0);
if (!button_surface) {
printf("SDL_CreateRGBSurface failed: %s\n", SDL_GetError());
return 0;
}
if (SDL_FillRect(button_surface,
NULL,
SDL_MapRGBA(button_surface->format,
back_color[j].r,
back_color[j].g,
back_color[j].b,
back_color[j].a)) != 0) {
printf("SDL_FillRect failed: %s\n", SDL_GetError());
return 0;
}
SDL_SetSurfaceAlphaMod(text_surface, text_alpha[j]);
SDL_Rect dstrect = {(BUTTON_WIDTH - text_surface->w) / 2,
(BUTTON_HEIGHT - text_surface->h) / 2,
text_surface->w,
text_surface->h};
if (SDL_BlitSurface(text_surface, NULL, button_surface, &dstrect) != 0) {
printf("SDL_BlitSurface failed: %s\n", SDL_GetError());
return 0;
}
SDL_Texture* const texture = SDL_CreateTextureFromSurface(renderer, button_surface);
if (!texture) {
printf("SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError());
return 0;
}
SDL_FreeSurface(button_surface);
textures_normal[i][j] = texture;
}
SDL_FreeSurface(text_surface);
}
TTF_CloseFont(font);
// event loop
bool quit = false;
size_t button_index = (size_t)-1;
bool pressed = false;
do {
// render
for (size_t i = 0; i != NUM_BUTTONS; ++i) {
const SDL_Rect rect = {0, (int)i * BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT};
SDL_RenderCopy(
renderer, textures_normal[i][button_index == i ? pressed ? 2 : 1 : 0], NULL, &rect);
}
SDL_RenderPresent(renderer);
// process events
SDL_Event event;
if (SDL_WaitEvent(&event) == 0) {
printf("SDL_WaitEvent failed: %s\n", SDL_GetError());
return 0;
}
do {
switch (event.type) {
case SDL_QUIT: {
quit = true;
break;
}
case SDL_WINDOWEVENT: {
switch (event.window.event) {
case SDL_WINDOWEVENT_CLOSE:
quit = true;
break;
case SDL_WINDOWEVENT_LEAVE:
button_index = (size_t)-1;
break;
}
break;
}
case SDL_MOUSEMOTION: {
if (event.motion.x < 0 || event.motion.x >= BUTTON_WIDTH ||
event.motion.y < 0) {
button_index = (size_t)-1;
break;
}
const int index = event.motion.y / BUTTON_HEIGHT;
if (index < 0 || index >= NUM_BUTTONS) {
button_index = (size_t)-1;
break;
}
button_index = index;
pressed = event.motion.state & SDL_BUTTON(1);
break;
}
case SDL_MOUSEBUTTONDOWN: {
if (event.button.button == 1) {
pressed = true;
}
break;
}
case SDL_MOUSEBUTTONUP: {
if (event.button.button == 1) {
pressed = false;
if (button_index != (size_t)-1) {
(*button_handler[button_index])(window);
}
}
break;
}
}
} while (SDL_PollEvent(&event) != 0);
} while (!quit);
// destroy textures
for (size_t i = 0; i != NUM_BUTTONS; ++i) {
for (size_t j = 0; j != NUM_STATES; ++j) {
SDL_DestroyTexture(textures_normal[i][j]);
}
}
// destroy renderer
SDL_DestroyRenderer(renderer);
// destroy window
SDL_DestroyWindow(window);
// quit NFD
NFD_Quit();
// quit SDL_ttf
TTF_Quit();
// quit SDL
SDL_Quit();
return 0;
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="CompanyName.ProductName.YourApp"
type="win32"
/>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<description>Example application for NFDe.</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
+21 -5
View File
@@ -28,6 +28,8 @@ set(ozz_build_simd_ref OFF CACHE BOOL "")
set(ozz_build_msvc_rt_dll OFF CACHE BOOL "") set(ozz_build_msvc_rt_dll OFF CACHE BOOL "")
add_subdirectory(3rdparty/ozz-animation) add_subdirectory(3rdparty/ozz-animation)
add_subdirectory(3rdparty/nativefiledialog-extended)
set(ThirdPartyIncludeDeps set(ThirdPartyIncludeDeps
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/glfw/deps> PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/glfw/deps>
@@ -43,8 +45,6 @@ set(ThirdPartyIncludeDeps
# Shared code by main executable and tests # Shared code by main executable and tests
add_library(AnimTestbedCode OBJECT add_library(AnimTestbedCode OBJECT
src/SyncTrack.cc
src/SyncTrack.h
src/ozzutils.cc src/ozzutils.cc
src/AnimGraph/AnimGraphNodes.cc src/AnimGraph/AnimGraphNodes.cc
src/AnimGraph/AnimGraphNodes.h src/AnimGraph/AnimGraphNodes.h
@@ -57,7 +57,11 @@ add_library(AnimTestbedCode OBJECT
src/AnimGraph/AnimNode.cc src/AnimGraph/AnimNode.cc
src/AnimGraph/AnimNode.h src/AnimGraph/AnimNode.h
src/AnimGraph/AnimGraphResource.cc src/AnimGraph/AnimGraphResource.cc
src/AnimGraph/AnimGraphResource.h) src/AnimGraph/AnimGraphResource.h
src/AnimGraph/SyncTrack.cc
src/AnimGraph/SyncTrack.h
src/AnimGraph/AnimLibrary.h
)
target_include_directories( target_include_directories(
AnimTestbedCode AnimTestbedCode
@@ -121,7 +125,16 @@ target_sources(AnimTestbed PRIVATE
3rdparty/imgui/imgui_stacklayout_internal.h 3rdparty/imgui/imgui_stacklayout_internal.h
) )
target_link_libraries(AnimTestbed AnimTestbedCode glfw ozz_base ozz_geometry ozz_animation ${OPENGL_LIBRARIES}) target_link_libraries(AnimTestbed
PUBLIC
AnimTestbedCode
glfw
ozz_base
ozz_geometry
ozz_animation
${OPENGL_LIBRARIES}
PRIVATE
nfd)
# Tests # Tests
add_executable(runtests) add_executable(runtests)
@@ -136,9 +149,12 @@ set(ozz_offline_test_objs
target_sources(runtests PRIVATE target_sources(runtests PRIVATE
tests/AnimGraphResourceTests.cc tests/AnimGraphResourceTests.cc
tests/AnimGraphEditorTests.cc tests/AnimGraphEditorTests.cc
# tests/AnimGraphEvalTests.cc tests/AnimGraphEvalTests.cc
tests/NodeDescriptorTests.cc tests/NodeDescriptorTests.cc
tests/SyncTrackTests.cc tests/SyncTrackTests.cc
tests/AnimDataTests.cc
tests/TestAnimData.cc
tests/TestAnimData.h
tests/main.cc tests/main.cc
${ozz_offline_test_objs} ${ozz_offline_test_objs}
) )
+7
View File
@@ -0,0 +1,7 @@
# AnimTestbed
A yet to be named project that implements an animation engine based on hierarchical blend trees.
# Dependencies
apt-get install libgtk-3-dev libxapp-gtk3-module
+35
View File
@@ -0,0 +1,35 @@
## Graph
### 1. Support of math nodes (or non-AnimNodes in general)
* Enables animators to add custom math for blend inputs or to adjust other inputs (e.g. LookAt or IK
targets).
**Open Issues**
1. When to do the evaluation? Two types of subgraphs:
a) Instant inputs (needed for blend node inputs) that have to be evaluated before
UpdateConnections
b) Processing nodes, e.g. for extracted bones.
### 2. Support of multiple output sockets
* E.g. extract Bone transform
* Increases Node complexity:
* AnimOutput
* AnimOutput + Data
* Data
(Data = bool, float, vec3, quat, ...)
**Open Issues**
1. Unclear when this is actually needed. Using more specific nodes that perform the desired logic
may be better (
c.f. https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprint-bone-driven-controller-in-unreal-engine).
Likely this is not crucial so should be avoided for now.
### 3. Multi-skeleton evaluation
Use case: riding on a horse, interaction between two characters.
+2 -2
View File
@@ -15,9 +15,9 @@ bool AnimGraphBlendTree::Init(AnimGraphContext& context) {
} }
} }
for (size_t i = 0; i < m_animdata_blocks.size(); i++) { for (size_t i = 0; i < m_pose_blocks.size(); i++) {
int num_soa_joints = context.m_skeleton->num_soa_joints(); int num_soa_joints = context.m_skeleton->num_soa_joints();
m_animdata_blocks[i]->m_local_matrices.resize(num_soa_joints); m_pose_blocks[i]->m_local_matrices.resize(num_soa_joints);
} }
return true; return true;
+27 -12
View File
@@ -6,6 +6,7 @@
#define ANIMTESTBED_ANIMGRAPHBLENDTREE_H #define ANIMTESTBED_ANIMGRAPHBLENDTREE_H
#include <algorithm> #include <algorithm>
#include <iostream>
#include "AnimNode.h" #include "AnimNode.h"
@@ -13,8 +14,6 @@
// AnimGraph (Runtime) // AnimGraph (Runtime)
// //
struct AnimGraphBlendTree : public AnimNode { struct AnimGraphBlendTree : public AnimNode {
AnimData m_local_transforms;
std::vector<AnimNode*> m_nodes; std::vector<AnimNode*> m_nodes;
std::vector<AnimNode*> m_eval_ordered_nodes; std::vector<AnimNode*> m_eval_ordered_nodes;
std::vector<std::vector<AnimGraphConnection> > m_node_input_connections; std::vector<std::vector<AnimGraphConnection> > m_node_input_connections;
@@ -30,15 +29,17 @@ struct AnimGraphBlendTree : public AnimNode {
return m_node_input_connections[0]; return m_node_input_connections[0];
} }
std::vector<AnimData*> m_animdata_blocks; std::vector<Pose*> m_pose_blocks;
NodeDescriptorBase* m_node_descriptor = nullptr; NodeDescriptorBase* m_node_descriptor = nullptr;
char* m_input_buffer = nullptr; char* m_input_buffer = nullptr;
char* m_output_buffer = nullptr; char* m_output_buffer = nullptr;
char* m_connection_data_storage = nullptr; char* m_connection_data_storage = nullptr;
char* m_const_node_inputs = nullptr; char* m_const_node_inputs = nullptr;
std::vector<Socket>& GetGraphOutputs() { return m_node_descriptor->m_inputs; } std::vector<Socket>& GetGraphOutputs() {
std::vector<Socket>& GetGraphInputs() { return m_node_descriptor->m_outputs; } return m_node_descriptor->m_outputs;
}
std::vector<Socket>& GetGraphInputs() { return m_node_descriptor->m_inputs; }
AnimDataAllocator m_anim_data_allocator; AnimDataAllocator m_anim_data_allocator;
@@ -48,6 +49,13 @@ struct AnimGraphBlendTree : public AnimNode {
// AnimNode overrides // AnimNode overrides
bool Init(AnimGraphContext& context) override; bool Init(AnimGraphContext& context) override;
/// Determines which nodes in the BlendTree are active.
///
/// Note: this does not use the provided input_connections, instead it marks
/// all nodes directly connected to the BlendTree outputs as active and then
/// propagates the node state throught the tree. For this each active node's
/// AnimNode::MarkActiveInputs() gets called.
void MarkActiveInputs( void MarkActiveInputs(
const std::vector<AnimGraphConnection>& input_connections) override; const std::vector<AnimGraphConnection>& input_connections) override;
void CalcSyncTrack( void CalcSyncTrack(
@@ -58,10 +66,10 @@ struct AnimGraphBlendTree : public AnimNode {
void PropagateTimeToNodeInputs(const AnimNode* node); void PropagateTimeToNodeInputs(const AnimNode* node);
void dealloc() { void dealloc() {
for (size_t i = 0; i < m_animdata_blocks.size(); i++) { for (size_t i = 0; i < m_pose_blocks.size(); i++) {
m_animdata_blocks[i]->m_local_matrices.vector::~vector(); m_pose_blocks[i]->m_local_matrices.vector::~vector();
} }
m_animdata_blocks.clear(); m_pose_blocks.clear();
m_node_input_connections.clear(); m_node_input_connections.clear();
m_node_output_connections.clear(); m_node_output_connections.clear();
@@ -93,7 +101,7 @@ struct AnimGraphBlendTree : public AnimNode {
} }
} }
/** Sets the address that is used for the specified AnimGraph input Socket. /** Sets the address that is used for the specified BlendTree input Socket.
* *
* @tparam T Type of the Socket. * @tparam T Type of the Socket.
* @param name Name of the Socket. * @param name Name of the Socket.
@@ -101,7 +109,7 @@ struct AnimGraphBlendTree : public AnimNode {
*/ */
template <typename T> template <typename T>
void SetInput(const char* name, T* value_ptr) { void SetInput(const char* name, T* value_ptr) {
m_node_descriptor->SetOutput(name, value_ptr); m_node_descriptor->SetInput(name, value_ptr);
std::vector<size_t> connected_node_indices; std::vector<size_t> connected_node_indices;
@@ -115,14 +123,14 @@ struct AnimGraphBlendTree : public AnimNode {
} }
} }
/** Sets the address that is used for the specified AnimGraph output Socket. /** Sets the address that is used for the specified BlendTree output Socket.
* *
* We update the pointer of the outputting node. We also have to ensure that * We update the pointer of the outputting node. We also have to ensure that
* all usages of that output use the same pointer. * all usages of that output use the same pointer.
* *
* @tparam T Type of the Socket. * @tparam T Type of the Socket.
* @param name Name of the Socket. * @param name Name of the Socket.
* @param value_ptr Pointer where the graph output output is written to at the end of evaluation. * @param value_ptr Pointer where the graph output is written to at the end of evaluation.
*/ */
template <typename T> template <typename T>
void SetOutput(const char* name, T* value_ptr) { void SetOutput(const char* name, T* value_ptr) {
@@ -148,6 +156,7 @@ struct AnimGraphBlendTree : public AnimNode {
return; return;
} }
// Ensure that all connections that consume the output use the updated address.
size_t output_node_index = size_t output_node_index =
GetAnimNodeIndex(graph_output_connection->m_source_node); GetAnimNodeIndex(graph_output_connection->m_source_node);
@@ -161,6 +170,12 @@ struct AnimGraphBlendTree : public AnimNode {
} }
*graph_output_connection->m_socket.m_reference.ptr_ptr = value_ptr; *graph_output_connection->m_socket.m_reference.ptr_ptr = value_ptr;
// And additionally update the BlendTree's node descriptor:
Socket* blend_tree_output_socket = m_node_descriptor->GetOutputSocket(name);
assert(blend_tree_output_socket != nullptr);
*blend_tree_output_socket->m_reference.ptr_ptr = value_ptr;
} }
/** Returns the address that is used for the specified AnimGraph output Socket. /** Returns the address that is used for the specified AnimGraph output Socket.
+50 -20
View File
@@ -8,7 +8,6 @@
#include <ozz/base/maths/soa_transform.h> #include <ozz/base/maths/soa_transform.h>
#include <cstring> #include <cstring>
#include <iostream>
#include <list> #include <list>
#include <map> #include <map>
#include <string> #include <string>
@@ -25,26 +24,53 @@
struct AnimGraph; struct AnimGraph;
struct AnimNode; struct AnimNode;
struct AnimData { struct Pose {
ozz::vector<ozz::math::SoaTransform> m_local_matrices; ozz::vector<ozz::math::SoaTransform> m_local_matrices;
}; };
struct AnimDataRef { struct AnimDataRef {
AnimData* ptr = nullptr; Pose* ptr = nullptr;
}; };
struct AnimationResource {
std::string m_name;
std::string m_filename;
ozz::animation::Animation* m_animation;
SyncTrack m_sync_track;
};
inline void to_json(
nlohmann::json& j,
const AnimationResource& animation_resource) {
j["type"] = "AnimationResource";
j["name"] = animation_resource.m_name;
j["filename"] = animation_resource.m_filename;
j["synctrack"] = animation_resource.m_sync_track;
}
inline void from_json(
const nlohmann::json& j,
AnimationResource& animation_resource) {
assert(j["type"] == "AnimationResource");
animation_resource.m_name = j["name"];
animation_resource.m_filename = j["filename"];
animation_resource.m_sync_track = j["synctrack"];
}
struct AnimDataAllocator { struct AnimDataAllocator {
struct AnimDataList { struct PoseList {
AnimData* m_anim_data = nullptr; Pose* m_anim_data = nullptr;
AnimDataList* next = nullptr; PoseList* next = nullptr;
}; };
std::list<AnimData*> m_anim_data_list; std::list<Pose*> m_anim_data_list;
size_t m_num_allocations = 0; size_t m_num_allocations = 0;
~AnimDataAllocator() { ~AnimDataAllocator() {
while (!m_anim_data_list.empty()) { while (!m_anim_data_list.empty()) {
AnimData* front = m_anim_data_list.front(); Pose* front = m_anim_data_list.front();
#ifdef ANIM_DATA_ALLOCATOR_DEBUG #ifdef ANIM_DATA_ALLOCATOR_DEBUG
std::cout << "about to delete with size " std::cout << "about to delete with size "
<< front->m_anim_data->m_local_matrices.size() << front->m_anim_data->m_local_matrices.size()
@@ -55,9 +81,9 @@ struct AnimDataAllocator {
} }
} }
AnimData* allocate(ozz::animation::Skeleton* skeleton) { Pose* allocate(ozz::animation::Skeleton* skeleton) {
if (m_anim_data_list.empty()) { if (m_anim_data_list.empty()) {
AnimData* result = new AnimData(); Pose* result = new Pose();
result->m_local_matrices.resize(skeleton->num_soa_joints()); result->m_local_matrices.resize(skeleton->num_soa_joints());
#ifdef ANIM_DATA_ALLOCATOR_DEBUG #ifdef ANIM_DATA_ALLOCATOR_DEBUG
std::cout << "Allocated with size " << result->m_local_matrices.size() std::cout << "Allocated with size " << result->m_local_matrices.size()
@@ -67,7 +93,7 @@ struct AnimDataAllocator {
return result; return result;
} }
AnimData* result = m_anim_data_list.front(); Pose* result = m_anim_data_list.front();
m_anim_data_list.pop_front(); m_anim_data_list.pop_front();
#ifdef ANIM_DATA_ALLOCATOR_DEBUG #ifdef ANIM_DATA_ALLOCATOR_DEBUG
@@ -78,7 +104,7 @@ struct AnimDataAllocator {
return result; return result;
} }
void free(AnimData* anim_data) { void free(Pose* anim_data) {
#ifdef ANIM_DATA_ALLOCATOR_DEBUG #ifdef ANIM_DATA_ALLOCATOR_DEBUG
std::cout << "Storing buffer with size " std::cout << "Storing buffer with size "
<< anim_data->m_local_matrices.size() << " " << anim_data << anim_data->m_local_matrices.size() << " " << anim_data
@@ -91,18 +117,27 @@ struct AnimDataAllocator {
size_t size() { return m_anim_data_list.size(); } size_t size() { return m_anim_data_list.size(); }
}; };
enum class AnimGraphType {
GraphTypeUndefined = 0,
GraphTypeBlendTree,
GraphTypeStateMachine,
GraphTypeLast
};
/*** Defines data on which an animation graph is executed (i.e. skeleton, animations).
*/
struct AnimGraphContext { struct AnimGraphContext {
AnimGraph* m_graph = nullptr; AnimGraph* m_graph = nullptr;
ozz::animation::Skeleton* m_skeleton = nullptr; ozz::animation::Skeleton* m_skeleton = nullptr;
typedef std::map<std::string, ozz::animation::Animation*> AnimationFileMap; typedef std::map<std::string, AnimationResource> AnimationFileMap;
AnimationFileMap m_animation_map; AnimationFileMap m_animation_map;
void freeAnimations() { void freeAnimations() {
AnimationFileMap::iterator animation_map_iter = m_animation_map.begin(); AnimationFileMap::iterator animation_map_iter = m_animation_map.begin();
while (animation_map_iter != m_animation_map.end()) { while (animation_map_iter != m_animation_map.end()) {
delete animation_map_iter->second; delete animation_map_iter->second.m_animation;
animation_map_iter++; animation_map_iter++;
} }
} }
@@ -231,7 +266,7 @@ SocketType GetSocketType() {
return SocketType::SocketTypeBool; return SocketType::SocketTypeBool;
} }
if constexpr (std::is_same<T, AnimData>::value) { if constexpr (std::is_same<T, Pose>::value) {
return SocketType::SocketTypeAnimation; return SocketType::SocketTypeAnimation;
} }
@@ -366,11 +401,6 @@ struct NodeDescriptorBase {
*socket->m_reference.ptr_ptr = value_ptr; *socket->m_reference.ptr_ptr = value_ptr;
} }
void SetOutputUnchecked(const char* name, void* value_ptr) {
Socket* socket = FindSocket(name, m_outputs);
*socket->m_reference.ptr_ptr = value_ptr;
}
Socket* GetOutputSocket(const char* name) const { Socket* GetOutputSocket(const char* name) const {
return FindSocket(name, m_outputs); return FindSocket(name, m_outputs);
} }
+7 -2
View File
@@ -132,7 +132,8 @@ bool AnimSamplerNode::Init(AnimGraphContext& context) {
AnimGraphContext::AnimationFileMap::const_iterator animation_map_iter; AnimGraphContext::AnimationFileMap::const_iterator animation_map_iter;
animation_map_iter = context.m_animation_map.find(m_filename); animation_map_iter = context.m_animation_map.find(m_filename);
if (animation_map_iter != context.m_animation_map.end()) { if (animation_map_iter != context.m_animation_map.end()) {
m_animation = animation_map_iter->second; m_animation = animation_map_iter->second.m_animation;
m_sync_track = animation_map_iter->second.m_sync_track;
} else { } else {
m_animation = new ozz::animation::Animation(); m_animation = new ozz::animation::Animation();
ozz::io::File file(m_filename.c_str(), "rb"); ozz::io::File file(m_filename.c_str(), "rb");
@@ -150,7 +151,11 @@ bool AnimSamplerNode::Init(AnimGraphContext& context) {
archive >> *m_animation; archive >> *m_animation;
context.m_animation_map[m_filename] = m_animation; context.m_animation_map[m_filename] = {
m_filename,
m_filename,
m_animation,
SyncTrack()};
} }
assert(context.m_skeleton != nullptr); assert(context.m_skeleton != nullptr);
+24 -10
View File
@@ -18,9 +18,9 @@ struct AnimNode;
// Blend2Node // Blend2Node
// //
struct Blend2Node : public AnimNode { struct Blend2Node : public AnimNode {
AnimData* i_input0 = nullptr; Pose* i_input0 = nullptr;
AnimData* i_input1 = nullptr; Pose* i_input1 = nullptr;
AnimData* o_output = nullptr; Pose* o_output = nullptr;
float* i_blend_weight = nullptr; float* i_blend_weight = nullptr;
bool m_sync_blend = false; bool m_sync_blend = false;
@@ -33,12 +33,12 @@ struct Blend2Node : public AnimNode {
} }
if (input.m_target_socket_name == "Input0" && *i_blend_weight < 0.999) { if (input.m_target_socket_name == "Input0" && *i_blend_weight < 0.999) {
input_node->m_state = AnimNodeEvalState::Activated; input_node->Activate(m_tick_number);
continue; continue;
} }
if (input.m_target_socket_name == "Input1" && *i_blend_weight > 0.001) { if (input.m_target_socket_name == "Input1" && *i_blend_weight > 0.001) {
input_node->m_state = AnimNodeEvalState::Activated; input_node->Activate(m_tick_number);
continue; continue;
} }
} }
@@ -59,6 +59,8 @@ struct NodeDescriptor<Blend2Node> : public NodeDescriptorBase {
RegisterProperty("Sync", &node->m_sync_blend); RegisterProperty("Sync", &node->m_sync_blend);
} }
virtual ~NodeDescriptor() = default;
void UpdateFlags() override { void UpdateFlags() override {
Socket* weight_input_socket = FindSocket("Weight", m_inputs); Socket* weight_input_socket = FindSocket("Weight", m_inputs);
assert(weight_input_socket != nullptr); assert(weight_input_socket != nullptr);
@@ -75,8 +77,8 @@ struct NodeDescriptor<Blend2Node> : public NodeDescriptorBase {
// SpeedScaleNode // SpeedScaleNode
// //
struct SpeedScaleNode : public AnimNode { struct SpeedScaleNode : public AnimNode {
AnimData* i_input = nullptr; Pose* i_input = nullptr;
AnimData* o_output = nullptr; Pose* o_output = nullptr;
float* i_speed_scale = nullptr; float* i_speed_scale = nullptr;
void UpdateTime(float time_last, float time_now) override { void UpdateTime(float time_last, float time_now) override {
@@ -104,13 +106,15 @@ struct NodeDescriptor<SpeedScaleNode> : public NodeDescriptorBase {
RegisterOutput("Output", &node->o_output); RegisterOutput("Output", &node->o_output);
} }
virtual ~NodeDescriptor() = default;
}; };
// //
// AnimSamplerNode // AnimSamplerNode
// //
struct AnimSamplerNode : public AnimNode { struct AnimSamplerNode : public AnimNode {
AnimData* o_output = nullptr; Pose* o_output = nullptr;
std::string m_filename; std::string m_filename;
ozz::animation::SamplingJob::Context m_sampling_context; ozz::animation::SamplingJob::Context m_sampling_context;
ozz::animation::Animation* m_animation = nullptr; ozz::animation::Animation* m_animation = nullptr;
@@ -132,14 +136,16 @@ struct NodeDescriptor<AnimSamplerNode> : public NodeDescriptorBase {
RegisterProperty("Filename", &node->m_filename); RegisterProperty("Filename", &node->m_filename);
} }
virtual ~NodeDescriptor() = default;
}; };
// //
// LockTranslationNode // LockTranslationNode
// //
struct LockTranslationNode : public AnimNode { struct LockTranslationNode : public AnimNode {
AnimData* i_input = nullptr; Pose* i_input = nullptr;
AnimData* o_output = nullptr; Pose* o_output = nullptr;
int m_locked_bone_index = 0; int m_locked_bone_index = 0;
bool m_lock_x = false; bool m_lock_x = false;
bool m_lock_y = false; bool m_lock_y = false;
@@ -159,6 +165,8 @@ struct NodeDescriptor<LockTranslationNode> : public NodeDescriptorBase {
RegisterProperty("LockAxisY", &node->m_lock_y); RegisterProperty("LockAxisY", &node->m_lock_y);
RegisterProperty("LockAxisZ", &node->m_lock_z); RegisterProperty("LockAxisZ", &node->m_lock_z);
} }
virtual ~NodeDescriptor() = default;
}; };
// //
@@ -177,6 +185,8 @@ struct NodeDescriptor<ConstScalarNode> : public NodeDescriptorBase {
RegisterOutput("ScalarOutput", &node->o_value); RegisterOutput("ScalarOutput", &node->o_value);
RegisterProperty("ScalarValue", &node->value); RegisterProperty("ScalarValue", &node->value);
} }
virtual ~NodeDescriptor() = default;
}; };
// //
@@ -202,6 +212,8 @@ struct NodeDescriptor<MathAddNode> : public NodeDescriptorBase {
RegisterInput("Input1", &node->i_input1); RegisterInput("Input1", &node->i_input1);
RegisterOutput("Output", &node->o_output); RegisterOutput("Output", &node->o_output);
} }
virtual ~NodeDescriptor() = default;
}; };
// //
@@ -232,6 +244,8 @@ struct NodeDescriptor<MathFloatToVec3Node> : public NodeDescriptorBase {
RegisterInput("Input2", &node->i_input2); RegisterInput("Input2", &node->i_input2);
RegisterOutput("Output", &node->o_output); RegisterOutput("Output", &node->o_output);
} }
virtual ~NodeDescriptor() = default;
}; };
AnimNode* AnimNodeFactory(const std::string& name); AnimNode* AnimNodeFactory(const std::string& name);
File diff suppressed because it is too large Load Diff
+174 -126
View File
@@ -5,14 +5,17 @@
#ifndef ANIMTESTBED_ANIMGRAPHRESOURCE_H #ifndef ANIMTESTBED_ANIMGRAPHRESOURCE_H
#define ANIMTESTBED_ANIMGRAPHRESOURCE_H #define ANIMTESTBED_ANIMGRAPHRESOURCE_H
#include <iostream>
#include "3rdparty/json/json.hpp" #include "3rdparty/json/json.hpp"
#include "AnimGraphNodes.h" #include "AnimGraphNodes.h"
struct AnimGraphBlendTree; struct AnimGraphBlendTree;
struct AnimGraphStateMachine; struct AnimGraphStateMachine;
struct BlendTreeResource;
struct AnimNodeResource { struct AnimNodeResource {
virtual ~AnimNodeResource() { delete m_virtual_socket_accessor; }; virtual ~AnimNodeResource() { delete m_virtual_socket_accessor; }
std::string m_name; std::string m_name;
std::string m_node_type_name; std::string m_node_type_name;
@@ -23,6 +26,30 @@ struct AnimNodeResource {
static inline AnimNodeResource* AnimNodeResourceFactory( static inline AnimNodeResource* AnimNodeResourceFactory(
const std::string& node_type_name); const std::string& node_type_name);
struct StateMachineTransitionResources {
size_t source_state_index = -1;
size_t target_state_index = -1;
float blend_time = 0.f;
bool sync_blend = false;
};
struct StateMachineResource {
std::vector<AnimNodeResource> m_states;
std::vector<StateMachineTransitionResources> m_transitions;
};
struct AnimGraphResource : AnimNodeResource {
~AnimGraphResource() override = default;
static constexpr char DefaultAnimOutput[] = "Output";
[[maybe_unused]] virtual bool SaveToFile(const char* filename) const = 0;
static AnimGraphResource* CreateFromFile(const char* filename);
};
typedef std::unique_ptr<AnimGraphResource> AnimGraphResourcePtr;
struct BlendTreeConnectionResource { struct BlendTreeConnectionResource {
int source_node_index = -1; int source_node_index = -1;
std::string source_socket_name; std::string source_socket_name;
@@ -39,14 +66,28 @@ struct BlendTreeConnectionResource {
} }
}; };
struct BlendTreeResource { struct BlendTreeResource : AnimGraphResource {
typedef std::pair<const AnimNodeResource*, std::string> NodeSocketNamePair;
typedef std::map<NodeSocketNamePair, int> NodeSocketDataOffsetMap;
std::vector<std::vector<size_t> > m_node_input_connection_indices; std::vector<std::vector<size_t> > m_node_input_connection_indices;
std::vector<std::vector<size_t> > m_node_inputs_subtree; std::vector<std::vector<size_t> > m_node_inputs_subtree;
~BlendTreeResource() { CleanupNodes(); } BlendTreeResource() {
m_virtual_socket_accessor = VirtualAnimNodeDescriptorFactory("BlendTree");
InitGraphConnectors();
RegisterBlendTreeOutputSocket<Pose>(AnimGraphResource::DefaultAnimOutput);
}
~BlendTreeResource() { ClearAllNodes(); }
[[maybe_unused]] bool SaveToFile(const char* filename) const override;
static BlendTreeResource* CreateFromFile(const char* filename);
void CreateBlendTreeInstance(AnimGraphBlendTree& result) const;
void Reset() { void Reset() {
CleanupNodes(); ClearAllNodes();
m_connections.clear(); m_connections.clear();
@@ -54,7 +95,7 @@ struct BlendTreeResource {
m_node_inputs_subtree.clear(); m_node_inputs_subtree.clear();
} }
void CleanupNodes() { void ClearAllNodes() {
for (AnimNodeResource* node_resource : m_nodes) { for (AnimNodeResource* node_resource : m_nodes) {
delete node_resource; delete node_resource;
} }
@@ -62,31 +103,80 @@ struct BlendTreeResource {
m_nodes.clear(); m_nodes.clear();
} }
void InitGraphConnectors() {
AddNode(AnimNodeResourceFactory("BlendTreeSockets"));
AnimNodeResource* output_node = GetGraphOutputNode();
output_node->m_name = "Outputs";
AddNode(AnimNodeResourceFactory("BlendTreeSockets"));
AnimNodeResource* input_node = GetGraphInputNode();
input_node->m_name = "Inputs";
}
[[nodiscard]] AnimNodeResource* GetGraphOutputNode() const { [[nodiscard]] AnimNodeResource* GetGraphOutputNode() const {
return m_nodes[0]; return m_nodes[0];
} }
[[nodiscard]] AnimNodeResource* GetGraphInputNode() const { [[nodiscard]] AnimNodeResource* GetGraphInputNode() const {
return m_nodes[1]; return m_nodes[1];
} }
Socket* GetGraphOutputSocket(const char* socket_name) const { Socket* GetGraphOutputSocket(const char* socket_name) const {
return GetGraphOutputNode()->m_virtual_socket_accessor->GetInputSocket( return GetGraphOutputNode()->m_virtual_socket_accessor->GetInputSocket(
socket_name); socket_name);
} }
Socket* GetGraphInputSocket(const char* socket_name) const { Socket* GetGraphInputSocket(const char* socket_name) const {
return GetGraphInputNode()->m_virtual_socket_accessor->GetOutputSocket( return GetGraphInputNode()->m_virtual_socket_accessor->GetOutputSocket(
socket_name); socket_name);
} }
template <typename T>
bool RegisterBlendTreeInputSocket(const std::string& socket_name) {
Socket socket;
socket.m_name = socket_name;
socket.m_type = GetSocketType<T>();
socket.m_type_size = sizeof(T);
return RegisterBlendTreeInputSocket(socket);
}
bool RegisterBlendTreeInputSocket(const Socket& socket) {
AnimNodeResource* input_node = GetGraphInputNode();
Socket* input_socket = GetGraphInputSocket(socket.m_name.c_str());
if (input_socket != nullptr) {
std::cerr << "Error: cannot register output socket as socket with name '"
<< socket.m_name << "' already exists!" << std::endl;
return false;
}
input_node->m_virtual_socket_accessor->m_outputs.push_back(socket);
m_virtual_socket_accessor->m_inputs =
input_node->m_virtual_socket_accessor->m_outputs;
return true;
}
template <typename T>
bool RegisterBlendTreeOutputSocket(const std::string& socket_name) {
Socket socket;
socket.m_name = socket_name;
socket.m_type = GetSocketType<T>();
socket.m_type_size = sizeof(T);
return RegisterBlendTreeOutputSocket(socket);
}
bool RegisterBlendTreeOutputSocket(const Socket& socket) {
AnimNodeResource* output_node = GetGraphOutputNode();
Socket* output_socket = GetGraphOutputSocket(socket.m_name.c_str());
if (output_socket != nullptr) {
std::cerr << "Error: cannot register output socket as socket with name '"
<< socket.m_name << "' already exists!" << std::endl;
return false;
}
output_node->m_virtual_socket_accessor->m_inputs.push_back(socket);
m_virtual_socket_accessor->m_outputs =
output_node->m_virtual_socket_accessor->m_inputs;
return true;
}
int GetNodeIndex(const AnimNodeResource* node_resource) const { int GetNodeIndex(const AnimNodeResource* node_resource) const {
for (size_t i = 0, n = m_nodes.size(); i < n; i++) { for (size_t i = 0, n = m_nodes.size(); i < n; i++) {
if (m_nodes[i] == node_resource) { if (m_nodes[i] == node_resource) {
@@ -99,18 +189,54 @@ struct BlendTreeResource {
return -1; return -1;
} }
void CreateUniqueNodeName(AnimNodeResource* node_resource) {
std::string node_name = node_resource->m_name;
if (node_name.empty()) {
node_name = node_resource->m_node_type_name;
}
int index = 0;
bool node_with_name_exists = false;
std::string node_base_name = node_name;
do {
node_with_name_exists = false;
node_resource->m_name = node_name;
for (AnimNodeResource* tree_node_resource : m_nodes) {
if (tree_node_resource->m_name == node_name) {
node_with_name_exists = true;
continue;
}
}
node_name = node_base_name + std::to_string(++index);
} while (node_with_name_exists);
}
[[maybe_unused]] size_t AddNode(AnimNodeResource* node_resource) { [[maybe_unused]] size_t AddNode(AnimNodeResource* node_resource) {
CreateUniqueNodeName(node_resource);
m_nodes.push_back(node_resource); m_nodes.push_back(node_resource);
m_node_input_connection_indices.emplace_back(); m_node_input_connection_indices.emplace_back();
m_node_inputs_subtree.emplace_back(); m_node_inputs_subtree.emplace_back();
return m_nodes.size() - 1; return m_nodes.size() - 1;
} }
void RemoveConnectionsForSocket(
const AnimNodeResource* node_resource,
const Socket& socket);
void RemoveNodeConnections(AnimNodeResource* node_resource);
[[maybe_unused]] bool RemoveNode(AnimNodeResource* node_resource);
[[nodiscard]] size_t GetNumNodes() const { return m_nodes.size(); } [[nodiscard]] size_t GetNumNodes() const { return m_nodes.size(); }
[[nodiscard]] AnimNodeResource* GetNode(size_t i) { return m_nodes[i]; } [[nodiscard]] AnimNodeResource* GetNode(size_t i) { return m_nodes[i]; }
[[nodiscard]] const AnimNodeResource* GetNode(size_t i) const { [[nodiscard]] const AnimNodeResource* GetNode(size_t i) const {
return m_nodes[i]; return m_nodes[i];
} }
[[nodiscard]] const std::vector<AnimNodeResource*>& GetNodes() const { [[nodiscard]] const std::vector<AnimNodeResource*>& GetNodes() const {
return m_nodes; return m_nodes;
} }
@@ -150,6 +276,7 @@ struct BlendTreeResource {
const size_t socket_input_index) const; const size_t socket_input_index) const;
std::vector<Socket> GetNodeOutputSockets(const AnimNodeResource* node) const; std::vector<Socket> GetNodeOutputSockets(const AnimNodeResource* node) const;
std::vector<Socket> GetNodeInputSockets(const AnimNodeResource* node) const; std::vector<Socket> GetNodeInputSockets(const AnimNodeResource* node) const;
bool ConnectSockets( bool ConnectSockets(
@@ -243,139 +370,60 @@ struct BlendTreeResource {
} }
private: private:
void CreateBlendTreeRuntimeNodeInstances(AnimGraphBlendTree& result) const;
void PrepareBlendTreeIOData(
AnimGraphBlendTree& instance,
NodeSocketDataOffsetMap& node_offset_map) const;
void SetRuntimeNodeProperties(AnimGraphBlendTree& result) const;
void CreateBlendTreeConnectionInstances(
AnimGraphBlendTree& instance,
NodeSocketDataOffsetMap& node_offset_map) const;
void InitGraphConnectors() {
AddNode(AnimNodeResourceFactory("BlendTreeSockets"));
AnimNodeResource* output_node = GetGraphOutputNode();
output_node->m_name = "Outputs";
output_node->m_position[0] = 200;
AddNode(AnimNodeResourceFactory("BlendTreeSockets"));
AnimNodeResource* input_node = GetGraphInputNode();
input_node->m_name = "Inputs";
input_node->m_position[0] = -200;
}
void UpdateNodeEvalOrder() { void UpdateNodeEvalOrder() {
m_node_eval_order.clear(); m_node_eval_order.clear();
UpdateNodeEvalOrderRecursive(0); UpdateNodeEvalOrderRecursive(0);
} }
void UpdateNodeEvalOrderRecursive(size_t node_index); void UpdateNodeEvalOrderRecursive(size_t node_index);
void UpdateNodeSubtrees(); void UpdateNodeSubtrees();
std::vector<AnimNodeResource*> m_nodes; std::vector<AnimNodeResource*> m_nodes;
std::vector<BlendTreeConnectionResource> m_connections; std::vector<BlendTreeConnectionResource> m_connections;
std::vector<size_t> m_node_eval_order; std::vector<size_t> m_node_eval_order;
friend class AnimGraphResource;
}; };
struct StateMachineTransitionResources { inline AnimNodeResource* AnimNodeResourceFactory(
size_t source_state_index = -1;
size_t target_state_index = -1;
float blend_time = 0.f;
bool sync_blend = false;
};
struct StateMachineResource {
std::vector<AnimNodeResource> m_states;
std::vector<StateMachineTransitionResources> m_transitions;
};
struct AnimGraphResource : AnimNodeResource {
virtual ~AnimGraphResource() { Clear(); };
std::string m_graph_type_name;
BlendTreeResource m_blend_tree_resource;
typedef std::pair<const AnimNodeResource*, std::string> NodeSocketPair;
typedef std::map<NodeSocketPair, int> NodeSocketDataOffsetMap;
StateMachineResource m_state_machine_resource;
void Clear() { m_blend_tree_resource.Reset(); }
bool SaveToFile(const char* filename) const;
bool LoadFromFile(const char* filename);
void CreateBlendTreeInstance(AnimGraphBlendTree& result) const;
template <typename T>
bool RegisterBlendTreeInputSocket(const std::string& socket_name) {
Socket socket;
socket.m_name = socket_name;
socket.m_type = GetSocketType<T>();
socket.m_type_size = sizeof(T);
return RegisterBlendTreeInputSocket(socket);
}
bool RegisterBlendTreeInputSocket(const Socket& socket) {
AnimNodeResource* input_node = m_blend_tree_resource.GetGraphInputNode();
Socket* input_socket =
m_blend_tree_resource.GetGraphInputSocket(socket.m_name.c_str());
if (input_socket != nullptr) {
std::cerr << "Error: cannot register output socket as socket with name '"
<< socket.m_name << "' already exists!" << std::endl;
return false;
}
input_node->m_virtual_socket_accessor->m_outputs.push_back(socket);
m_virtual_socket_accessor->m_inputs =
input_node->m_virtual_socket_accessor->m_outputs;
return true;
}
template <typename T>
bool RegisterBlendTreeOutputSocket(const std::string& socket_name) {
Socket socket;
socket.m_name = socket_name;
socket.m_type = GetSocketType<T>();
socket.m_type_size = sizeof(T);
return RegisterBlendTreeOutputSocket(socket);
}
bool RegisterBlendTreeOutputSocket(const Socket& socket) {
AnimNodeResource* output_node = m_blend_tree_resource.GetGraphOutputNode();
Socket* output_socket =
m_blend_tree_resource.GetGraphOutputSocket(socket.m_name.c_str());
if (output_socket != nullptr) {
std::cerr << "Error: cannot register output socket as socket with name '"
<< socket.m_name << "' already exists!" << std::endl;
return false;
}
output_node->m_virtual_socket_accessor->m_inputs.push_back(socket);
m_virtual_socket_accessor->m_outputs =
output_node->m_virtual_socket_accessor->m_inputs;
return true;
}
void CreateStateMachineInstance(AnimGraphStateMachine& result) const;
private:
// BlendTree
bool SaveBlendTreeResourceToFile(const char* filename) const;
void CreateBlendTreeRuntimeNodeInstances(AnimGraphBlendTree& result) const;
void PrepareBlendTreeIOData(
AnimGraphBlendTree& instance,
NodeSocketDataOffsetMap& node_offset_map) const;
void CreateBlendTreeConnectionInstances(
AnimGraphBlendTree& instance,
NodeSocketDataOffsetMap& node_offset_map) const;
void SetRuntimeNodeProperties(AnimGraphBlendTree& result) const;
bool SaveStateMachineResourceToFile(const char* filename) const;
bool LoadStateMachineResourceFromJson(nlohmann::json const& json_data);
};
static inline AnimNodeResource* AnimNodeResourceFactory(
const std::string& node_type_name) { const std::string& node_type_name) {
AnimNodeResource* result; AnimNodeResource* result;
if (node_type_name == "BlendTree") { if (node_type_name == "BlendTree") {
AnimGraphResource* blend_tree_resource = new AnimGraphResource(); AnimGraphResource* blend_tree_resource = new BlendTreeResource();
blend_tree_resource->m_graph_type_name = "BlendTree";
blend_tree_resource->m_blend_tree_resource.InitGraphConnectors();
result = blend_tree_resource; result = blend_tree_resource;
} else { } else {
result = new AnimNodeResource(); result = new AnimNodeResource();
result->m_virtual_socket_accessor =
VirtualAnimNodeDescriptorFactory(node_type_name);
} }
result->m_node_type_name = node_type_name; result->m_node_type_name = node_type_name;
result->m_virtual_socket_accessor =
VirtualAnimNodeDescriptorFactory(node_type_name);
return result; return result;
} }
+144
View File
@@ -0,0 +1,144 @@
//
// Created by martin on 11.04.25.
//
#ifndef ANIMLIBRARY_H
#define ANIMLIBRARY_H
#include <iostream>
#include "AnimGraph/AnimGraphData.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
/** Manage a set of animations used for an AnimGraph.
*
* By default, it behaves like a resource that allows to resolve animation names to
* AnimationResources. However, it can also trigger loading of the referenced resources from their
* filenames. This only happens on-demand.
*/
struct AnimLibrary {
typedef std::map<std::string, AnimationResource> AnimationResourceMap;
AnimationResourceMap mAnimations = {};
std::vector<ozz::animation::Animation*> mManagedAnimations = {};
static constexpr const char* EXTERNAL_ANIMATION = "<external>";
AnimLibrary() = default;
~AnimLibrary() { Reset(); }
bool AddAnimation(
const std::string& name,
ozz::animation::Animation* animation) {
AnimationResource animation_resource;
animation_resource.m_name = name;
animation_resource.m_filename = EXTERNAL_ANIMATION;
animation_resource.m_animation = animation;
mAnimations[name] = animation_resource;
return true;
}
bool AddAnimationFile(const std::string& name, const std::string& filename) {
if (mAnimations.find(name) != mAnimations.end()) {
std::cerr << "Cannot add animation '" << name
<< "' to library. Animation already exists." << std::endl;
return false;
}
AnimationResource animation_resource;
animation_resource.m_name = name;
animation_resource.m_animation = nullptr;
animation_resource.m_filename = filename;
mAnimations[name] = animation_resource;
return true;
}
void Reset() {
for (ozz::animation::Animation* animation : mManagedAnimations) {
assert(animation->num_tracks() < 5);
delete animation;
}
mManagedAnimations.clear();
mAnimations.clear();
}
void LoadAnimations() {
for (AnimationResourceMap::iterator iter = mAnimations.begin();
iter != mAnimations.end();
++iter) {
if (iter->second.m_filename == EXTERNAL_ANIMATION) {
continue;
}
if (iter->second.m_animation != nullptr) {
continue;
}
assert(!iter->second.m_filename.empty());
ozz::io::File file(iter->second.m_filename.c_str(), "rb");
if (!file.opened()) {
ozz::log::Err() << "Failed to open animation file "
<< iter->second.m_filename << "." << std::endl;
continue;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Animation>()) {
ozz::log::Err() << "Failed to load animation instance from file "
<< iter->second.m_filename << "." << std::endl;
continue;
}
iter->second.m_animation = new ozz::animation::Animation;
archive >> *iter->second.m_animation;
mManagedAnimations.push_back(iter->second.m_animation);
}
}
};
inline void to_json(nlohmann::json& j, const AnimLibrary& animation_library) {
j["type"] = "AnimationLibrary";
for (AnimLibrary::AnimationResourceMap::const_iterator iter =
animation_library.mAnimations.cbegin();
iter != animation_library.mAnimations.cend();
++iter) {
j["animations"][iter->first] = iter->second;
}
}
inline void from_json(const nlohmann::json& j, AnimLibrary& animation_library) {
animation_library.Reset();
if (!j.contains("type") || j["type"] != "AnimationLibrary") {
std::cerr << "Invalid type. Expected 'AnimationLibrary'." << std::endl;
}
if (!j.contains("animations")) {
std::cerr << "Invalid AnimationLibrary. Expected 'animations' key."
<< std::endl;
}
for (nlohmann::json::const_iterator iter = j["animations"].begin();
iter != j["animations"].cend();
++iter) {
AnimationResource animation_resource = *iter;
if (!animation_resource.m_filename.empty()) {
animation_library.AddAnimationFile(
iter.key(),
j["animations"][iter.key()]["filename"]);
animation_library.mAnimations[iter.key()].m_sync_track =
animation_resource.m_sync_track;
} else {
animation_library.mAnimations[iter.key()] = *iter;
}
}
}
#endif //ANIMLIBRARY_H
+8 -4
View File
@@ -45,8 +45,7 @@ struct AnimNode {
for (const auto& input : input_connections) { for (const auto& input : input_connections) {
AnimNode* input_node = input.m_source_node; AnimNode* input_node = input.m_source_node;
if (input_node != nullptr) { if (input_node != nullptr) {
input_node->m_tick_number = m_tick_number; input_node->Activate(m_tick_number);
input_node->m_state = AnimNodeEvalState::Activated;
} }
} }
} }
@@ -64,13 +63,18 @@ struct AnimNode {
} }
} }
virtual void UpdateTime(float time_last, float time_now) { virtual void UpdateTime(const float time_last, const float time_now) {
m_time_last = time_last; m_time_last = time_last;
m_time_now = time_now; m_time_now = time_now;
m_state = AnimNodeEvalState::TimeUpdated; m_state = AnimNodeEvalState::TimeUpdated;
} }
virtual void Evaluate(AnimGraphContext& context){}; virtual void Evaluate(AnimGraphContext& context) {};
void Activate(const int tick_number) {
m_tick_number = tick_number;
m_state = AnimNodeEvalState::Activated;
}
}; };
#endif //ANIMTESTBED_ANIMNODE_H #endif //ANIMTESTBED_ANIMNODE_H
@@ -3,7 +3,3 @@
// //
#include "SyncTrack.h" #include "SyncTrack.h"
#include <imgui.h>
#include <sstream>
+206
View File
@@ -0,0 +1,206 @@
//
// Created by martin on 19.11.21.
//
#ifndef ANIMTESTBED_SYNCTRACK_H
#define ANIMTESTBED_SYNCTRACK_H
#include <cassert>
#include "3rdparty/json/json.hpp"
constexpr int cSyncTrackMaxIntervals = 8;
/// Metadata used for synced animation blending.
//
// A SyncTrack consists of multiple SyncInterval that are adjacent to each
// other.
//
// Important definitions:
//
// - Absolute Time: time within an animation duration in seconds.
// - Ratio: time relative to the animations duration, e.g. 0.5 corresponds to
// 50% of the duration.
// - SyncTime is a floating point value where the integer parts defines the
// SyncInterval and the fractional part the fraction within the interval. I.e.
// a SyncTime of 5.332 means it is ~33% through interval 5.
//
// A SyncInterval is defined by a ratio of the starting point and the ratio of
// the interval's duration.
struct SyncTrack {
SyncTrack() : m_duration(0.f), m_num_intervals(1) {
for (int i = 0; i < cSyncTrackMaxIntervals; i++) {
m_interval_start_ratio[i] = 0.f;
m_interval_duration_ratio[i] = 0.f;
}
m_interval_duration_ratio[0] = 1.0f;
}
float m_duration;
int m_num_intervals;
float m_interval_start_ratio
[cSyncTrackMaxIntervals]; //< Starting time of interval in absolute time.
float m_interval_duration_ratio[cSyncTrackMaxIntervals]; //<
float CalcSyncFromAbsTime(float abs_time) {
for (int i = 0; i < m_num_intervals; i++) {
float query_abs_time = abs_time;
float interval_start = m_interval_start_ratio[i] * m_duration;
float interval_end =
interval_start + m_interval_duration_ratio[i] * m_duration;
if (query_abs_time < interval_start) {
query_abs_time += m_duration;
}
if (query_abs_time >= interval_start && query_abs_time < interval_end) {
return float(i)
+ (query_abs_time - interval_start)
/ (interval_end - interval_start);
}
}
assert(false && "Invalid absolute time");
return -1.f;
}
float CalcRatioFromSyncTime(float sync_time) {
float interval_ratio = fmodf(sync_time, 1.0f);
int interval = int(sync_time - interval_ratio);
return fmodf(
m_interval_start_ratio[interval]
+ m_interval_duration_ratio[interval] * interval_ratio,
1.0f);
}
bool operator==(const SyncTrack& other) const {
bool result = m_duration == other.m_duration
&& m_num_intervals == other.m_num_intervals;
if (!result) {
return false;
}
for (int i = 0; i < m_num_intervals; i++) {
if ((fabsf(m_interval_start_ratio[i] - other.m_interval_start_ratio[i])
> 1.0e-5)
|| (fabsf(
m_interval_duration_ratio[i]
- other.m_interval_duration_ratio[i])
> 1.0e-5)) {
return false;
}
}
return true;
}
/** Constructs SyncTrack from markers.
*
* Markers are specified in absolute time and must be >= 0 and <= duration.
* They define the start of the interval. The last marker is implicitly the
* (possibly looped) first marker.
*/
static SyncTrack CreateFromMarkers(
float duration,
const std::vector<float>& markers) {
assert(markers.size() > 0);
assert(markers.size() < cSyncTrackMaxIntervals);
SyncTrack result;
result.m_duration = duration;
result.m_num_intervals = markers.size();
for (int i = 0; i < markers.size(); i++) {
assert(markers[i] >= 0.f && markers[i] <= duration);
int end_index = i == (markers.size() - 1) ? 0 : i + 1;
float interval_start = markers[i];
float interval_end = markers[end_index];
if (interval_end == interval_start) {
interval_end = interval_start + duration;
} else if (interval_end < interval_start) {
interval_end += duration;
}
result.m_interval_start_ratio[i] = interval_start / duration;
result.m_interval_duration_ratio[i] =
(interval_end - interval_start) / duration;
}
return result;
}
static SyncTrack
Blend(float weight, const SyncTrack& track_A, const SyncTrack& track_B) {
assert(track_A.m_num_intervals == track_B.m_num_intervals);
SyncTrack result;
result.m_num_intervals = track_A.m_num_intervals;
result.m_duration =
(1.0f - weight) * track_A.m_duration + weight * track_B.m_duration;
float interval_0_offset =
track_B.m_interval_start_ratio[0] - track_A.m_interval_start_ratio[0];
if (interval_0_offset > 0.5f) {
interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f);
} else if (interval_0_offset < -0.5) {
interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f);
}
result.m_interval_start_ratio[0] = fmodf(
1.0 + (1.0f - weight) * track_A.m_interval_start_ratio[0]
+ weight * (track_A.m_interval_start_ratio[0] + interval_0_offset),
1.0f);
for (int i = 0; i < result.m_num_intervals; i++) {
float interval_duration_A = track_A.m_interval_duration_ratio[i];
float interval_duration_B = track_B.m_interval_duration_ratio[i];
result.m_interval_duration_ratio[i] =
(1.0f - weight) * interval_duration_A + weight * interval_duration_B;
if (i < cSyncTrackMaxIntervals) {
result.m_interval_start_ratio[i + 1] =
result.m_interval_start_ratio[i]
+ result.m_interval_duration_ratio[i];
if (result.m_interval_start_ratio[i + 1] > 1.0f) {
result.m_interval_start_ratio[i + 1] =
fmodf(result.m_interval_start_ratio[i + 1], 1.0f);
}
}
}
assert(result.m_num_intervals < cSyncTrackMaxIntervals);
return result;
}
};
inline void to_json(nlohmann::json& j, const SyncTrack& sync_track) {
j["type"] = "SyncTrack";
j["duration"] = sync_track.m_duration;
for (int i = 0; i < sync_track.m_num_intervals; i++) {
j["interval"][i]["start"] = sync_track.m_interval_start_ratio[i];
j["interval"][i]["ratio"] = sync_track.m_interval_duration_ratio[i];
}
}
inline void from_json(const nlohmann::json& j, SyncTrack& sync_track) {
assert(j["type"] == "SyncTrack");
sync_track.m_duration = j["duration"];
sync_track.m_num_intervals = j["interval"].size();
assert(sync_track.m_num_intervals < cSyncTrackMaxIntervals);
for (int i = 0; i < sync_track.m_num_intervals; i++) {
sync_track.m_interval_start_ratio[i] = j["interval"][i]["start"];
sync_track.m_interval_duration_ratio[i] = j["interval"][i]["ratio"];
}
}
#endif //ANIMTESTBED_SYNCTRACK_H
+330 -129
View File
@@ -14,9 +14,12 @@
#include "imgui.h" #include "imgui.h"
#include "imnodes.h" #include "imnodes.h"
#include "misc/cpp/imgui_stdlib.h" #include "misc/cpp/imgui_stdlib.h"
#include "nfd.h"
#include "ozz/base/log.h"
#include "src/AnimGraph/AnimGraphResource.h" #include "src/AnimGraph/AnimGraphResource.h"
struct EditorState { struct EditorState {
std::string filename;
AnimGraphResource* rootGraphResource = nullptr; AnimGraphResource* rootGraphResource = nullptr;
std::vector<AnimGraphResource*> hierarchyStack; std::vector<AnimGraphResource*> hierarchyStack;
@@ -24,9 +27,29 @@ struct EditorState {
bool isGraphLoadedThisFrame = false; bool isGraphLoadedThisFrame = false;
ImVec2 mousePopupStart = {}; ImVec2 mousePopupStart = {};
char statusLine[1024] = {};
};
struct NodeConnectionDebugState {
struct SocketInfo {
ax::NodeEditor::PinId pin = {};
int nodeId = {};
const AnimNodeResource* nodeResource = {};
int socketId = {};
const Socket* socket = {};
};
SocketInfo sourceSocket = {};
SocketInfo targetSocket = {};
void Reset() {
sourceSocket = {};
targetSocket = {};
}
}; };
static EditorState sEditorState; static EditorState sEditorState;
static NodeConnectionDebugState sNodeConnectionDebugState;
constexpr int cPinIconSize = 24; constexpr int cPinIconSize = 24;
@@ -112,25 +135,6 @@ bool NodeSocketEditor(Socket& socket) {
return modified; return modified;
} }
void RemoveBlendTreeConnectionsForSocket(
BlendTreeResource& blend_tree_resource,
AnimNodeResource* node_resource,
Socket& socket) {
const BlendTreeConnectionResource* connection =
blend_tree_resource.FindConnectionForSocket(node_resource, socket.m_name);
while (connection != nullptr) {
blend_tree_resource.DisconnectSockets(
blend_tree_resource.GetNode(connection->source_node_index),
connection->source_socket_name,
blend_tree_resource.GetNode(connection->target_node_index),
connection->target_socket_name);
connection = blend_tree_resource.FindConnectionForSocket(
node_resource,
socket.m_name);
}
}
void SyncTrackEditor(SyncTrack* sync_track) { void SyncTrackEditor(SyncTrack* sync_track) {
ImGui::SliderFloat("duration", &sync_track->m_duration, 0.001f, 10.f); ImGui::SliderFloat("duration", &sync_track->m_duration, 0.001f, 10.f);
@@ -151,21 +155,7 @@ void SyncTrackEditor(SyncTrack* sync_track) {
} }
ImGui::Text("Marker:"); ImGui::Text("Marker:");
for (int i = 0; i < sync_track->m_num_intervals; i++) { ImGui::Text("TODO");
ImGui::Text("%2d:", i);
ImGui::SameLine();
std::ostringstream marker_stream;
marker_stream << i;
ImGui::SliderFloat(
marker_stream.str().c_str(),
&sync_track->m_sync_markers[i],
0.f,
1.f);
}
if (ImGui::Button("Update Intervals")) {
sync_track->CalcIntervals();
}
} }
void SkinnedMeshWidget(SkinnedMesh* skinned_mesh) { void SkinnedMeshWidget(SkinnedMesh* skinned_mesh) {
@@ -211,9 +201,15 @@ void SkinnedMeshWidget(SkinnedMesh* skinned_mesh) {
} }
} }
void AnimGraphEditorRenderSidebar( void BlendTreeEditorRenderSidebar(
BlendTreeResource& blend_tree_resource, BlendTreeResource* blend_tree_resource,
AnimNodeResource* node_resource) { AnimNodeResource* node_resource) {
BlendTreeResource* current_blend_tree_resource =
dynamic_cast<BlendTreeResource*>(
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]);
assert(current_blend_tree_resource != nullptr);
ImGui::Text( ImGui::Text(
"[%s (%2.2f, %2.2f)]", "[%s (%2.2f, %2.2f)]",
node_resource->m_node_type_name.c_str(), node_resource->m_node_type_name.c_str(),
@@ -273,7 +269,7 @@ void AnimGraphEditorRenderSidebar(
} }
} }
if (node_resource == blend_tree_resource.GetGraphOutputNode()) { if (node_resource == blend_tree_resource->GetGraphOutputNode()) {
ImGui::Text("Outputs"); ImGui::Text("Outputs");
// Graph outputs are the inputs of the output node! // Graph outputs are the inputs of the output node!
@@ -316,14 +312,14 @@ void AnimGraphEditorRenderSidebar(
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (ImGui::Button("+")) { if (ImGui::Button("+")) {
AnimGraphResource* current_graph_resource = current_blend_tree_resource->RegisterBlendTreeOutputSocket<float>(
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; "GraphFloatOutput"
current_graph_resource->RegisterBlendTreeOutputSocket<float>( + std::to_string(current_blend_tree_resource->GetGraphOutputNode()
"GraphFloatOutput"); ->m_virtual_socket_accessor->m_inputs.size()));
} }
} }
if (node_resource == blend_tree_resource.GetGraphInputNode()) { if (node_resource == blend_tree_resource->GetGraphInputNode()) {
ImGui::Text("Inputs"); ImGui::Text("Inputs");
// Graph inputs are the outputs of the input node! // Graph inputs are the outputs of the input node!
@@ -340,10 +336,7 @@ void AnimGraphEditorRenderSidebar(
current_graph_resource->m_virtual_socket_accessor->m_inputs = inputs; current_graph_resource->m_virtual_socket_accessor->m_inputs = inputs;
} }
if (ImGui::Button("X")) { if (ImGui::Button("X")) {
RemoveBlendTreeConnectionsForSocket( blend_tree_resource->RemoveConnectionsForSocket(node_resource, input);
blend_tree_resource,
node_resource,
input);
iter = inputs.erase(iter); iter = inputs.erase(iter);
} else { } else {
iter++; iter++;
@@ -352,14 +345,22 @@ void AnimGraphEditorRenderSidebar(
} }
if (ImGui::Button("+")) { if (ImGui::Button("+")) {
AnimGraphResource* current_graph_resource = current_blend_tree_resource->RegisterBlendTreeInputSocket<float>(
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; "GraphFloatInput"
current_graph_resource->RegisterBlendTreeInputSocket<float>( + std::to_string(current_blend_tree_resource->GetGraphInputNode()
"GraphFloatInput"); ->m_virtual_socket_accessor->m_outputs.size()));
} }
} }
} }
void AnimGraphEditorResetHierarchyStack() {
sEditorState.hierarchyStack.clear();
sEditorState.hierarchyStack.push_back(sEditorState.rootGraphResource);
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] =
sEditorState.hierarchyStack.back();
sEditorState.hierarchyStackIndex = 0;
}
void AnimGraphEditorClear() { void AnimGraphEditorClear() {
if (ax::NodeEditor::GetCurrentEditor() != nullptr) { if (ax::NodeEditor::GetCurrentEditor() != nullptr) {
ax::NodeEditor::ClearSelection(); ax::NodeEditor::ClearSelection();
@@ -369,18 +370,13 @@ void AnimGraphEditorClear() {
delete sEditorState.rootGraphResource; delete sEditorState.rootGraphResource;
} }
sEditorState.rootGraphResource = new AnimGraphResource(); sEditorState.filename = "";
sEditorState.rootGraphResource =
dynamic_cast<AnimGraphResource*>(AnimNodeResourceFactory("BlendTree"));
sEditorState.rootGraphResource->m_name = "Root"; sEditorState.rootGraphResource->m_name = "Root";
sEditorState.rootGraphResource->m_graph_type_name = "BlendTree"; sEditorState.isGraphLoadedThisFrame = true;
sEditorState.rootGraphResource->m_blend_tree_resource.InitGraphConnectors();
sEditorState.rootGraphResource->m_virtual_socket_accessor =
new NodeDescriptorBase;
sEditorState.hierarchyStack.clear(); AnimGraphEditorResetHierarchyStack();
sEditorState.hierarchyStack.push_back(sEditorState.rootGraphResource);
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] =
sEditorState.hierarchyStack.back();
sEditorState.hierarchyStackIndex = 0;
} }
void BlendTreeEditorNodePopup() { void BlendTreeEditorNodePopup() {
@@ -430,12 +426,15 @@ void BlendTreeEditorNodePopup() {
} }
if (!node_type_name.empty()) { if (!node_type_name.empty()) {
BlendTreeResource* current_blend_tree_resource =
dynamic_cast<BlendTreeResource*>(
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]);
AnimNodeResource* node_resource = AnimNodeResourceFactory(node_type_name); AnimNodeResource* node_resource = AnimNodeResourceFactory(node_type_name);
ax::NodeEditor::SetNodePosition( ax::NodeEditor::SetNodePosition(
ax::NodeEditor::NodeId(node_resource), ax::NodeEditor::NodeId(node_resource),
sEditorState.mousePopupStart); sEditorState.mousePopupStart);
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] current_blend_tree_resource->AddNode(node_resource);
->m_blend_tree_resource.AddNode(node_resource);
} }
ImGui::EndPopup(); ImGui::EndPopup();
@@ -446,17 +445,60 @@ void BlendTreeEditorNodePopup() {
void AnimGraphEditorMenuBar() { void AnimGraphEditorMenuBar() {
ImGui::BeginMenuBar(); ImGui::BeginMenuBar();
if (ImGui::Button("Save")) { if (ImGui::BeginMenu("File")) {
sEditorState.rootGraphResource->SaveToFile("editor_graph.json"); if (ImGui::MenuItem("New")) {
}
if (ImGui::Button("Load")) {
AnimGraphEditorClear();
sEditorState.rootGraphResource->LoadFromFile("editor_graph.json");
sEditorState.isGraphLoadedThisFrame = true;
}
if (ImGui::Button("Clear")) {
AnimGraphEditorClear(); AnimGraphEditorClear();
} }
if (ImGui::MenuItem("Load ...", "Ctrl+L")) {
nfdu8char_t* outPath;
nfdu8filteritem_t filters[1] = {{"Json files", "json"}};
nfdopendialogu8args_t args = {0};
args.filterList = filters;
args.filterCount = 1;
args.defaultPath = ".";
nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);
if (result == NFD_OKAY) {
AnimGraphEditorClear();
delete sEditorState.rootGraphResource;
sEditorState.filename = outPath;
sEditorState.rootGraphResource =
AnimGraphResource::CreateFromFile(sEditorState.filename.c_str());
AnimGraphEditorResetHierarchyStack();
NFD_FreePathU8(outPath);
}
}
if (ImGui::MenuItem(
"Save",
"Ctrl+S",
nullptr,
!sEditorState.filename.empty())) {
sEditorState.rootGraphResource->SaveToFile(sEditorState.filename.c_str());
}
if (ImGui::MenuItem("Save as ...")) {
nfdu8char_t* outPath;
nfdu8filteritem_t filters[1] = {{"Json files", "json"}};
nfdsavedialogu8args_t args = {0};
args.filterList = filters;
args.filterCount = 1;
args.defaultPath = ".";
nfdresult_t result =
NFD_SaveDialogU8(&outPath, args.filterList, 1, ".", "BlendTree.json");
if (result == NFD_OKAY) {
sEditorState.filename = outPath;
sEditorState.rootGraphResource->SaveToFile(
sEditorState.filename.c_str());
NFD_FreePathU8(outPath);
}
}
ImGui::EndMenu();
}
if (ImGui::Button("Content")) { if (ImGui::Button("Content")) {
ax::NodeEditor::NavigateToContent(); ax::NodeEditor::NavigateToContent();
} }
@@ -476,9 +518,11 @@ void AnimGraphEditorMenuBar() {
} }
void AnimGraphEditorBreadcrumbNavigation() { void AnimGraphEditorBreadcrumbNavigation() {
ImGui::Text("Navigation:");
ImGui::SameLine();
for (size_t i = 0, n = sEditorState.hierarchyStack.size(); i < n; i++) { for (size_t i = 0, n = sEditorState.hierarchyStack.size(); i < n; i++) {
AnimGraphResource* graph_resource = AnimGraphResource* graph_resource = sEditorState.hierarchyStack[i];
dynamic_cast<AnimGraphResource*>(sEditorState.hierarchyStack[i]);
ImGui::PushID(graph_resource); ImGui::PushID(graph_resource);
bool highlight_button = i == sEditorState.hierarchyStackIndex; bool highlight_button = i == sEditorState.hierarchyStackIndex;
@@ -505,65 +549,113 @@ void AnimGraphEditorBreadcrumbNavigation() {
} }
} }
void HandleConnectionCreation( void BlendTreeHandleConnectionCreation(BlendTreeResource* current_blend_tree) {
BlendTreeResource& current_blend_tree) { // Create Connections
if (ax::NodeEditor::BeginCreate()) { if (ax::NodeEditor::BeginCreate()) {
ax::NodeEditor::PinId input_pin_id, output_pin_id; ax::NodeEditor::PinId input_pin_id, output_pin_id;
if (ax::NodeEditor::QueryNewLink(&input_pin_id, &output_pin_id)) { if (ax::NodeEditor::QueryNewLink(&input_pin_id, &output_pin_id)) {
int source_node_index; if (input_pin_id == output_pin_id) {
int source_node_socket_index; ax::NodeEditor::RejectNewItem();
ax::NodeEditor::EndCreate();
return;
}
ax::NodeEditor::PinId source_pin = input_pin_id;
ax::NodeEditor::PinId target_pin = output_pin_id;
int source_node_index = -1;
int source_node_socket_index = -1;
const AnimNodeResource* source_node = nullptr; const AnimNodeResource* source_node = nullptr;
const Socket* source_socket = nullptr; const Socket* source_socket = nullptr;
if (input_pin_id) { if (input_pin_id) {
if (IsPinInput(input_pin_id.Get())) {
source_pin = input_pin_id;
target_pin = output_pin_id;
} else {
target_pin = input_pin_id;
source_pin = output_pin_id;
}
}
if (output_pin_id) {
if (IsPinOutput(output_pin_id.Get())) {
target_pin = output_pin_id;
source_pin = input_pin_id;
} else {
source_pin = output_pin_id;
target_pin = input_pin_id;
}
}
if (!source_pin.Invalid) {
OutputPinIdToNodeIndexAndSocketIndex( OutputPinIdToNodeIndexAndSocketIndex(
input_pin_id.Get(), source_pin.Get(),
&source_node_index, &source_node_index,
&source_node_socket_index); &source_node_socket_index);
source_node = current_blend_tree.GetNode(source_node_index); source_node = current_blend_tree->GetNode(source_node_index);
if (source_node != nullptr) {
if (source_node->m_virtual_socket_accessor->m_outputs.size() if (source_node->m_virtual_socket_accessor->m_outputs.size()
< source_node_socket_index) { < source_node_socket_index) {
source_node_socket_index = -1; source_node_socket_index = -1;
} else { } else {
source_socket = current_blend_tree.GetNodeOutputSocketByIndex( source_socket = current_blend_tree->GetNodeOutputSocketByIndex(
source_node, source_node,
source_node_socket_index); source_node_socket_index);
} }
} }
}
int target_node_index; int target_node_index = -1;
int target_node_socket_index; int target_node_socket_index = -1;
const AnimNodeResource* target_node = nullptr; const AnimNodeResource* target_node = nullptr;
const Socket* target_socket = nullptr; const Socket* target_socket = nullptr;
if (output_pin_id) { if (!target_pin.Invalid) {
InputPinIdToNodeIndexAndSocketIndex( InputPinIdToNodeIndexAndSocketIndex(
output_pin_id.Get(), target_pin.Get(),
&target_node_index, &target_node_index,
&target_node_socket_index); &target_node_socket_index);
target_node = current_blend_tree.GetNode(target_node_index); target_node = current_blend_tree->GetNode(target_node_index);
if (target_node != nullptr) {
if (target_node->m_virtual_socket_accessor->m_inputs.size() if (target_node->m_virtual_socket_accessor->m_inputs.size()
< target_node_socket_index) { < target_node_socket_index) {
target_node_socket_index = -1; target_node_socket_index = -1;
} else { } else {
target_socket = current_blend_tree.GetNodeInputSocketByIndex( target_socket = current_blend_tree->GetNodeInputSocketByIndex(
target_node, target_node,
target_node_socket_index); target_node_socket_index);
} }
} }
}
if (input_pin_id && output_pin_id) { sNodeConnectionDebugState.sourceSocket.pin = source_pin;
sNodeConnectionDebugState.sourceSocket.nodeId = source_node_index;
sNodeConnectionDebugState.sourceSocket.nodeResource = source_node;
sNodeConnectionDebugState.sourceSocket.socketId =
source_node_socket_index;
sNodeConnectionDebugState.sourceSocket.socket = source_socket;
sNodeConnectionDebugState.targetSocket.pin = target_pin;
sNodeConnectionDebugState.targetSocket.nodeId = target_node_index;
sNodeConnectionDebugState.targetSocket.nodeResource = target_node;
sNodeConnectionDebugState.targetSocket.socketId =
target_node_socket_index;
sNodeConnectionDebugState.targetSocket.socket = target_socket;
if (!source_pin.Invalid && !target_pin.Invalid) {
if (source_socket == nullptr || target_socket == nullptr if (source_socket == nullptr || target_socket == nullptr
|| !current_blend_tree.IsConnectionValid( || !current_blend_tree->IsConnectionValid(
source_node, source_node,
source_socket->m_name, source_socket->m_name,
target_node, target_node,
target_socket->m_name)) { target_socket->m_name)) {
ax::NodeEditor::RejectNewItem(); ax::NodeEditor::RejectNewItem();
} else if (ax::NodeEditor::AcceptNewItem()) { } else if (ax::NodeEditor::AcceptNewItem()) {
current_blend_tree.ConnectSockets( current_blend_tree->ConnectSockets(
source_node, source_node,
source_socket->m_name, source_socket->m_name,
target_node, target_node,
@@ -576,12 +668,12 @@ void HandleConnectionCreation(
} }
void BlendTreeRenderNodes( void BlendTreeRenderNodes(
BlendTreeResource& current_blend_tree, BlendTreeResource* current_blend_tree,
ax::NodeEditor::Utilities::BlueprintNodeBuilder& builder) { ax::NodeEditor::Utilities::BlueprintNodeBuilder& builder) {
for (size_t node_index = 0, n = current_blend_tree.GetNumNodes(); for (size_t node_index = 0, n = current_blend_tree->GetNumNodes();
node_index < n; node_index < n;
node_index++) { node_index++) {
AnimNodeResource* node_resource = current_blend_tree.GetNode(node_index); AnimNodeResource* node_resource = current_blend_tree->GetNode(node_index);
ax::NodeEditor::NodeId node_id(node_resource); ax::NodeEditor::NodeId node_id(node_resource);
@@ -594,22 +686,29 @@ void BlendTreeRenderNodes(
} }
builder.Header(); builder.Header();
if (node_resource->m_name != "") {
ImGui::Text("%s", node_resource->m_name.c_str());
} else {
ImGui::Text("%s", node_resource->m_node_type_name.c_str()); ImGui::Text("%s", node_resource->m_node_type_name.c_str());
}
ImGui::Spring(0); ImGui::Spring(0);
builder.EndHeader(); builder.EndHeader();
// Inputs // Inputs
std::vector<Socket> node_inputs = std::vector<Socket> node_inputs =
current_blend_tree.GetNodeInputSockets(node_resource); current_blend_tree->GetNodeInputSockets(node_resource);
for (size_t j = 0, ni = node_inputs.size(); j < ni; j++) { for (size_t j = 0, ni = node_inputs.size(); j < ni; j++) {
Socket& socket = node_inputs[j]; Socket& socket = node_inputs[j];
builder.Input(NodeIndexAndSocketIndexToInputPinId( ax::NodeEditor::PinId input_pin = NodeIndexAndSocketIndexToInputPinId(
static_cast<int>(node_index), static_cast<int>(node_index),
static_cast<int>(j))); static_cast<int>(j));
builder.Input(input_pin);
assert(!input_pin.Invalid);
DrawSocketIcon( DrawSocketIcon(
socket.m_type, socket.m_type,
current_blend_tree.IsSocketConnected(node_resource, socket.m_name)); current_blend_tree->IsSocketConnected(node_resource, socket.m_name));
ImGui::Spring(0); ImGui::Spring(0);
//ImGui::PushItemWidth(100.0f); //ImGui::PushItemWidth(100.0f);
@@ -620,7 +719,7 @@ void BlendTreeRenderNodes(
// Outputs // Outputs
std::vector<Socket> node_outputs = std::vector<Socket> node_outputs =
current_blend_tree.GetNodeOutputSockets(node_resource); current_blend_tree->GetNodeOutputSockets(node_resource);
for (size_t j = 0, ni = node_outputs.size(); j < ni; j++) { for (size_t j = 0, ni = node_outputs.size(); j < ni; j++) {
Socket& socket = node_outputs[j]; Socket& socket = node_outputs[j];
builder.Output(NodeIndexAndSocketIndexToOutputPinId( builder.Output(NodeIndexAndSocketIndexToOutputPinId(
@@ -632,7 +731,7 @@ void BlendTreeRenderNodes(
ImGui::Spring(0); ImGui::Spring(0);
DrawSocketIcon( DrawSocketIcon(
socket.m_type, socket.m_type,
current_blend_tree.IsSocketConnected(node_resource, socket.m_name)); current_blend_tree->IsSocketConnected(node_resource, socket.m_name));
builder.EndOutput(); builder.EndOutput();
} }
@@ -645,21 +744,21 @@ void BlendTreeRenderNodes(
} }
} }
void BlendTreeRenderConnections(BlendTreeResource& current_blend_tree) { void BlendTreeRenderConnections(BlendTreeResource* current_blend_tree) {
for (size_t connection_id = 0, n = current_blend_tree.GetNumConnections(); for (size_t connection_id = 0, n = current_blend_tree->GetNumConnections();
connection_id < n; connection_id < n;
connection_id++) { connection_id++) {
const BlendTreeConnectionResource* connection_resource = const BlendTreeConnectionResource* connection_resource =
current_blend_tree.GetConnection(connection_id); current_blend_tree->GetConnection(connection_id);
const AnimNodeResource* source_node_resource = const AnimNodeResource* source_node_resource =
current_blend_tree.GetNode(connection_resource->source_node_index); current_blend_tree->GetNode(connection_resource->source_node_index);
int source_socket_index = int source_socket_index =
source_node_resource->m_virtual_socket_accessor->GetOutputIndex( source_node_resource->m_virtual_socket_accessor->GetOutputIndex(
connection_resource->source_socket_name.c_str()); connection_resource->source_socket_name.c_str());
const AnimNodeResource* target_node_resource = const AnimNodeResource* target_node_resource =
current_blend_tree.GetNode(connection_resource->target_node_index); current_blend_tree->GetNode(connection_resource->target_node_index);
int target_socket_index = int target_socket_index =
target_node_resource->m_virtual_socket_accessor->GetInputIndex( target_node_resource->m_virtual_socket_accessor->GetInputIndex(
connection_resource->target_socket_name.c_str()); connection_resource->target_socket_name.c_str());
@@ -677,55 +776,142 @@ void BlendTreeRenderConnections(BlendTreeResource& current_blend_tree) {
target_socket_pin_id); target_socket_pin_id);
} }
} }
void BlendTreeEditorDebugWidget() {
ImGui::Begin("Connection Debug Panel");
ImGui::BeginTable("Connection", 3);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Pin");
ImGui::TableNextColumn();
ImGui::Text("%p", sNodeConnectionDebugState.sourceSocket.pin.AsPointer());
ImGui::TableNextColumn();
ImGui::Text("%p", sNodeConnectionDebugState.targetSocket.pin.AsPointer());
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Node");
ImGui::TableNextColumn();
if (sNodeConnectionDebugState.sourceSocket.nodeResource) {
ImGui::Text(
"%s (%p)",
sNodeConnectionDebugState.sourceSocket.nodeResource->m_node_type_name
.c_str(),
sNodeConnectionDebugState.sourceSocket.nodeResource);
}
ImGui::TableNextColumn();
if (sNodeConnectionDebugState.targetSocket.nodeResource) {
ImGui::Text(
"%s (%p)",
sNodeConnectionDebugState.targetSocket.nodeResource->m_node_type_name
.c_str(),
sNodeConnectionDebugState.targetSocket.nodeResource);
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("NodeId");
ImGui::TableNextColumn();
ImGui::Text("%d", sNodeConnectionDebugState.sourceSocket.nodeId);
ImGui::TableNextColumn();
ImGui::Text("%d", sNodeConnectionDebugState.targetSocket.nodeId);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Socket");
ImGui::TableNextColumn();
if (sNodeConnectionDebugState.sourceSocket.socket) {
ImGui::Text(
"%s (%p)",
sNodeConnectionDebugState.sourceSocket.socket->m_name.c_str(),
sNodeConnectionDebugState.sourceSocket.socket);
}
ImGui::TableNextColumn();
if (sNodeConnectionDebugState.targetSocket.socket) {
ImGui::Text(
"%s (%p)",
sNodeConnectionDebugState.targetSocket.socket->m_name.c_str(),
sNodeConnectionDebugState.targetSocket.socket);
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("SocketId");
ImGui::TableNextColumn();
ImGui::Text("%x", sNodeConnectionDebugState.sourceSocket.socketId);
ImGui::TableNextColumn();
ImGui::Text("%x", sNodeConnectionDebugState.targetSocket.socketId);
ImGui::EndTable();
ImGui::End();
}
void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) {
sEditorState.statusLine[0] = '\0';
sEditorState.statusLine[sizeof(sEditorState.statusLine) - 1] = '\0';
sNodeConnectionDebugState.Reset();
ax::NodeEditor::SetCurrentEditor(context); ax::NodeEditor::SetCurrentEditor(context);
AnimGraphEditorMenuBar(); AnimGraphEditorMenuBar();
AnimGraphEditorBreadcrumbNavigation(); AnimGraphEditorBreadcrumbNavigation();
ImGui::Columns(2); static ImGuiTableFlags flags =
ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Resizable;
ImGui::BeginTable("GraphEditorWithSidebar", 2, flags);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
// //
// Node editor canvas // Node editor canvas
// //
ax::NodeEditor::Begin("Graph Editor"); ImVec2 graph_size = ImGui::GetContentRegionAvail();
graph_size.y -= 20;
ax::NodeEditor::Begin("Graph Editor", graph_size);
AnimGraphResource* current_graph = BlendTreeResource* current_blend_tree_resource =
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; dynamic_cast<BlendTreeResource*>(
BlendTreeResource& current_blend_tree = current_graph->m_blend_tree_resource; sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]);
if (current_blend_tree_resource) {
ax::NodeEditor::Utilities::BlueprintNodeBuilder builder; ax::NodeEditor::Utilities::BlueprintNodeBuilder builder;
BlendTreeRenderNodes(current_blend_tree, builder); BlendTreeRenderNodes(current_blend_tree_resource, builder);
BlendTreeRenderConnections(current_blend_tree_resource);
BlendTreeRenderConnections(current_blend_tree); BlendTreeHandleConnectionCreation(current_blend_tree_resource);
HandleConnectionCreation(current_blend_tree);
BlendTreeEditorNodePopup(); BlendTreeEditorNodePopup();
}
ax::NodeEditor::End(); ax::NodeEditor::End();
ImGui::Text("Status: %s", sEditorState.statusLine);
// //
// Sidebar // Sidebar
// //
ImGui::NextColumn(); ImGui::TableSetColumnIndex(1);
if (ax::NodeEditor::GetSelectedObjectCount() > 0) { if (current_blend_tree_resource
&& ax::NodeEditor::GetSelectedObjectCount() > 0) {
ax::NodeEditor::NodeId selected_node_id = 0; ax::NodeEditor::NodeId selected_node_id = 0;
ax::NodeEditor::GetSelectedNodes(&selected_node_id, 1); ax::NodeEditor::GetSelectedNodes(&selected_node_id, 1);
if (selected_node_id.Get() != 0) { if (selected_node_id.Get() != 0) {
AnimGraphEditorRenderSidebar( BlendTreeEditorRenderSidebar(
sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] current_blend_tree_resource,
->m_blend_tree_resource,
selected_node_id.AsPointer<AnimNodeResource>()); selected_node_id.AsPointer<AnimNodeResource>());
} }
} }
ImGui::Columns(1); ImGui::EndTable();
BlendTreeEditorDebugWidget();
// Clear flag, however it may be re-set further down when handling double // Clear flag, however it may be re-set further down when handling double
// clicking into subgraphs. // clicking into subgraphs.
@@ -769,24 +955,39 @@ void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) {
BlendTreeConnectionResource* connection_resource = BlendTreeConnectionResource* connection_resource =
hovered_link.AsPointer<BlendTreeConnectionResource>(); hovered_link.AsPointer<BlendTreeConnectionResource>();
if (connection_resource && ImGui::IsKeyPressed(ImGuiKey_Delete)) { if (connection_resource && current_blend_tree_resource
BlendTreeResource* blend_tree_resource = && ImGui::IsKeyPressed(ImGuiKey_Delete)) {
&sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] current_blend_tree_resource->DisconnectSockets(
->m_blend_tree_resource; current_blend_tree_resource->GetNode(
connection_resource->source_node_index),
blend_tree_resource->DisconnectSockets(
blend_tree_resource->GetNode(connection_resource->source_node_index),
connection_resource->source_socket_name, connection_resource->source_socket_name,
blend_tree_resource->GetNode(connection_resource->target_node_index), current_blend_tree_resource->GetNode(
connection_resource->target_node_index),
connection_resource->target_socket_name); connection_resource->target_socket_name);
ax::NodeEditor::DeleteLink(hovered_link); ax::NodeEditor::DeleteLink(hovered_link);
} }
} }
ax::NodeEditor::NodeId hovered_node = ax::NodeEditor::GetHoveredNode();
if (!hovered_node.Invalid) {
AnimNodeResource* node_resource =
hovered_node.AsPointer<AnimNodeResource>();
if (node_resource && current_blend_tree_resource
&& ImGui::IsKeyPressed(ImGuiKey_Delete)) {
current_blend_tree_resource->RemoveNodeConnections(node_resource);
current_blend_tree_resource->RemoveNode(node_resource);
}
}
ax::NodeEditor::SetCurrentEditor(nullptr); ax::NodeEditor::SetCurrentEditor(nullptr);
} }
void AnimGraphEditorGetRuntimeGraph(AnimGraphBlendTree& blend_tree) { void AnimGraphEditorGetRuntimeBlendTree(AnimGraphBlendTree& blend_tree) {
sEditorState.rootGraphResource->CreateBlendTreeInstance(blend_tree); BlendTreeResource* root_blend_tree_resource =
dynamic_cast<BlendTreeResource*>(sEditorState.rootGraphResource);
assert(root_blend_tree_resource);
root_blend_tree_resource->CreateBlendTreeInstance(blend_tree);
} }
+21 -7
View File
@@ -13,6 +13,9 @@ struct SkinnedMesh;
struct AnimGraphBlendTree; struct AnimGraphBlendTree;
struct SyncTrack; struct SyncTrack;
constexpr int cMaxSocketsPerNode = 500;
constexpr int cMaxNodesPerGraph = 1000;
inline int GenerateInputAttributeId(int node_id, int input_index) { inline int GenerateInputAttributeId(int node_id, int input_index) {
return ((input_index + 1) << 14) + node_id; return ((input_index + 1) << 14) + node_id;
} }
@@ -36,31 +39,42 @@ SplitOutputAttributeId(int attribute_id, int* node_id, int* output_index) {
inline int NodeIndexAndSocketIndexToInputPinId( inline int NodeIndexAndSocketIndexToInputPinId(
int node_index, int node_index,
int input_socket_index) { int input_socket_index) {
return node_index * 1000 + input_socket_index; return node_index * cMaxNodesPerGraph + input_socket_index;
} }
inline int NodeIndexAndSocketIndexToOutputPinId( inline int NodeIndexAndSocketIndexToOutputPinId(
int node_index, int node_index,
int output_socket_index) { int output_socket_index) {
return node_index * 1000 + 500 + output_socket_index; return node_index * cMaxNodesPerGraph + cMaxSocketsPerNode
+ output_socket_index;
} }
inline void InputPinIdToNodeIndexAndSocketIndex( inline void InputPinIdToNodeIndexAndSocketIndex(
unsigned long input_pin_id, unsigned long input_pin_id,
int* node_index, int* node_index,
int* socket_index) { int* socket_index) {
*socket_index = input_pin_id % 1000; *socket_index = static_cast<int>(input_pin_id) % cMaxNodesPerGraph;
*node_index = (input_pin_id - *socket_index) / 1000; *node_index =
(static_cast<int>(input_pin_id) - *socket_index) / cMaxNodesPerGraph;
} }
inline void OutputPinIdToNodeIndexAndSocketIndex( inline void OutputPinIdToNodeIndexAndSocketIndex(
unsigned long output_pin_id, unsigned long output_pin_id,
int* node_index, int* node_index,
int* socket_index) { int* socket_index) {
*socket_index = ((output_pin_id - 500) % 1000); *socket_index =
*node_index = (output_pin_id - *socket_index) / 1000; ((static_cast<int>(output_pin_id) - cMaxSocketsPerNode)
% cMaxNodesPerGraph);
*node_index =
(static_cast<int>(output_pin_id) - *socket_index) / cMaxNodesPerGraph;
} }
inline bool IsPinInput(unsigned long pin_id) {
return ((pin_id % cMaxNodesPerGraph) >= cMaxSocketsPerNode);
}
inline bool IsPinOutput(unsigned long pin_id) { return !IsPinInput(pin_id); }
void SyncTrackEditor(SyncTrack* sync_track); void SyncTrackEditor(SyncTrack* sync_track);
void SkinnedMeshWidget(SkinnedMesh* skinned_mesh); void SkinnedMeshWidget(SkinnedMesh* skinned_mesh);
@@ -69,6 +83,6 @@ void AnimGraphEditorClear();
void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context); void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context);
void AnimGraphEditorGetRuntimeGraph(AnimGraphBlendTree& anim_graph); void AnimGraphEditorGetRuntimeBlendTree(AnimGraphBlendTree& anim_graph);
#endif //ANIMTESTBED_ANIMGRAPHEDITOR_H #endif //ANIMTESTBED_ANIMGRAPHEDITOR_H
+21 -20
View File
@@ -42,8 +42,8 @@ inline void Camera_Init(Camera* camera) {
camera->pitch = 10 * M_PI / 180.0f; camera->pitch = 10 * M_PI / 180.0f;
memcpy(&camera->mtxView, &mtx_identity, sizeof(camera->mtxView)); memcpy(&camera->mtxView, &mtx_identity, sizeof(camera->mtxView));
Camera_CalcToMatrix(camera, &camera->mtxView); Camera_CalcToMatrix(camera, &camera->mtxView[0]);
Camera_CalcFromMatrix(camera, &camera->mtxView); Camera_CalcFromMatrix(camera, &camera->mtxView[0]);
} }
void Camera_CalcFromMatrix(Camera* camera, float* mat) { void Camera_CalcFromMatrix(Camera* camera, float* mat) {
@@ -72,11 +72,11 @@ void Camera_CalcFromMatrix(Camera* camera, float* mat) {
camera->pos[1] = -simd4f_get_y(eye); camera->pos[1] = -simd4f_get_y(eye);
camera->pos[2] = -simd4f_get_z(eye); camera->pos[2] = -simd4f_get_z(eye);
// gLog ("ViewMat"); // gLog ("ViewMat");
// gLog ("%f, %f, %f, %f", mtx->x[0], mtx->x[1], mtx->x[2], mtx->x[3]); // gLog ("%f, %f, %f, %f", mtx->x[0], mtx->x[1], mtx->x[2], mtx->x[3]);
// gLog ("%f, %f, %f, %f", mtx->y[0], mtx->y[1], mtx->y[2], mtx->y[3]); // gLog ("%f, %f, %f, %f", mtx->y[0], mtx->y[1], mtx->y[2], mtx->y[3]);
// gLog ("%f, %f, %f, %f", mtx->z[0], mtx->z[1], mtx->z[2], mtx->z[3]); // gLog ("%f, %f, %f, %f", mtx->z[0], mtx->z[1], mtx->z[2], mtx->z[3]);
// gLog ("%f, %f, %f, %f", mtx->w[0], mtx->w[1], mtx->w[2], mtx->w[3]); // gLog ("%f, %f, %f, %f", mtx->w[0], mtx->w[1], mtx->w[2], mtx->w[3]);
} }
void Camera_CalcToMatrix(Camera* camera, float* mat) { void Camera_CalcToMatrix(Camera* camera, float* mat) {
@@ -87,9 +87,10 @@ void Camera_CalcToMatrix(Camera* camera, float* mat) {
const float d = 10.0f; const float d = 10.0f;
simd4f eye = simd4f_create (camera->pos[0], camera->pos[1], camera->pos[2], 1.f); simd4f eye =
simd4f forward = simd4f_create (-cp * ch, -sp, cp * sh, 0.f); simd4f_create(camera->pos[0], camera->pos[1], camera->pos[2], 1.f);
simd4f right = simd4f_cross3 (forward, simd4f_create (0.f, 1.f, 0.f, 1.f)); simd4f forward = simd4f_create(-cp * ch, -sp, cp * sh, 0.f);
simd4f right = simd4f_cross3(forward, simd4f_create(0.f, 1.f, 0.f, 1.f));
simd4f up = simd4f_cross3(right, forward); simd4f up = simd4f_cross3(right, forward);
simd4f center = simd4f_add(simd4f_mul(forward, simd4f_splat(d)), eye); simd4f center = simd4f_add(simd4f_mul(forward, simd4f_splat(d)), eye);
@@ -105,9 +106,9 @@ void Camera_CalcToMatrix(Camera* camera, float* mat) {
simd4x4f_lookat(&mtx, eye, center, up); simd4x4f_lookat(&mtx, eye, center, up);
simd4f_ustore4(mtx.x, mat); simd4f_ustore4(mtx.x, mat);
simd4f_ustore4(mtx.y, mat +4); simd4f_ustore4(mtx.y, mat + 4);
simd4f_ustore4(mtx.z, mat +8); simd4f_ustore4(mtx.z, mat + 8);
simd4f_ustore4(mtx.w, mat +12); simd4f_ustore4(mtx.w, mat + 12);
} }
inline void Camera_Update( inline void Camera_Update(
@@ -120,14 +121,14 @@ inline void Camera_Update(
float accel[3]) { float accel[3]) {
assert(camera); assert(camera);
assert((width > 0) && (height > 0)); assert((width > 0) && (height > 0));
const float w = (float) width; const float w = (float)width;
const float h = (float) height; const float h = (float)height;
simd4x4f proj; simd4x4f proj;
simd4x4f_perspective(&proj, camera->fov, w/h, camera->near, camera->far); simd4x4f_perspective(&proj, camera->fov, w / h, camera->near, camera->far);
simd4f_ustore4(proj.x, camera->mtxProj); simd4f_ustore4(proj.x, camera->mtxProj);
simd4f_ustore4(proj.y, camera->mtxProj +4); simd4f_ustore4(proj.y, camera->mtxProj + 4);
simd4f_ustore4(proj.z, camera->mtxProj +8); simd4f_ustore4(proj.z, camera->mtxProj + 8);
simd4f_ustore4(proj.w, camera->mtxProj +12); simd4f_ustore4(proj.w, camera->mtxProj + 12);
if (mouse_dx != 0.f || mouse_dy != 0.f || accel != NULL) { if (mouse_dx != 0.f || mouse_dy != 0.f || accel != NULL) {
const float mouse_sensitivity = 20.0f; const float mouse_sensitivity = 20.0f;
@@ -153,6 +154,6 @@ inline void Camera_Update(
camera->vel[i] = camera->vel[i] * 0.1; camera->vel[i] = camera->vel[i] * 0.1;
} }
Camera_CalcToMatrix(camera, &camera->mtxView); Camera_CalcToMatrix(camera, &camera->mtxView[0]);
} }
} }
+5 -2
View File
@@ -4,9 +4,13 @@
#include "SkinnedMesh.h" #include "SkinnedMesh.h"
#include <HandmadeMath.h>
#include <imgui.h> #include <imgui.h>
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
SkinnedMesh::~SkinnedMesh() { SkinnedMesh::~SkinnedMesh() {
while (m_animations.size() > 0) { while (m_animations.size() > 0) {
ozz::animation::Animation* animation_ptr = ozz::animation::Animation* animation_ptr =
@@ -98,4 +102,3 @@ void SkinnedMesh::CalcModelMatrices() {
} }
void SkinnedMesh::DrawSkeleton() {} void SkinnedMesh::DrawSkeleton() {}
+1 -9
View File
@@ -5,21 +5,13 @@
#ifndef ANIMTESTBED_SKINNEDMESH_H #ifndef ANIMTESTBED_SKINNEDMESH_H
#define ANIMTESTBED_SKINNEDMESH_H #define ANIMTESTBED_SKINNEDMESH_H
// ozz-animation headers #include "AnimGraph/SyncTrack.h"
#include <cmath> // fmodf
#include <memory> // std::unique_ptr, std::make_unique
#include "SyncTrack.h"
#include "ozz/animation/runtime/animation.h" #include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/animation/runtime/sampling_job.h" #include "ozz/animation/runtime/sampling_job.h"
#include "ozz/animation/runtime/skeleton.h" #include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/containers/vector.h" #include "ozz/base/containers/vector.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
#include "ozz/base/maths/soa_transform.h" #include "ozz/base/maths/soa_transform.h"
#include "ozz/base/maths/vec_float.h"
struct SkinnedMesh { struct SkinnedMesh {
SkinnedMesh() : m_sync_track_override(false), m_override_anim(0.f) {} SkinnedMesh() : m_sync_track_override(false), m_override_anim(0.f) {}
+6 -69
View File
@@ -5,72 +5,15 @@
#include "SkinnedMeshResource.h" #include "SkinnedMeshResource.h"
#include <fstream> #include <fstream>
#include <iostream>
#include "3rdparty/json/json.hpp" #include "3rdparty/json/json.hpp"
inline void to_json(nlohmann::json& j, const SyncTrack& syncTrack) { NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
j["type"] = "SyncTrack"; SkinnedMeshResource,
j["duration"] = syncTrack.m_duration; m_type,
for (int i = 0; i < syncTrack.m_num_intervals; i++) { m_resource_file,
j["markers"][i] = syncTrack.m_sync_markers[i]; m_skeleton_file);
}
}
inline void from_json(const nlohmann::json& j, SyncTrack& syncTrack) {
if (!j.contains("type") || j["type"] != "SyncTrack") {
std::cerr << "Unable to parse SyncTrack: wrong json type!" << std::endl;
return;
}
syncTrack.m_duration = j["duration"];
syncTrack.m_num_intervals = j["markers"].size();
if (syncTrack.m_num_intervals > cSyncTrackMaxIntervals) {
std::cerr << "Invalid number of sync intervals: found " << syncTrack.m_num_intervals << " maximum is " << cSyncTrackMaxIntervals << "." << std::endl;
syncTrack = SyncTrack();
}
for (int i = 0; i < syncTrack.m_num_intervals; i++) {
syncTrack.m_sync_markers[i] = j["markers"].at(i);
}
}
inline void to_json(nlohmann::json& j, const SkinnedMeshResource& skinnedMeshResource) {
j["type"] = "SkinnedMeshResource";
j["skeleton"]["file"] = skinnedMeshResource.m_skeleton_file;
for (int i = 0; i < skinnedMeshResource.m_animation_files.size(); i++) {
j["animations"][i]["file"] = skinnedMeshResource.m_animation_files[i];
j["animations"][i]["sync_track"] = skinnedMeshResource.m_sync_tracks[i];
}
}
inline void from_json(const nlohmann::json& j, SkinnedMeshResource& skinnedMeshResource) {
if (!j.contains("type") || j["type"] != "SkinnedMeshResource") {
std::cerr << "Unable to parse SkinnedMeshResource: wrong json type!" << std::endl;
return;
}
if (!j.contains("skeleton") || !j["skeleton"].contains("file")) {
std::cerr << "Unable to parse SkinnedMeshResource: no skeleton file found!" << std::endl;
}
skinnedMeshResource.m_skeleton_file = j["skeleton"]["file"];
if (j.contains("animations")) {
int num_animations = j["animations"].size();
for (int i = 0; i < num_animations; i++) {
if (!j["animations"][i].contains("file") || !j["animations"][i].contains("sync_track")) {
std::cerr << "Unable to parse SkinnedMeshResource: invalid animation definition" << std::endl;
return;
}
skinnedMeshResource.m_animation_files.push_back(j["animations"][i]["file"]);
skinnedMeshResource.m_sync_tracks.push_back(j["animations"][i]["sync_track"].get<SyncTrack>());
}
}
}
bool SkinnedMeshResource::saveToFile(const char* filename) const { bool SkinnedMeshResource::saveToFile(const char* filename) const {
nlohmann::json j = *this; nlohmann::json j = *this;
@@ -103,10 +46,4 @@ bool SkinnedMeshResource::loadFromFile(const char* filename) {
void SkinnedMeshResource::createInstance(SkinnedMesh& skinnedMesh) const { void SkinnedMeshResource::createInstance(SkinnedMesh& skinnedMesh) const {
skinnedMesh.LoadSkeleton(m_skeleton_file.c_str()); skinnedMesh.LoadSkeleton(m_skeleton_file.c_str());
for (int i = 0; i < m_animation_files.size(); i++) {
skinnedMesh.LoadAnimation(m_animation_files[i].c_str());
skinnedMesh.m_animation_sync_track.back() = m_sync_tracks[i];
}
} }
+3 -3
View File
@@ -8,14 +8,14 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "SyncTrack.h" #include "AnimGraph/SyncTrack.h"
#include "SkinnedMesh.h" #include "SkinnedMesh.h"
struct SkinnedMeshResource { struct SkinnedMeshResource {
constexpr static char TypeStr[] = "SkinnedMeshResource";
std::string m_type = TypeStr;
std::string m_resource_file; std::string m_resource_file;
std::string m_skeleton_file; std::string m_skeleton_file;
std::vector<std::string> m_animation_files;
std::vector<SyncTrack> m_sync_tracks;
bool saveToFile(const char* filename) const; bool saveToFile(const char* filename) const;
bool loadFromFile(const char* filename); bool loadFromFile(const char* filename);
-155
View File
@@ -1,155 +0,0 @@
//
// Created by martin on 19.11.21.
//
#ifndef ANIMTESTBED_SYNCTRACK_H
#define ANIMTESTBED_SYNCTRACK_H
#include <cassert>
#include <cmath>
#include <iostream>
constexpr int cSyncTrackMaxIntervals = 8;
struct SyncTrack {
SyncTrack() : m_duration(0.f), m_num_intervals(0) {
for (int i = 0; i < cSyncTrackMaxIntervals; i++) {
m_sync_markers[i] = 0.f;
m_interval_ratio[i] = 0.f;
m_interval_ratio[i] = 0.f;
}
}
float m_duration;
int m_num_intervals;
float m_sync_markers[cSyncTrackMaxIntervals];
float m_interval_start[cSyncTrackMaxIntervals];
float m_interval_ratio[cSyncTrackMaxIntervals];
void CalcIntervals() {
if (m_num_intervals == 0) {
m_num_intervals = 1;
m_sync_markers[0] = 0.f;
}
for (int i = 0; i < m_num_intervals; i++) {
assert(m_sync_markers[i] >= 0.f && m_sync_markers[i] <= 1.0f);
int end_index = i < m_num_intervals - 1 ? i + 1 : 0;
m_interval_start[i] = m_sync_markers[i];
float interval_end = m_sync_markers[end_index];
if (interval_end < m_interval_start[i]) {
interval_end += 1.0f;
}
m_interval_ratio[i] = interval_end - m_interval_start[i];
}
}
float CalcSyncFromAbsTime(float abs_time) {
float sync_time = fmodf(abs_time, m_duration) / m_duration;
int interval_index = 0;
while (sync_time >= m_interval_ratio[interval_index]) {
sync_time -= m_interval_ratio[interval_index];
interval_index++;
}
return float(interval_index) + sync_time / m_interval_ratio[interval_index];
}
float CalcRatioFromSyncTime(float sync_time) {
float interval_ratio = fmodf(sync_time, 1.0f);
int interval = int(sync_time - interval_ratio);
return fmodf(
m_interval_start[interval]
+ m_interval_ratio[interval] * interval_ratio,
1.0f);
}
bool operator==(const SyncTrack& other) const {
bool result = m_duration == other.m_duration
&& m_num_intervals == other.m_num_intervals;
if (!result) {
return false;
}
for (int i = 0; i < m_num_intervals; i++) {
if ((fabsf(m_interval_start[i] - other.m_interval_start[i]) > 1.0e-5)
|| (fabsf(m_interval_ratio[i] - other.m_interval_ratio[i])
> 1.0e-5)) {
return false;
}
}
return true;
}
static SyncTrack CreateFromMarkers(
float duration,
int n_markers,
float markers[cSyncTrackMaxIntervals]) {
SyncTrack result;
result.m_duration = duration;
result.m_num_intervals = n_markers;
for (int i = 0; i < n_markers; i++) {
result.m_sync_markers[i] = markers[i];
}
result.CalcIntervals();
return result;
}
static SyncTrack
Blend(float weight, const SyncTrack& track_A, const SyncTrack& track_B) {
assert(track_A.m_num_intervals == track_B.m_num_intervals);
SyncTrack result;
result.m_num_intervals = track_A.m_num_intervals;
result.m_duration =
(1.0f - weight) * track_A.m_duration + weight * track_B.m_duration;
float interval_0_offset =
track_B.m_interval_start[0] - track_A.m_interval_start[0];
if (interval_0_offset > 0.5f) {
interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f);
} else if (interval_0_offset < -0.5) {
interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f);
}
result.m_interval_start[0] = fmodf(
1.0 + (1.0f - weight) * track_A.m_interval_start[0]
+ weight * (track_A.m_interval_start[0] + interval_0_offset),
1.0f);
result.m_sync_markers[0] = result.m_interval_start[0];
for (int i = 0; i < result.m_num_intervals; i++) {
float interval_duration_A = track_A.m_interval_ratio[i];
float interval_duration_B = track_B.m_interval_ratio[i];
result.m_interval_ratio[i] =
(1.0f - weight) * interval_duration_A + weight * interval_duration_B;
if (i < cSyncTrackMaxIntervals) {
result.m_interval_start[i + 1] =
result.m_interval_start[i] + result.m_interval_ratio[i];
if (result.m_interval_start[i + 1] > 1.0f) {
result.m_interval_start[i + 1] =
fmodf(result.m_interval_start[i + 1], 1.0f);
}
result.m_sync_markers[i + 1] = result.m_interval_start[i + 1];
}
}
assert (result.m_num_intervals < cSyncTrackMaxIntervals);
return result;
}
};
#endif //ANIMTESTBED_SYNCTRACK_H
+479 -615
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
//
// Created by martin on 11.04.25.
//
#include "3rdparty/json/json.hpp"
#include "AnimGraph/AnimLibrary.h"
#include "TestAnimData.h"
#include "catch.hpp"
using namespace nlohmann;
TEST_CASE("Serialize AnimLibrary", "[AnimLibrary]") {
AnimLibrary library;
TestAnimData::SingleBoneSkeleton single_bone_testdata;
REQUIRE(library.AddAnimationFile(
"translation_x",
single_bone_testdata.animation_translate_x_resource.m_filename));
REQUIRE(library.AddAnimationFile(
"translation_y",
single_bone_testdata.animation_translate_y_resource.m_filename));
// We're not actually doing anything with the animations, however loading them here ensures that
// when running valgrind a memory leak is detected.
library.LoadAnimations();
// serialize
json library_data;
library_data = library;
// deserialize
AnimLibrary library_deserialized;
library_deserialized = library_data;
CHECK(library_deserialized.mAnimations.size() == library.mAnimations.size());
for (AnimLibrary::AnimationResourceMap::const_iterator iter =
library.mAnimations.cbegin();
iter != library.mAnimations.cend();
++iter) {
CHECK(
library_deserialized.mAnimations.find(iter->first)
!= library_deserialized.mAnimations.end());
}
}
+167 -59
View File
@@ -2,25 +2,31 @@
// Created by martin on 04.02.22. // Created by martin on 04.02.22.
// //
#include "AnimGraph/AnimGraph.h" #include "AnimGraph/AnimGraphBlendTree.h"
#include "AnimGraph/AnimGraphBlendTreeResource.h" #include "AnimGraph/AnimGraphResource.h"
#include "AnimGraph/AnimGraphEditor.h"
#include "catch.hpp" #include "catch.hpp"
#include "ozz/animation/offline/animation_builder.h" #include "ozz/animation/offline/animation_builder.h"
#include "ozz/animation/offline/raw_animation.h" #include "ozz/animation/offline/raw_animation.h"
#include "ozz/animation/offline/raw_skeleton.h" #include "ozz/animation/offline/raw_skeleton.h"
#include "ozz/animation/offline/skeleton_builder.h" #include "ozz/animation/offline/skeleton_builder.h"
#include "ozz/animation/runtime/animation.h" #include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/sampling_job.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/io/archive.h" #include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h" #include "ozz/base/log.h"
#include "ozz/base/maths/soa_transform.h"
struct SimpleAnimFixture { struct SimpleAnimFixture {
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = nullptr; ozz::unique_ptr<ozz::animation::Skeleton> skeleton = nullptr;
ozz::animation::offline::RawAnimation raw_animation_translation_x; ozz::animation::offline::RawAnimation raw_animation_translation_x;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_x = nullptr; ozz::unique_ptr<ozz::animation::Animation> animation_translate_x = nullptr;
SyncTrack animation_translate_x_sync_track = {};
ozz::animation::offline::RawAnimation raw_animation_translation_y; ozz::animation::offline::RawAnimation raw_animation_translation_y;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_y = nullptr; ozz::unique_ptr<ozz::animation::Animation> animation_translate_y = nullptr;
SyncTrack animation_translate_y_sync_track = {};
ozz::vector<ozz::math::SoaTransform> animation_output; ozz::vector<ozz::math::SoaTransform> animation_output;
ozz::animation::SamplingJob::Context sampling_context; ozz::animation::SamplingJob::Context sampling_context;
@@ -63,7 +69,7 @@ struct SimpleAnimFixture {
bone0_translations.push_back(translation_key); bone0_translations.push_back(translation_key);
translation_key.time = 1.f; translation_key.time = 1.f;
translation_key.value = ozz::math::Float3(1.f, 0.f, 9.f); translation_key.value = ozz::math::Float3(1.f, 0.f, 0.f);
bone0_translations.push_back(translation_key); bone0_translations.push_back(translation_key);
bone0_track.translations = bone0_translations; bone0_track.translations = bone0_translations;
@@ -120,99 +126,201 @@ TEST_CASE_METHOD(
sampled_translation.z[0] == Approx(translation_key.value.z).margin(0.01)); sampled_translation.z[0] == Approx(translation_key.value.z).margin(0.01));
} }
TEST_CASE("AnimDataPlacementNew", "[AnimGraphEval]") { TEST_CASE("PosePlacementNew", "[AnimGraphEval]") {
int anim_data_size = sizeof(AnimData); int pose_size = sizeof(Pose);
char* buf = new char[anim_data_size]; char* buf = new char[pose_size];
AnimData* anim_data_newed = new AnimData; Pose* pose_newed = new Pose;
anim_data_newed->m_local_matrices.resize(2); pose_newed->m_local_matrices.resize(2);
delete anim_data_newed; delete pose_newed;
AnimData* anim_data_ptr = new (buf) AnimData; Pose* pose_ptr = new (buf) Pose;
anim_data_ptr->m_local_matrices.resize(4); pose_ptr->m_local_matrices.resize(4);
anim_data_ptr->m_local_matrices.resize(0); pose_ptr->m_local_matrices.resize(0);
anim_data_ptr->m_local_matrices.vector::~vector(); pose_ptr->m_local_matrices.vector::~vector();
delete[] buf; delete[] buf;
} }
TEST_CASE_METHOD( TEST_CASE_METHOD(
SimpleAnimFixture, SimpleAnimFixture,
"AnimGraphSimpleEval", "AnimGraphSimpleEval",
"[AnimGraphEvalTests]") { "[AnimGraphEvalTests]") {
AnimGraphBlendTreeResource graph_resource; BlendTreeResource* blend_tree_resource =
dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
// Add nodes // Add nodes
size_t trans_x_node_index = size_t trans_x_node_index =
graph_resource.addNode(AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
size_t trans_y_node_index = size_t trans_y_node_index =
graph_resource.addNode(AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
size_t blend_node_index = size_t blend_node_index =
graph_resource.addNode(AnimNodeResourceFactory("Blend2")); blend_tree_resource->AddNode(AnimNodeResourceFactory("Blend2"));
// Setup nodes // Setup nodes
AnimNodeResource& trans_x_node = graph_resource.m_nodes[trans_x_node_index]; AnimNodeResource* trans_x_node =
trans_x_node.m_socket_accessor->SetPropertyValue("Filename", std::string("trans_x")); blend_tree_resource->GetNode(trans_x_node_index);
trans_x_node.m_name = "trans_x"; trans_x_node->m_virtual_socket_accessor->SetPropertyValue(
"Filename",
std::string("trans_x"));
trans_x_node->m_name = "trans_x";
AnimNodeResource& trans_y_node = graph_resource.m_nodes[trans_y_node_index]; AnimNodeResource* trans_y_node =
trans_y_node.m_socket_accessor->SetPropertyValue("Filename", std::string("trans_y")); blend_tree_resource->GetNode(trans_y_node_index);
trans_y_node.m_name = "trans_y"; trans_y_node->m_virtual_socket_accessor->SetPropertyValue(
"Filename",
std::string("trans_y"));
trans_y_node->m_name = "trans_y";
AnimNodeResource& blend_node = graph_resource.m_nodes[blend_node_index]; AnimNodeResource* blend_node = blend_tree_resource->GetNode(blend_node_index);
blend_node.m_name = "BlendWalkRun"; blend_node->m_name = "BlendWalkRun";
// Setup graph outputs and inputs blend_tree_resource->RegisterBlendTreeInputSocket<float>("GraphFloatInput");
AnimNodeResource& graph_output_node = graph_resource.getGraphOutputNode();
graph_output_node.m_socket_accessor->RegisterInput<AnimData>("GraphOutput", nullptr);
AnimNodeResource& graph_input_node =
graph_resource.getGraphInputNode();
graph_input_node.m_socket_accessor->RegisterOutput<float>(
"GraphFloatInput",
nullptr);
// Wire up nodes // Wire up nodes
graph_resource.connectSockets(trans_x_node, "Output", blend_node, "Input0"); CHECK(blend_tree_resource
graph_resource.connectSockets(trans_y_node, "Output", blend_node, "Input1"); ->ConnectSockets(trans_x_node, "Output", blend_node, "Input0"));
graph_resource.connectSockets( CHECK(blend_tree_resource
->ConnectSockets(trans_y_node, "Output", blend_node, "Input1"));
CHECK(blend_tree_resource->ConnectSockets(
blend_node, blend_node,
"Output", "Output",
graph_resource.getGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput"); "Output"));
REQUIRE(graph_resource.connectSockets(graph_input_node, "GraphFloatInput", blend_node, "Weight"));
CHECK(blend_tree_resource->ConnectSockets(
blend_tree_resource->GetGraphInputNode(),
"GraphFloatInput",
blend_node,
"Weight"));
// Prepare animation maps // Prepare animation maps
AnimGraphContext graph_context; AnimGraphContext graph_context;
graph_context.m_skeleton = skeleton.get(); graph_context.m_skeleton = skeleton.get();
graph_context.m_animation_map["trans_x"] = animation_translate_x.get(); graph_context.m_animation_map["trans_x"] = {
graph_context.m_animation_map["trans_y"] = animation_translate_y.get(); "trans_x",
"",
animation_translate_x.get(),
animation_translate_x_sync_track};
graph_context.m_animation_map["trans_y"] = {
"trans_y",
"",
animation_translate_y.get(),
animation_translate_y_sync_track};
// Instantiate graph // Instantiate graph
AnimGraph graph; AnimGraphBlendTree blend_tree;
graph_resource.createInstance(graph); blend_tree_resource->CreateBlendTreeInstance(blend_tree);
graph.init(graph_context);
blend_tree.Init(graph_context);
// Get runtime graph inputs and outputs // Get runtime graph inputs and outputs
float graph_float_input = 0.f; float graph_float_input = 0.f;
graph.SetInput("GraphFloatInput", &graph_float_input); blend_tree.SetInput("GraphFloatInput", &graph_float_input);
CHECK(blend_tree.GetGraphInputs().size() == 1);
CHECK(
*blend_tree.GetGraphInputs()[0].m_reference.ptr_ptr
== &graph_float_input);
AnimData graph_anim_output; Pose graph_anim_output;
graph_anim_output.m_local_matrices.resize(skeleton->num_joints()); graph_anim_output.m_local_matrices.resize(skeleton->num_joints());
graph.SetOutput("GraphOutput", &graph_anim_output); blend_tree.SetOutput("Output", &graph_anim_output);
CHECK(blend_tree.GetGraphOutputs().size() == 1);
CHECK(
*blend_tree.GetGraphOutputs()[0].m_reference.ptr_ptr
== &graph_anim_output);
WHEN("Blend Weight == 0.") {
// Evaluate graph
graph_float_input = 0.f;
blend_tree.StartUpdateTick();
blend_tree.MarkActiveInputs({});
THEN("Only Blend2 and first input of Blend2 node is active.") {
CHECK(
blend_tree.m_nodes[trans_x_node_index]->m_state
== AnimNodeEvalState::Activated);
CHECK(
blend_tree.m_nodes[trans_y_node_index]->m_state
== AnimNodeEvalState::Deactivated);
CHECK(
blend_tree.m_nodes[blend_node_index]->m_state
== AnimNodeEvalState::Activated);
}
blend_tree.UpdateTime(0.0, 0.5f);
blend_tree.Evaluate(graph_context);
CHECK(
graph_anim_output.m_local_matrices[0].translation.x[0]
== Approx(0.5).margin(0.01));
CHECK(
graph_anim_output.m_local_matrices[0].translation.y[0]
== Approx(0.0).margin(0.01));
}
WHEN("Blend Weight 0.1") {
// Evaluate graph // Evaluate graph
graph_float_input = 0.1f; graph_float_input = 0.1f;
graph.markActiveNodes(); blend_tree.StartUpdateTick();
CHECK(graph.m_nodes[trans_x_node_index]->m_state == AnimNodeEvalState::Activated); blend_tree.MarkActiveInputs({});
CHECK(graph.m_nodes[trans_y_node_index]->m_state == AnimNodeEvalState::Activated);
CHECK(graph.m_nodes[blend_node_index]->m_state == AnimNodeEvalState::Activated);
graph.updateTime(0.5f); THEN("All nodes are active.") {
graph.evaluate(graph_context); CHECK(
blend_tree.m_nodes[trans_x_node_index]->m_state
== AnimNodeEvalState::Activated);
CHECK(
blend_tree.m_nodes[trans_y_node_index]->m_state
== AnimNodeEvalState::Activated);
CHECK(
blend_tree.m_nodes[blend_node_index]->m_state
== AnimNodeEvalState::Activated);
}
CHECK(graph_anim_output.m_local_matrices[0].translation.x[0] == Approx(0.5).margin(0.1)); blend_tree.UpdateTime(0.0, 0.5f);
CHECK(graph_anim_output.m_local_matrices[0].translation.y[0] == Approx(0.05).margin(0.01)); blend_tree.Evaluate(graph_context);
CHECK(
graph_anim_output.m_local_matrices[0].translation.x[0]
== Approx(0.45).margin(0.01));
CHECK(
graph_anim_output.m_local_matrices[0].translation.y[0]
== Approx(0.05).margin(0.01));
}
WHEN("Blend Weight 1.") {
// Evaluate graph
graph_float_input = 1.f;
blend_tree.StartUpdateTick();
blend_tree.MarkActiveInputs({});
THEN("Only Blend2 and second input of Blend2 are active.") {
CHECK(
blend_tree.m_nodes[trans_x_node_index]->m_state
== AnimNodeEvalState::Deactivated);
CHECK(
blend_tree.m_nodes[trans_y_node_index]->m_state
== AnimNodeEvalState::Activated);
CHECK(
blend_tree.m_nodes[blend_node_index]->m_state
== AnimNodeEvalState::Activated);
}
blend_tree.UpdateTime(0.0, 0.5f);
blend_tree.Evaluate(graph_context);
CHECK(
graph_anim_output.m_local_matrices[0].translation.x[0]
== Approx(0.).margin(0.01));
CHECK(
graph_anim_output.m_local_matrices[0].translation.y[0]
== Approx(0.5).margin(0.01));
}
delete blend_tree_resource;
} }
+209 -263
View File
@@ -11,22 +11,26 @@
#include "ozz/base/io/stream.h" #include "ozz/base/io/stream.h"
#include "ozz/base/log.h" #include "ozz/base/log.h"
class SimpleAnimSamplerGraphResource { class BlendTreeResourceFixture {
protected: public:
AnimGraphResource graph_resource; BlendTreeResourceFixture() {
blend_tree_resource =
dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
}
virtual ~BlendTreeResourceFixture() { delete blend_tree_resource; }
BlendTreeResource* blend_tree_resource = nullptr; BlendTreeResource* blend_tree_resource = nullptr;
};
class SimpleAnimSamplerBlendTreeResourceFixture
: public BlendTreeResourceFixture {
protected:
size_t walk_node_index = -1; size_t walk_node_index = -1;
AnimNodeResource* walk_node = nullptr; AnimNodeResource* walk_node = nullptr;
public: public:
SimpleAnimSamplerGraphResource() { SimpleAnimSamplerBlendTreeResourceFixture() {
graph_resource.m_name = "AnimSamplerBlendTree";
graph_resource.m_node_type_name = "BlendTree";
graph_resource.m_graph_type_name = "BlendTree";
blend_tree_resource = &graph_resource.m_blend_tree_resource;
blend_tree_resource->InitGraphConnectors();
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
walk_node_index = walk_node_index =
blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
@@ -37,7 +41,7 @@ class SimpleAnimSamplerGraphResource {
std::string("media/Walking-loop.ozz")); std::string("media/Walking-loop.ozz"));
AnimNodeResource* graph_node = blend_tree_resource->GetGraphOutputNode(); AnimNodeResource* graph_node = blend_tree_resource->GetGraphOutputNode();
graph_node->m_virtual_socket_accessor->RegisterInput<AnimData>( graph_node->m_virtual_socket_accessor->RegisterInput<Pose>(
"GraphOutput", "GraphOutput",
nullptr); nullptr);
@@ -45,14 +49,12 @@ class SimpleAnimSamplerGraphResource {
walk_node, walk_node,
"Output", "Output",
blend_tree_resource->GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput"); AnimGraphResource::DefaultAnimOutput);
} }
}; };
class Blend2GraphResource { class Blend2BlendTreeResource : public BlendTreeResourceFixture {
protected: protected:
AnimGraphResource graph_resource;
BlendTreeResource* blend_tree_resource = nullptr;
size_t walk_node_index = -1; size_t walk_node_index = -1;
size_t run_node_index = -1; size_t run_node_index = -1;
size_t blend_node_index = -1; size_t blend_node_index = -1;
@@ -61,13 +63,8 @@ class Blend2GraphResource {
AnimNodeResource* blend_node = nullptr; AnimNodeResource* blend_node = nullptr;
public: public:
Blend2GraphResource() { Blend2BlendTreeResource() {
graph_resource.m_name = "WalkRunBlendGraph"; blend_tree_resource->m_name = "WalkRunBlendGraph";
graph_resource.m_node_type_name = "BlendTree";
graph_resource.m_graph_type_name = "BlendTree";
blend_tree_resource = &graph_resource.m_blend_tree_resource;
blend_tree_resource->InitGraphConnectors();
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
walk_node_index = walk_node_index =
@@ -93,11 +90,12 @@ class Blend2GraphResource {
blend_node->m_name = "BlendWalkRun"; blend_node->m_name = "BlendWalkRun";
AnimNodeResource* graph_node = blend_tree_resource->GetGraphOutputNode(); AnimNodeResource* graph_node = blend_tree_resource->GetGraphOutputNode();
graph_node->m_virtual_socket_accessor->RegisterInput<AnimData>(
"GraphOutput",
nullptr);
REQUIRE(graph_node->m_virtual_socket_accessor->m_inputs.size() == 1); REQUIRE(graph_node->m_virtual_socket_accessor->m_inputs.size() == 1);
REQUIRE(
graph_node->m_virtual_socket_accessor->m_inputs[0].m_name
== AnimGraphResource::DefaultAnimOutput);
REQUIRE( REQUIRE(
blend_node->m_virtual_socket_accessor->GetInputIndex("Input0") == 0); blend_node->m_virtual_socket_accessor->GetInputIndex("Input0") == 0);
REQUIRE( REQUIRE(
@@ -112,7 +110,7 @@ class Blend2GraphResource {
blend_node, blend_node,
"Output", "Output",
blend_tree_resource->GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput"); AnimGraphResource::DefaultAnimOutput);
} }
}; };
@@ -128,12 +126,8 @@ class Blend2GraphResource {
// | | // | |
// +----------------------------------------+ // +----------------------------------------+
// //
class EmbeddedBlendTreeGraphResource { class EmbeddedBlendTreeGraphResource : public BlendTreeResourceFixture {
protected: protected:
AnimGraphResource parent_graph_resource;
BlendTreeResource* parent_blend_tree_resource = nullptr;
AnimGraphResource* embedded_graph = nullptr;
BlendTreeResource* embedded_blend_tree_resource = nullptr; BlendTreeResource* embedded_blend_tree_resource = nullptr;
size_t walk_node_index = -1; size_t walk_node_index = -1;
@@ -143,25 +137,13 @@ class EmbeddedBlendTreeGraphResource {
public: public:
EmbeddedBlendTreeGraphResource() { EmbeddedBlendTreeGraphResource() {
parent_graph_resource.m_name = "ParentBlendTree"; blend_tree_resource->m_name = "ParentBlendTree";
parent_graph_resource.m_graph_type_name = "BlendTree";
parent_graph_resource.m_node_type_name = "BlendTree";
parent_blend_tree_resource = &parent_graph_resource.m_blend_tree_resource;
parent_blend_tree_resource->Reset();
parent_blend_tree_resource->InitGraphConnectors();
// Setup parent outputs
AnimNodeResource* parent_blend_tree_outputs =
parent_blend_tree_resource->GetGraphOutputNode();
parent_blend_tree_outputs->m_virtual_socket_accessor
->RegisterInput<AnimData>("Output", nullptr);
// Parent AnimSampler // Parent AnimSampler
walk_node_index = parent_blend_tree_resource->AddNode( walk_node_index =
AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
walk_node_resource = parent_blend_tree_resource->GetNode(walk_node_index); walk_node_resource = blend_tree_resource->GetNode(walk_node_index);
walk_node_resource->m_name = "WalkAnim"; walk_node_resource->m_name = "WalkAnim";
walk_node_resource->m_virtual_socket_accessor->SetPropertyValue( walk_node_resource->m_virtual_socket_accessor->SetPropertyValue(
"Filename", "Filename",
@@ -170,26 +152,23 @@ class EmbeddedBlendTreeGraphResource {
// //
// Embedded Tree // Embedded Tree
// //
embedded_blend_tree_node_index = parent_blend_tree_resource->AddNode( embedded_blend_tree_node_index =
AnimNodeResourceFactory("BlendTree")); blend_tree_resource->AddNode(AnimNodeResourceFactory("BlendTree"));
embedded_graph = dynamic_cast<AnimGraphResource*>( embedded_blend_tree_resource = dynamic_cast<BlendTreeResource*>(
parent_blend_tree_resource->GetNode(embedded_blend_tree_node_index)); blend_tree_resource->GetNode(embedded_blend_tree_node_index));
embedded_graph->m_name = "EmbeddedBlendTree"; embedded_blend_tree_resource->m_name = "EmbeddedBlendTree";
embedded_graph->m_node_type_name = "BlendTree";
embedded_graph->m_graph_type_name = "BlendTree";
embedded_blend_tree_resource = &embedded_graph->m_blend_tree_resource;
// Embedded: outputs // Embedded: outputs
AnimNodeResource* embedded_outputs = AnimNodeResource* embedded_outputs =
embedded_blend_tree_resource->GetGraphOutputNode(); embedded_blend_tree_resource->GetGraphOutputNode();
embedded_outputs->m_virtual_socket_accessor->RegisterInput<AnimData>( embedded_outputs->m_virtual_socket_accessor->RegisterInput<Pose>(
"AnimOutput", "AnimOutput",
nullptr); nullptr);
// Embedded: inputs // Embedded: inputs
AnimNodeResource* embedded_inputs = AnimNodeResource* embedded_inputs =
embedded_blend_tree_resource->GetGraphInputNode(); embedded_blend_tree_resource->GetGraphInputNode();
embedded_inputs->m_virtual_socket_accessor->RegisterOutput<AnimData>( embedded_inputs->m_virtual_socket_accessor->RegisterOutput<Pose>(
"AnimInput", "AnimInput",
nullptr); nullptr);
@@ -215,13 +194,16 @@ class EmbeddedBlendTreeGraphResource {
"AnimOutput"); "AnimOutput");
// Parent: setup connections // Parent: setup connections
REQUIRE(parent_blend_tree_resource->ConnectSockets( const AnimNodeResource* parent_blend_tree_outputs =
blend_tree_resource->GetGraphOutputNode();
REQUIRE(blend_tree_resource->ConnectSockets(
walk_node_resource, walk_node_resource,
"Output", "Output",
embedded_graph, embedded_blend_tree_resource,
"AnimInput")); "AnimInput"));
REQUIRE(parent_blend_tree_resource->ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
embedded_graph, embedded_blend_tree_resource,
"AnimOutput", "AnimOutput",
parent_blend_tree_outputs, parent_blend_tree_outputs,
"Output")); "Output"));
@@ -241,12 +223,8 @@ class EmbeddedBlendTreeGraphResource {
// | | // | |
// +----------------------------------------+ // +----------------------------------------+
// //
class EmbeddedTreeBlend2GraphResource { class EmbeddedTreeBlend2GraphResource : public BlendTreeResourceFixture {
protected: protected:
AnimGraphResource parent_graph_resource;
BlendTreeResource* parent_blend_tree_resource = nullptr;
AnimGraphResource* embedded_graph = nullptr;
BlendTreeResource* embedded_blend_tree_resource = nullptr; BlendTreeResource* embedded_blend_tree_resource = nullptr;
size_t walk_node_index = -1; size_t walk_node_index = -1;
@@ -260,32 +238,20 @@ class EmbeddedTreeBlend2GraphResource {
public: public:
EmbeddedTreeBlend2GraphResource() { EmbeddedTreeBlend2GraphResource() {
parent_graph_resource.m_name = "ParentBlendTree"; blend_tree_resource->m_name = "ParentBlendTree";
parent_graph_resource.m_graph_type_name = "BlendTree";
parent_graph_resource.m_node_type_name = "BlendTree";
parent_blend_tree_resource = &parent_graph_resource.m_blend_tree_resource;
parent_blend_tree_resource->Reset();
parent_blend_tree_resource->InitGraphConnectors();
// Setup parent outputs
AnimNodeResource* parent_blend_tree_outputs =
parent_blend_tree_resource->GetGraphOutputNode();
parent_blend_tree_outputs->m_virtual_socket_accessor
->RegisterInput<AnimData>("Output", nullptr);
// Setup parent inputs // Setup parent inputs
AnimNodeResource* parent_blend_tree_inputs = AnimNodeResource* parent_blend_tree_inputs =
parent_blend_tree_resource->GetGraphInputNode(); blend_tree_resource->GetGraphInputNode();
parent_blend_tree_inputs->m_virtual_socket_accessor->RegisterOutput<float>( parent_blend_tree_inputs->m_virtual_socket_accessor->RegisterOutput<float>(
"EmbeddedBlend2Weight", "EmbeddedBlend2Weight",
nullptr); nullptr);
// Parent AnimSampler // Parent AnimSampler
walk_node_index = parent_blend_tree_resource->AddNode( walk_node_index =
AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
walk_node_resource = parent_blend_tree_resource->GetNode(walk_node_index); walk_node_resource = blend_tree_resource->GetNode(walk_node_index);
walk_node_resource->m_name = "WalkAnim"; walk_node_resource->m_name = "WalkAnim";
walk_node_resource->m_virtual_socket_accessor->SetPropertyValue( walk_node_resource->m_virtual_socket_accessor->SetPropertyValue(
"Filename", "Filename",
@@ -294,21 +260,17 @@ class EmbeddedTreeBlend2GraphResource {
// //
// Embedded Tree // Embedded Tree
// //
embedded_blend_tree_node_index = parent_blend_tree_resource->AddNode( embedded_blend_tree_node_index =
AnimNodeResourceFactory("BlendTree")); blend_tree_resource->AddNode(AnimNodeResourceFactory("BlendTree"));
embedded_graph = dynamic_cast<AnimGraphResource*>( embedded_blend_tree_resource = dynamic_cast<BlendTreeResource*>(
parent_blend_tree_resource->GetNode(embedded_blend_tree_node_index)); blend_tree_resource->GetNode(embedded_blend_tree_node_index));
embedded_graph->m_name = "EmbeddedTreeBlend2GraphResource"; embedded_blend_tree_resource->m_name = "EmbeddedTreeBlend2GraphResource";
embedded_graph->m_node_type_name = "BlendTree";
embedded_graph->m_graph_type_name = "BlendTree";
embedded_blend_tree_resource = &embedded_graph->m_blend_tree_resource;
// Embedded: outputs
embedded_graph->RegisterBlendTreeOutputSocket<AnimData>("AnimOutput");
// Embedded: inputs // Embedded: inputs
embedded_graph->RegisterBlendTreeInputSocket<AnimData>("AnimInput"); embedded_blend_tree_resource->RegisterBlendTreeInputSocket<Pose>(
embedded_graph->RegisterBlendTreeInputSocket<float>("BlendWeight"); "AnimInput");
embedded_blend_tree_resource->RegisterBlendTreeInputSocket<float>(
"BlendWeight");
// Embedded nodes // Embedded nodes
embedded_blend2_node_index = embedded_blend_tree_resource->AddNode( embedded_blend2_node_index = embedded_blend_tree_resource->AddNode(
@@ -346,7 +308,7 @@ class EmbeddedTreeBlend2GraphResource {
embedded_blend2_node_resource, embedded_blend2_node_resource,
"Output", "Output",
embedded_blend_tree_resource->GetGraphOutputNode(), embedded_blend_tree_resource->GetGraphOutputNode(),
"AnimOutput")); AnimGraphResource::DefaultAnimOutput));
REQUIRE(embedded_blend_tree_resource->ConnectSockets( REQUIRE(embedded_blend_tree_resource->ConnectSockets(
embedded_blend_tree_resource->GetGraphInputNode(), embedded_blend_tree_resource->GetGraphInputNode(),
"BlendWeight", "BlendWeight",
@@ -354,20 +316,23 @@ class EmbeddedTreeBlend2GraphResource {
"Weight")); "Weight"));
// Parent: setup connections // Parent: setup connections
REQUIRE(parent_blend_tree_resource->ConnectSockets( AnimNodeResource* parent_blend_tree_outputs =
blend_tree_resource->GetGraphOutputNode();
REQUIRE(blend_tree_resource->ConnectSockets(
walk_node_resource, walk_node_resource,
"Output", "Output",
embedded_graph, embedded_blend_tree_resource,
"AnimInput")); "AnimInput"));
REQUIRE(parent_blend_tree_resource->ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
embedded_graph, embedded_blend_tree_resource,
"AnimOutput", AnimGraphResource::DefaultAnimOutput,
parent_blend_tree_outputs, parent_blend_tree_outputs,
"Output")); AnimGraphResource::DefaultAnimOutput));
REQUIRE(parent_blend_tree_resource->ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
parent_blend_tree_inputs, parent_blend_tree_inputs,
"EmbeddedBlend2Weight", "EmbeddedBlend2Weight",
embedded_graph, embedded_blend_tree_resource,
"BlendWeight")); "BlendWeight"));
} }
}; };
@@ -396,6 +361,14 @@ bool load_skeleton(ozz::animation::Skeleton& skeleton, const char* filename) {
void CheckBlendTreeResourcesEqual( void CheckBlendTreeResourcesEqual(
const BlendTreeResource* blend_tree_resource_reference, const BlendTreeResource* blend_tree_resource_reference,
const BlendTreeResource* blend_tree_resource_rhs) { const BlendTreeResource* blend_tree_resource_rhs) {
REQUIRE(
blend_tree_resource_reference->m_node_type_name
== blend_tree_resource_rhs->m_node_type_name);
REQUIRE(blend_tree_resource_reference->m_node_type_name == "BlendTree");
REQUIRE(
blend_tree_resource_reference->m_name == blend_tree_resource_rhs->m_name);
REQUIRE( REQUIRE(
blend_tree_resource_reference->GetNumNodes() blend_tree_resource_reference->GetNumNodes()
== blend_tree_resource_rhs->GetNumNodes()); == blend_tree_resource_rhs->GetNumNodes());
@@ -430,27 +403,6 @@ void CheckBlendTreeResourcesEqual(
} }
} }
void CheckAnimGraphResourceEqual(
const AnimGraphResource& graph_resource_reference,
const AnimGraphResource& graph_resource_rhs) {
REQUIRE(
graph_resource_reference.m_graph_type_name
== graph_resource_rhs.m_graph_type_name);
REQUIRE(graph_resource_reference.m_name == graph_resource_rhs.m_name);
REQUIRE(graph_resource_reference.m_graph_type_name == "BlendTree");
const BlendTreeResource* blend_tree_resource_reference =
&graph_resource_reference.m_blend_tree_resource;
const BlendTreeResource* blend_tree_resource_rhs =
&graph_resource_rhs.m_blend_tree_resource;
CheckBlendTreeResourcesEqual(
blend_tree_resource_reference,
blend_tree_resource_rhs);
}
TEST_CASE("InputAttributeConversion", "[AnimGraphResource]") { TEST_CASE("InputAttributeConversion", "[AnimGraphResource]") {
int node_id = 3321; int node_id = 3321;
int input_index = 221; int input_index = 221;
@@ -471,24 +423,26 @@ TEST_CASE("InputAttributeConversion", "[AnimGraphResource]") {
} }
TEST_CASE_METHOD( TEST_CASE_METHOD(
SimpleAnimSamplerGraphResource, SimpleAnimSamplerBlendTreeResourceFixture,
"SimpleAnimSamplerGraphResource saving and loading results in same " "SimpleAnimSamplerGraphResource saving and loading results in same "
"resource", "resource",
"[SimpleAnimSamplerGraphResource]") { "[SimpleAnimSamplerGraphResource]") {
graph_resource.SaveToFile("TestGraphAnimSamplerBlendTree.json"); blend_tree_resource->SaveToFile("TestGraphAnimSamplerBlendTree.json");
AnimGraphResource graph_resource_loaded; std::unique_ptr<BlendTreeResource> blend_tree_resource_loaded(
graph_resource_loaded.LoadFromFile("TestGraphAnimSamplerBlendTree.json"); BlendTreeResource::CreateFromFile("TestGraphAnimSamplerBlendTree.json"));
CheckAnimGraphResourceEqual(graph_resource, graph_resource_loaded); CheckBlendTreeResourcesEqual(
blend_tree_resource,
blend_tree_resource_loaded.get());
} }
TEST_CASE_METHOD( TEST_CASE_METHOD(
SimpleAnimSamplerGraphResource, SimpleAnimSamplerBlendTreeResourceFixture,
"SimpleAnimSamplerGraphResource emulated evaluation", "SimpleAnimSamplerGraphResource emulated evaluation",
"[SimpleAnimSamplerGraphResource]") { "[SimpleAnimSamplerGraphResource]") {
AnimGraphBlendTree anim_graph_blend_tree; AnimGraphBlendTree anim_graph_blend_tree;
graph_resource.CreateBlendTreeInstance(anim_graph_blend_tree); blend_tree_resource->CreateBlendTreeInstance(anim_graph_blend_tree);
AnimGraphContext graph_context; AnimGraphContext graph_context;
ozz::animation::Skeleton skeleton; ozz::animation::Skeleton skeleton;
@@ -528,9 +482,11 @@ TEST_CASE_METHOD(
REQUIRE(anim_sampler_walk->m_animation != nullptr); REQUIRE(anim_sampler_walk->m_animation != nullptr);
// Ensure that outputs are properly propagated. // Ensure that outputs are properly propagated.
AnimData output; Pose output;
output.m_local_matrices.resize(skeleton.num_soa_joints()); output.m_local_matrices.resize(skeleton.num_soa_joints());
anim_graph_blend_tree.SetOutput("GraphOutput", &output); anim_graph_blend_tree.SetOutput(
AnimGraphResource::DefaultAnimOutput,
&output);
REQUIRE(anim_sampler_walk->o_output == &output); REQUIRE(anim_sampler_walk->o_output == &output);
WHEN("Emulating Graph Evaluation") { WHEN("Emulating Graph Evaluation") {
@@ -545,60 +501,49 @@ TEST_CASE_METHOD(
// Checks that node const inputs are properly set. // Checks that node const inputs are properly set.
// //
TEST_CASE("AnimSamplerSpeedScaleGraph", "[AnimGraphResource]") { TEST_CASE("AnimSamplerSpeedScaleGraph", "[AnimGraphResource]") {
AnimGraphResource graph_resource; BlendTreeResource* blend_tree_resource =
graph_resource.m_name = "AnimSamplerSpeedScaleGraph"; dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
graph_resource.m_graph_type_name = "BlendTree"; blend_tree_resource->m_name = "AnimSamplerSpeedScaleBlendTree";
BlendTreeResource& blend_tree_resource = graph_resource.m_blend_tree_resource;
blend_tree_resource.Reset();
blend_tree_resource.InitGraphConnectors();
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
size_t walk_node_index = size_t walk_node_index =
blend_tree_resource.AddNode(AnimNodeResourceFactory("AnimSampler")); blend_tree_resource->AddNode(AnimNodeResourceFactory("AnimSampler"));
size_t speed_scale_node_index = size_t speed_scale_node_index =
blend_tree_resource.AddNode(AnimNodeResourceFactory("SpeedScale")); blend_tree_resource->AddNode(AnimNodeResourceFactory("SpeedScale"));
AnimNodeResource* walk_node = blend_tree_resource.GetNode(walk_node_index); AnimNodeResource* walk_node = blend_tree_resource->GetNode(walk_node_index);
walk_node->m_name = "WalkAnim"; walk_node->m_name = "WalkAnim";
walk_node->m_virtual_socket_accessor->SetPropertyValue( walk_node->m_virtual_socket_accessor->SetPropertyValue(
"Filename", "Filename",
std::string("media/Walking-loop.ozz")); std::string("media/Walking-loop.ozz"));
AnimNodeResource* speed_scale_node = AnimNodeResource* speed_scale_node =
blend_tree_resource.GetNode(speed_scale_node_index); blend_tree_resource->GetNode(speed_scale_node_index);
speed_scale_node->m_name = "SpeedScale"; speed_scale_node->m_name = "SpeedScale";
float speed_scale_value = 1.35f; float speed_scale_value = 1.35f;
speed_scale_node->m_virtual_socket_accessor->SetInputValue( speed_scale_node->m_virtual_socket_accessor->SetInputValue(
"SpeedScale", "SpeedScale",
speed_scale_value); speed_scale_value);
AnimNodeResource* graph_node = blend_tree_resource.GetGraphOutputNode();
graph_node->m_virtual_socket_accessor->RegisterInput<AnimData>(
"GraphOutput",
nullptr);
blend_tree_resource blend_tree_resource
.ConnectSockets(walk_node, "Output", speed_scale_node, "Input"); ->ConnectSockets(walk_node, "Output", speed_scale_node, "Input");
blend_tree_resource.ConnectSockets( blend_tree_resource->ConnectSockets(
speed_scale_node, speed_scale_node,
"Output", "Output",
blend_tree_resource.GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput"); AnimGraphResource::DefaultAnimOutput);
graph_resource.SaveToFile( constexpr char filename[] =
"TestGraphAnimSamplerSpeedScaleGraph.animgraph.json"); "TestGraphAnimSamplerSpeedScaleGraph.animgraph.json";
AnimGraphResource graph_resource_loaded;
graph_resource_loaded.LoadFromFile(
"TestGraphAnimSamplerSpeedScaleGraph.animgraph.json");
BlendTreeResource& blend_tree_resource_loaded = REQUIRE(blend_tree_resource->SaveToFile(filename));
graph_resource_loaded.m_blend_tree_resource; BlendTreeResource* blend_tree_resource_loaded =
BlendTreeResource::CreateFromFile(filename);
Socket* speed_scale_resource_loaded_input = Socket* speed_scale_resource_loaded_input =
blend_tree_resource_loaded.GetNode(speed_scale_node_index) blend_tree_resource_loaded->GetNode(speed_scale_node_index)
->m_virtual_socket_accessor->GetInputSocket("SpeedScale"); ->m_virtual_socket_accessor->GetInputSocket("SpeedScale");
REQUIRE(speed_scale_resource_loaded_input != nullptr); REQUIRE(speed_scale_resource_loaded_input != nullptr);
@@ -607,7 +552,7 @@ TEST_CASE("AnimSamplerSpeedScaleGraph", "[AnimGraphResource]") {
Catch::Matchers::WithinAbs(speed_scale_value, 0.1)); Catch::Matchers::WithinAbs(speed_scale_value, 0.1));
AnimGraphBlendTree blend_tree; AnimGraphBlendTree blend_tree;
graph_resource_loaded.CreateBlendTreeInstance(blend_tree); blend_tree_resource_loaded->CreateBlendTreeInstance(blend_tree);
REQUIRE_THAT( REQUIRE_THAT(
*dynamic_cast<SpeedScaleNode*>(blend_tree.m_nodes[speed_scale_node_index]) *dynamic_cast<SpeedScaleNode*>(blend_tree.m_nodes[speed_scale_node_index])
@@ -616,7 +561,7 @@ TEST_CASE("AnimSamplerSpeedScaleGraph", "[AnimGraphResource]") {
WHEN("Checking node eval order and node subtrees") { WHEN("Checking node eval order and node subtrees") {
const std::vector<size_t>& eval_order = const std::vector<size_t>& eval_order =
graph_resource_loaded.m_blend_tree_resource.GetNodeEvalOrder(); blend_tree_resource_loaded->GetNodeEvalOrder();
THEN("Walk node gets evaluated before speed scale node") { THEN("Walk node gets evaluated before speed scale node") {
CHECK(eval_order.size() == 2); CHECK(eval_order.size() == 2);
@@ -626,23 +571,26 @@ TEST_CASE("AnimSamplerSpeedScaleGraph", "[AnimGraphResource]") {
THEN("Subtree of the speed scale node contains only the walk node") { THEN("Subtree of the speed scale node contains only the walk node") {
CHECK( CHECK(
graph_resource_loaded.m_blend_tree_resource blend_tree_resource_loaded
.m_node_inputs_subtree[speed_scale_node_index] ->m_node_inputs_subtree[speed_scale_node_index]
.size() .size()
== 1); == 1);
CHECK( CHECK(
graph_resource_loaded.m_blend_tree_resource blend_tree_resource_loaded
.m_node_inputs_subtree[speed_scale_node_index][0] ->m_node_inputs_subtree[speed_scale_node_index][0]
== walk_node_index); == walk_node_index);
} }
} }
delete blend_tree_resource_loaded;
delete blend_tree_resource;
} }
// //
// Checks that connections additions and removals are properly validated. // Checks that connections additions and removals are properly validated.
// //
TEST_CASE_METHOD( TEST_CASE_METHOD(
Blend2GraphResource, Blend2BlendTreeResource,
"Connectivity Tests", "Connectivity Tests",
"[AnimGraphResource][Blend2GraphResource]") { "[AnimGraphResource][Blend2GraphResource]") {
INFO("Removing Blend2 -> Output Connection") INFO("Removing Blend2 -> Output Connection")
@@ -651,7 +599,7 @@ TEST_CASE_METHOD(
blend_node, blend_node,
"Output", "Output",
blend_tree_resource->GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput") AnimGraphResource::DefaultAnimOutput)
== true); == true);
CHECK(blend_tree_resource->GetNodeEvalOrder().empty()); CHECK(blend_tree_resource->GetNodeEvalOrder().empty());
@@ -667,7 +615,7 @@ TEST_CASE_METHOD(
speed_scale_node_resource, speed_scale_node_resource,
"Output", "Output",
blend_tree_resource->GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput") AnimGraphResource::DefaultAnimOutput)
== true); == true);
const std::vector<size_t>& tree_eval_order = const std::vector<size_t>& tree_eval_order =
@@ -694,7 +642,7 @@ TEST_CASE_METHOD(
speed_scale_node_resource, speed_scale_node_resource,
"Output", "Output",
blend_tree_resource->GetGraphOutputNode(), blend_tree_resource->GetGraphOutputNode(),
"GraphOutput")); AnimGraphResource::DefaultAnimOutput));
CHECK(blend_tree_resource CHECK(blend_tree_resource
->DisconnectSockets(walk_node, "Output", blend_node, "Input0")); ->DisconnectSockets(walk_node, "Output", blend_node, "Input0"));
CHECK( CHECK(
@@ -706,19 +654,27 @@ TEST_CASE_METHOD(
== false); == false);
} }
TEST_CASE("FreeAnimGraphResource", "[Test]") {
AnimGraphResourcePtr graph_resource(
dynamic_cast<AnimGraphResource*>(AnimNodeResourceFactory("BlendTree")));
graph_resource->SaveToFile("UniqueSaveToFile.json");
AnimGraphResourcePtr graph_resource_loaded(
AnimGraphResource::CreateFromFile("UniqueSaveToFile.json"));
}
TEST_CASE_METHOD( TEST_CASE_METHOD(
Blend2GraphResource, Blend2BlendTreeResource,
"Blend2GraphResource saving and loading results in same resource", "Blend2GraphResource saving and loading results in same resource",
"[Blend2GraphResource]") { "[Blend2GraphResource]") {
graph_resource.SaveToFile("TestGraphBlend2Graph.animgraph.json"); constexpr char filename[] = "TestGraphBlend2Graph.animgraph.json";
AnimGraphResource graph_resource_loaded;
graph_resource_loaded.LoadFromFile("TestGraphBlend2Graph.animgraph.json");
CheckAnimGraphResourceEqual(graph_resource, graph_resource_loaded);
REQUIRE(blend_tree_resource->SaveToFile(filename));
BlendTreeResource* blend_tree_resource_loaded = BlendTreeResource* blend_tree_resource_loaded =
&graph_resource_loaded.m_blend_tree_resource; BlendTreeResource::CreateFromFile(filename);
CheckBlendTreeResourcesEqual(blend_tree_resource, blend_tree_resource_loaded);
// Check that the constant weight of the Blend2 node was properly applied when // Check that the constant weight of the Blend2 node was properly applied when
// loading the resource. // loading the resource.
@@ -732,14 +688,16 @@ TEST_CASE_METHOD(
Catch::Matchers::WithinAbs( Catch::Matchers::WithinAbs(
blend2_node_descriptor_loaded->GetInputValue<float>("Weight"), blend2_node_descriptor_loaded->GetInputValue<float>("Weight"),
0.01)); 0.01));
delete blend_tree_resource_loaded;
} }
TEST_CASE_METHOD( TEST_CASE_METHOD(
Blend2GraphResource, Blend2BlendTreeResource,
"Blend2GraphResource graph unsynced evaluation", "Blend2GraphResource graph unsynced evaluation",
"[Blend2GraphResource]") { "[Blend2GraphResource]") {
AnimGraphBlendTree blend_tree_graph; AnimGraphBlendTree blend_tree_graph;
graph_resource.CreateBlendTreeInstance(blend_tree_graph); blend_tree_resource->CreateBlendTreeInstance(blend_tree_graph);
AnimGraphContext graph_context; AnimGraphContext graph_context;
ozz::animation::Skeleton skeleton; ozz::animation::Skeleton skeleton;
@@ -806,8 +764,8 @@ TEST_CASE_METHOD(
CHECK(blend2_instance->i_input0 == anim_sampler_walk->o_output); CHECK(blend2_instance->i_input0 == anim_sampler_walk->o_output);
CHECK(blend2_instance->i_input1 == anim_sampler_run->o_output); CHECK(blend2_instance->i_input1 == anim_sampler_run->o_output);
AnimData* graph_output = static_cast<AnimData*>( Pose* graph_output = blend_tree_graph.GetOutputPtr<Pose>(
blend_tree_graph.GetOutputPtr<AnimData>("GraphOutput")); AnimGraphResource::DefaultAnimOutput);
CHECK( CHECK(
graph_output->m_local_matrices.size() graph_output->m_local_matrices.size()
@@ -837,21 +795,16 @@ TEST_CASE_METHOD(
// //
// //
TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") { TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
AnimGraphResource graph_resource_origin; BlendTreeResource* blend_tree_resource =
graph_resource_origin.m_name = "TestInputOutputGraph"; dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
graph_resource_origin.m_graph_type_name = "BlendTree"; blend_tree_resource->m_name = "TestInputOutputGraph";
BlendTreeResource& blend_tree_resource =
graph_resource_origin.m_blend_tree_resource;
blend_tree_resource.Reset();
blend_tree_resource.InitGraphConnectors();
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
size_t float_to_vec3_node_index = blend_tree_resource.AddNode( size_t float_to_vec3_node_index = blend_tree_resource->AddNode(
AnimNodeResourceFactory("MathFloatToVec3Node")); AnimNodeResourceFactory("MathFloatToVec3Node"));
AnimNodeResource* graph_output_node = AnimNodeResource* graph_output_node =
blend_tree_resource.GetGraphOutputNode(); blend_tree_resource->GetGraphOutputNode();
graph_output_node->m_virtual_socket_accessor->RegisterInput<float>( graph_output_node->m_virtual_socket_accessor->RegisterInput<float>(
"GraphFloatOutput", "GraphFloatOutput",
nullptr); nullptr);
@@ -860,38 +813,38 @@ TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
nullptr); nullptr);
AnimNodeResource* graph_input_node_resource = AnimNodeResource* graph_input_node_resource =
blend_tree_resource.GetGraphInputNode(); blend_tree_resource->GetGraphInputNode();
graph_input_node_resource->m_virtual_socket_accessor->RegisterOutput<float>( graph_input_node_resource->m_virtual_socket_accessor->RegisterOutput<float>(
"GraphFloatInput", "GraphFloatInput",
nullptr); nullptr);
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
AnimNodeResource* float_to_vec3_node_resource = AnimNodeResource* float_to_vec3_node_resource =
blend_tree_resource.GetNode(float_to_vec3_node_index); blend_tree_resource->GetNode(float_to_vec3_node_index);
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node_resource, graph_input_node_resource,
"GraphFloatInput", "GraphFloatInput",
graph_output_node, graph_output_node,
"GraphFloatOutput")); "GraphFloatOutput"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node_resource, graph_input_node_resource,
"GraphFloatInput", "GraphFloatInput",
float_to_vec3_node_resource, float_to_vec3_node_resource,
"Input0")); "Input0"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node_resource, graph_input_node_resource,
"GraphFloatInput", "GraphFloatInput",
float_to_vec3_node_resource, float_to_vec3_node_resource,
"Input1")); "Input1"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node_resource, graph_input_node_resource,
"GraphFloatInput", "GraphFloatInput",
float_to_vec3_node_resource, float_to_vec3_node_resource,
"Input2")); "Input2"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
float_to_vec3_node_resource, float_to_vec3_node_resource,
"Output", "Output",
graph_output_node, graph_output_node,
@@ -899,18 +852,15 @@ TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
WHEN("Saving and loading graph resource") { WHEN("Saving and loading graph resource") {
const char* filename = "TestGraphResourceSaveLoadGraphInputs.json"; const char* filename = "TestGraphResourceSaveLoadGraphInputs.json";
graph_resource_origin.SaveToFile(filename); REQUIRE(blend_tree_resource->SaveToFile(filename));
AnimGraphResource graph_resource_loaded; BlendTreeResource* blend_tree_resource_loaded =
graph_resource_loaded.LoadFromFile(filename); BlendTreeResource::CreateFromFile(filename);
BlendTreeResource& graph_blend_tree_loaded =
graph_resource_loaded.m_blend_tree_resource;
const AnimNodeResource* graph_loaded_output_node = const AnimNodeResource* graph_loaded_output_node =
graph_blend_tree_loaded.GetGraphOutputNode(); blend_tree_resource_loaded->GetGraphOutputNode();
const AnimNodeResource* graph_loaded_input_node = const AnimNodeResource* graph_loaded_input_node =
graph_blend_tree_loaded.GetGraphInputNode(); blend_tree_resource_loaded->GetGraphInputNode();
THEN("Graph inputs and outputs must be in loaded resource as well.") { THEN("Graph inputs and outputs must be in loaded resource as well.") {
REQUIRE( REQUIRE(
@@ -939,7 +889,7 @@ TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
WHEN("Instantiating an AnimGraph") { WHEN("Instantiating an AnimGraph") {
AnimGraphBlendTree blend_tree_node; AnimGraphBlendTree blend_tree_node;
graph_resource_loaded.CreateBlendTreeInstance(blend_tree_node); blend_tree_resource_loaded->CreateBlendTreeInstance(blend_tree_node);
float graph_float_input = 123.456f; float graph_float_input = 123.456f;
blend_tree_node.SetInput("GraphFloatInput", &graph_float_input); blend_tree_node.SetInput("GraphFloatInput", &graph_float_input);
@@ -979,7 +929,11 @@ TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
} }
} }
} }
delete blend_tree_resource_loaded;
} }
delete blend_tree_resource;
} }
// //
@@ -996,23 +950,18 @@ TEST_CASE("ResourceSaveLoadMathGraphInputs", "[AnimGraphResource]") {
// GraphFloat1Output -> GraphFLoatInputSingle * 3 // GraphFloat1Output -> GraphFLoatInputSingle * 3
// //
TEST_CASE("SimpleMathEvaluations", "[AnimGraphResource]") { TEST_CASE("SimpleMathEvaluations", "[AnimGraphResource]") {
AnimGraphResource graph_resource_origin; BlendTreeResource* blend_tree_resource =
graph_resource_origin.m_name = "TestSimpleMathGraph"; dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
graph_resource_origin.m_graph_type_name = "BlendTree"; blend_tree_resource->m_name = "TestSimpleMathGraph";
BlendTreeResource& blend_tree_resource =
graph_resource_origin.m_blend_tree_resource;
blend_tree_resource.Reset();
blend_tree_resource.InitGraphConnectors();
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
size_t math_add0_node_index = size_t math_add0_node_index =
blend_tree_resource.AddNode(AnimNodeResourceFactory("MathAddNode")); blend_tree_resource->AddNode(AnimNodeResourceFactory("MathAddNode"));
size_t math_add1_node_index = size_t math_add1_node_index =
blend_tree_resource.AddNode(AnimNodeResourceFactory("MathAddNode")); blend_tree_resource->AddNode(AnimNodeResourceFactory("MathAddNode"));
AnimNodeResource* graph_output_node = AnimNodeResource* graph_output_node =
blend_tree_resource.GetGraphOutputNode(); blend_tree_resource->GetGraphOutputNode();
graph_output_node->m_virtual_socket_accessor->RegisterInput<float>( graph_output_node->m_virtual_socket_accessor->RegisterInput<float>(
"GraphFloat0Output", "GraphFloat0Output",
@@ -1024,57 +973,57 @@ TEST_CASE("SimpleMathEvaluations", "[AnimGraphResource]") {
"GraphFloat2Output", "GraphFloat2Output",
nullptr); nullptr);
AnimNodeResource* graph_input_node = blend_tree_resource.GetGraphInputNode(); AnimNodeResource* graph_input_node = blend_tree_resource->GetGraphInputNode();
graph_input_node->m_virtual_socket_accessor->RegisterOutput<float>( graph_input_node->m_virtual_socket_accessor->RegisterOutput<float>(
"GraphFloatInput", "GraphFloatInput",
nullptr); nullptr);
// Prepare graph inputs and outputs // Prepare graph inputs and outputs
AnimNodeResource* math_add0_node = AnimNodeResource* math_add0_node =
blend_tree_resource.GetNode(math_add0_node_index); blend_tree_resource->GetNode(math_add0_node_index);
AnimNodeResource* math_add1_node = AnimNodeResource* math_add1_node =
blend_tree_resource.GetNode(math_add1_node_index); blend_tree_resource->GetNode(math_add1_node_index);
// direct output // direct output
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node, graph_input_node,
"GraphFloatInput", "GraphFloatInput",
graph_output_node, graph_output_node,
"GraphFloat0Output")); "GraphFloat0Output"));
// add0 node // add0 node
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node, graph_input_node,
"GraphFloatInput", "GraphFloatInput",
math_add0_node, math_add0_node,
"Input0")); "Input0"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node, graph_input_node,
"GraphFloatInput", "GraphFloatInput",
math_add0_node, math_add0_node,
"Input1")); "Input1"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
math_add0_node, math_add0_node,
"Output", "Output",
graph_output_node, graph_output_node,
"GraphFloat1Output")); "GraphFloat1Output"));
// add1 node // add1 node
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
math_add0_node, math_add0_node,
"Output", "Output",
math_add1_node, math_add1_node,
"Input0")); "Input0"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
graph_input_node, graph_input_node,
"GraphFloatInput", "GraphFloatInput",
math_add1_node, math_add1_node,
"Input1")); "Input1"));
REQUIRE(blend_tree_resource.ConnectSockets( REQUIRE(blend_tree_resource->ConnectSockets(
math_add1_node, math_add1_node,
"Output", "Output",
graph_output_node, graph_output_node,
@@ -1082,14 +1031,14 @@ TEST_CASE("SimpleMathEvaluations", "[AnimGraphResource]") {
WHEN("Saving and loading graph resource") { WHEN("Saving and loading graph resource") {
const char* filename = "TestGraphResourceSaveLoadGraphInputs.json"; const char* filename = "TestGraphResourceSaveLoadGraphInputs.json";
graph_resource_origin.SaveToFile(filename); REQUIRE(blend_tree_resource->SaveToFile(filename));
AnimGraphResource graph_resource_loaded; BlendTreeResource* blend_tree_resource_loaded =
graph_resource_loaded.LoadFromFile(filename); BlendTreeResource::CreateFromFile(filename);
WHEN("Instantiating an AnimGraph") { WHEN("Instantiating an AnimGraph") {
AnimGraphBlendTree blend_tree; AnimGraphBlendTree blend_tree;
graph_resource_loaded.CreateBlendTreeInstance(blend_tree); blend_tree_resource_loaded->CreateBlendTreeInstance(blend_tree);
float graph_float_input = 123.456f; float graph_float_input = 123.456f;
blend_tree.SetInput("GraphFloatInput", &graph_float_input); blend_tree.SetInput("GraphFloatInput", &graph_float_input);
@@ -1123,7 +1072,11 @@ TEST_CASE("SimpleMathEvaluations", "[AnimGraphResource]") {
context.freeAnimations(); context.freeAnimations();
} }
} }
delete blend_tree_resource_loaded;
} }
delete blend_tree_resource;
} }
// //
@@ -1143,33 +1096,28 @@ TEST_CASE_METHOD(
"EmbeddedBlendTreeGraphResource saving and loading results in same " "EmbeddedBlendTreeGraphResource saving and loading results in same "
"resource", "resource",
"[EmbeddedBlendTreeGraphResource]") { "[EmbeddedBlendTreeGraphResource]") {
parent_graph_resource.SaveToFile("TestGraphEmbeddedBlendTree.json"); constexpr char filename[] = "TestGraphEmbeddedBlendTree.json";
REQUIRE(blend_tree_resource->SaveToFile(filename));
AnimGraphResource parent_graph_resource_loaded; BlendTreeResource* blend_tree_resource_loaded =
parent_graph_resource_loaded.LoadFromFile("TestGraphEmbeddedBlendTree.json"); BlendTreeResource::CreateFromFile(filename);
// Check the loaded parent graph // Check the loaded parent graph
CheckAnimGraphResourceEqual( CheckBlendTreeResourcesEqual(blend_tree_resource, blend_tree_resource_loaded);
parent_graph_resource,
parent_graph_resource_loaded);
const BlendTreeResource& parent_blend_tree_resource_loaded =
parent_graph_resource_loaded.m_blend_tree_resource;
// Check the loaded embedded graph // Check the loaded embedded graph
REQUIRE( REQUIRE(
parent_blend_tree_resource_loaded.GetNode(3)->m_node_type_name blend_tree_resource_loaded->GetNode(3)->m_node_type_name == "BlendTree");
== "BlendTree");
const AnimGraphResource* embedded_graph_loaded =
dynamic_cast<const AnimGraphResource*>(
parent_blend_tree_resource_loaded.GetNode(3));
const BlendTreeResource* embedded_blend_tree_resource_loaded = const BlendTreeResource* embedded_blend_tree_resource_loaded =
&embedded_graph_loaded->m_blend_tree_resource; dynamic_cast<const BlendTreeResource*>(
blend_tree_resource_loaded->GetNode(3));
CheckBlendTreeResourcesEqual( CheckBlendTreeResourcesEqual(
embedded_blend_tree_resource, embedded_blend_tree_resource,
embedded_blend_tree_resource_loaded); embedded_blend_tree_resource_loaded);
delete blend_tree_resource_loaded;
} }
TEST_CASE_METHOD( TEST_CASE_METHOD(
@@ -1178,7 +1126,7 @@ TEST_CASE_METHOD(
"[EmbeddedBlendTreeGraphResource]") { "[EmbeddedBlendTreeGraphResource]") {
AnimGraphBlendTree blend_tree; AnimGraphBlendTree blend_tree;
parent_graph_resource.CreateBlendTreeInstance(blend_tree); blend_tree_resource->CreateBlendTreeInstance(blend_tree);
AnimGraphContext graph_context; AnimGraphContext graph_context;
ozz::animation::Skeleton skeleton; ozz::animation::Skeleton skeleton;
@@ -1255,7 +1203,7 @@ TEST_CASE_METHOD(
"[EmbeddedTreeBlend2GraphResource]") { "[EmbeddedTreeBlend2GraphResource]") {
AnimGraphBlendTree blend_tree; AnimGraphBlendTree blend_tree;
parent_graph_resource.CreateBlendTreeInstance(blend_tree); blend_tree_resource->CreateBlendTreeInstance(blend_tree);
AnimGraphContext graph_context; AnimGraphContext graph_context;
ozz::animation::Skeleton skeleton; ozz::animation::Skeleton skeleton;
@@ -1338,21 +1286,19 @@ TEST_CASE_METHOD(
TEST_CASE( TEST_CASE(
"Register AnimGraphResource Blendtree Sockets", "Register AnimGraphResource Blendtree Sockets",
"[AnimGraphResource]") { "[AnimGraphResource]") {
AnimNodeResource* blend_tree_anim_node_resource = BlendTreeResource* blend_tree_resource =
AnimNodeResourceFactory("BlendTree"); dynamic_cast<BlendTreeResource*>(AnimNodeResourceFactory("BlendTree"));
AnimGraphResource* blend_tree_graph_resource =
dynamic_cast<AnimGraphResource*>(blend_tree_anim_node_resource);
Socket socket; Socket socket;
socket.m_name = "FloatSocket"; socket.m_name = "FloatSocket";
socket.m_type = SocketType::SocketTypeFloat; socket.m_type = SocketType::SocketTypeFloat;
socket.m_reference.ptr = nullptr; socket.m_reference.ptr = nullptr;
CHECK(blend_tree_graph_resource->RegisterBlendTreeInputSocket(socket)); CHECK(blend_tree_resource->RegisterBlendTreeInputSocket(socket));
CHECK(!blend_tree_graph_resource->RegisterBlendTreeInputSocket(socket)); CHECK(!blend_tree_resource->RegisterBlendTreeInputSocket(socket));
CHECK(blend_tree_graph_resource->RegisterBlendTreeOutputSocket(socket)); CHECK(blend_tree_resource->RegisterBlendTreeOutputSocket(socket));
CHECK(!blend_tree_graph_resource->RegisterBlendTreeOutputSocket(socket)); CHECK(!blend_tree_resource->RegisterBlendTreeOutputSocket(socket));
delete blend_tree_anim_node_resource; delete blend_tree_resource;
} }
+21 -12
View File
@@ -8,26 +8,34 @@
TEST_CASE("Descriptor Access", "[NodeDescriptorTests]") { TEST_CASE("Descriptor Access", "[NodeDescriptorTests]") {
Blend2Node blend2Node; Blend2Node blend2Node;
NodeDescriptor<Blend2Node> blend2Descriptor (&blend2Node); NodeDescriptor<Blend2Node> blend2Descriptor(&blend2Node);
CHECK(blend2Descriptor.m_inputs.size() == 3); CHECK(blend2Descriptor.m_inputs.size() == 3);
CHECK(*blend2Descriptor.m_inputs[0].m_reference.ptr_ptr == blend2Node.i_input0); CHECK(
CHECK(*blend2Descriptor.m_inputs[1].m_reference.ptr_ptr == blend2Node.i_input1); *blend2Descriptor.m_inputs[0].m_reference.ptr_ptr == blend2Node.i_input0);
CHECK(*blend2Descriptor.m_inputs[2].m_reference.ptr_ptr == blend2Node.i_blend_weight); CHECK(
*blend2Descriptor.m_inputs[1].m_reference.ptr_ptr == blend2Node.i_input1);
CHECK(
*blend2Descriptor.m_inputs[2].m_reference.ptr_ptr
== blend2Node.i_blend_weight);
CHECK(blend2Descriptor.m_inputs[0].m_type_size == sizeof(AnimData)); CHECK(blend2Descriptor.m_inputs[0].m_type_size == sizeof(Pose));
CHECK(blend2Descriptor.m_inputs[2].m_type_size == 4); CHECK(blend2Descriptor.m_inputs[2].m_type_size == 4);
CHECK(blend2Descriptor.m_outputs.size() == 1); CHECK(blend2Descriptor.m_outputs.size() == 1);
CHECK(*blend2Descriptor.m_outputs[0].m_reference.ptr_ptr == blend2Node.o_output); CHECK(
*blend2Descriptor.m_outputs[0].m_reference.ptr_ptr
== blend2Node.o_output);
CHECK(blend2Descriptor.m_properties.size() == 1); CHECK(blend2Descriptor.m_properties.size() == 1);
CHECK(blend2Descriptor.m_properties[0].m_reference.ptr == &blend2Node.m_sync_blend); CHECK(
blend2Descriptor.m_properties[0].m_reference.ptr
== &blend2Node.m_sync_blend);
// Check we can properly update inputs // Check we can properly update inputs
CHECK(blend2Node.i_input0 == nullptr); CHECK(blend2Node.i_input0 == nullptr);
AnimData some_anim_data; Pose pose;
blend2Descriptor.SetInput("Input0", &some_anim_data); blend2Descriptor.SetInput("Input0", &pose);
CHECK(blend2Node.i_input0 == &some_anim_data); CHECK(blend2Node.i_input0 == &pose);
// Check we properly can set properties // Check we properly can set properties
CHECK(blend2Node.m_sync_blend == false); CHECK(blend2Node.m_sync_blend == false);
@@ -41,6 +49,7 @@ TEST_CASE("Descriptor Access", "[NodeDescriptorTests]") {
blend2Descriptor.UpdateFlags(); blend2Descriptor.UpdateFlags();
Socket* weight_input_socket = blend2Descriptor.GetInputSocket("Weight"); Socket* weight_input_socket = blend2Descriptor.GetInputSocket("Weight");
CHECK(weight_input_socket != nullptr); CHECK(weight_input_socket != nullptr);
CHECK(weight_input_socket->m_flags & SocketFlagAffectsTime == SocketFlagAffectsTime); CHECK(
weight_input_socket->m_flags
& SocketFlagAffectsTime == SocketFlagAffectsTime);
} }
+128 -97
View File
@@ -2,201 +2,232 @@
// Created by martin on 16.11.21. // Created by martin on 16.11.21.
// //
#include "SyncTrack.h" #include "AnimGraph/SyncTrack.h"
#include "catch.hpp" #include "catch.hpp"
TEST_CASE("Basic", "[SyncTrack]") { TEST_CASE("Basic", "[SyncTrack]") {
SyncTrack track_A; SyncTrack track_a;
track_A.m_num_intervals = 2; track_a.m_num_intervals = 2;
track_A.m_duration = 2.0; track_a.m_duration = 2.0;
track_A.m_interval_start[0] = 0.f; track_a.m_interval_start_ratio[0] = 0.f;
track_A.m_interval_ratio[0] = 0.7; track_a.m_interval_duration_ratio[0] = 0.7;
track_A.m_interval_start[1] = 0.7f; track_a.m_interval_start_ratio[1] = 0.7f;
track_A.m_interval_ratio[1] = 0.3; track_a.m_interval_duration_ratio[1] = 0.3;
SyncTrack track_B; SyncTrack track_b;
track_B.m_num_intervals = 2; track_b.m_num_intervals = 2;
track_B.m_duration = 1.5; track_b.m_duration = 1.5;
track_B.m_interval_start[0] = 0.0f; track_b.m_interval_start_ratio[0] = 0.0f;
track_B.m_interval_ratio[0] = 0.6; track_b.m_interval_duration_ratio[0] = 0.6;
track_B.m_interval_start[1] = 0.6f; track_b.m_interval_start_ratio[1] = 0.6f;
track_B.m_interval_ratio[1] = 0.4; track_b.m_interval_duration_ratio[1] = 0.4;
WHEN("Calculating sync time of track_B at 0.5 duration") { WHEN("Calculating sync time of track_B at 0.5 duration") {
float sync_time_at_0_75 = float sync_time_at_0_75 =
track_B.CalcSyncFromAbsTime(0.5 * track_B.m_duration); track_b.CalcSyncFromAbsTime(0.5 * track_b.m_duration);
REQUIRE(sync_time_at_0_75 == Catch::Detail::Approx(0.83333)); REQUIRE(sync_time_at_0_75 == Catch::Detail::Approx(0.83333));
} }
WHEN("Calculating sync time of track_B at 0.6 duration") { WHEN("Calculating sync time of track_B at 0.6 duration") {
float sync_time_at_0_6 = float sync_time_at_0_6 =
track_B.CalcSyncFromAbsTime(0.6 * track_B.m_duration); track_b.CalcSyncFromAbsTime(0.6 * track_b.m_duration);
REQUIRE(sync_time_at_0_6 == Catch::Detail::Approx(1.0)); REQUIRE(sync_time_at_0_6 == Catch::Detail::Approx(1.0));
} }
WHEN("Calculating sync time of track_B at 0.7 duration") { WHEN("Calculating sync time of track_B at 0.7 duration") {
float sync_time_at_0_7 = float sync_time_at_0_7 =
track_B.CalcSyncFromAbsTime(0.7 * track_B.m_duration); track_b.CalcSyncFromAbsTime(0.7 * track_b.m_duration);
REQUIRE(sync_time_at_0_7 == Catch::Detail::Approx(1.25)); REQUIRE(sync_time_at_0_7 == Catch::Detail::Approx(1.25));
} }
WHEN("Calculating sync time of track_B at 0.0 duration") { WHEN("Calculating sync time of track_B at 0.0 duration") {
float sync_time_at_1_0 = float sync_time_at_1_0 =
track_B.CalcSyncFromAbsTime(0.0 * track_B.m_duration); track_b.CalcSyncFromAbsTime(0.0 * track_b.m_duration);
REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(0.0)); REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(0.0));
} }
WHEN("Calculating sync time of track_B at 1.0 duration") { WHEN("Calculating sync time of track_B at 1.0 duration") {
float sync_time_at_1_0 = float sync_time_at_1_0 =
track_B.CalcSyncFromAbsTime(0.9999 * track_B.m_duration); track_b.CalcSyncFromAbsTime(0.9999 * track_b.m_duration);
REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(2.0).epsilon(0.001f)); REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(2.0).epsilon(0.001f));
} }
WHEN("Calculating ratio from sync time on track_A at 0.83333") { WHEN("Calculating ratio from sync time on track_A at 0.83333") {
float ratio = track_A.CalcRatioFromSyncTime(0.83333333); float ratio = track_a.CalcRatioFromSyncTime(0.83333333);
REQUIRE(ratio == Catch::Detail::Approx(0.5833333)); REQUIRE(ratio == Catch::Detail::Approx(0.5833333));
} }
WHEN("Calculating ratio from sync time on track_A at 0.83333") { WHEN("Calculating ratio from sync time on track_A at 0.83333") {
float ratio = track_A.CalcRatioFromSyncTime(1.25); float ratio = track_a.CalcRatioFromSyncTime(1.25);
REQUIRE(ratio == Catch::Detail::Approx(0.775)); REQUIRE(ratio == Catch::Detail::Approx(0.775));
} }
WHEN("Blending two synctracks with weight 0.") { WHEN("Blending two synctracks with weight 0.") {
SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B); SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b);
THEN("Result must equal track_A") { REQUIRE(track_A == blended); } THEN("Result must equal track_A") { REQUIRE(track_a == blended); }
} }
WHEN("Blending two synctracks with weight 1.") { WHEN("Blending two synctracks with weight 1.") {
SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B); SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b);
THEN("Result must equal track_B") { REQUIRE(track_B == blended); } THEN("Result must equal track_B") { REQUIRE(track_b == blended); }
} }
} }
TEST_CASE("Sync Marker Interval Calculation", "[SyncTrack]") { TEST_CASE("Sync Track From Marker", "[SyncTrack]") {
SyncTrack track_A; SyncTrack track = SyncTrack::CreateFromMarkers(2.0f, {0.9f, 0.2f});
track_A.m_num_intervals = 2;
track_A.m_duration = 2.0;
track_A.m_sync_markers[0] = 0.9;
track_A.m_sync_markers[1] = 0.2;
WHEN("Calculating intervals") { WHEN("Querying Ratios") {
track_A.CalcIntervals(); CHECK(track.m_interval_start_ratio[0] == Catch::Detail::Approx(0.45f));
CHECK(track.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.65f));
CHECK(track_A.m_interval_start[0] == Catch::Detail::Approx(0.9f)); CHECK(track.m_interval_start_ratio[1] == Catch::Detail::Approx(0.1f));
CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3f)); CHECK(track.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.35f));
CHECK(track_A.m_interval_start[1] == Catch::Detail::Approx(0.2f)); WHEN("Querying ratio at sync time at 0.001") {
CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.7f)); float ratio = track.CalcRatioFromSyncTime(0.0001f);
CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001));
}
WHEN("Querying ratio at sync time at 1.001") { WHEN("Querying ratio at sync time at 0.9999") {
float ratio = track_A.CalcRatioFromSyncTime(1.0001f); float ratio = track.CalcRatioFromSyncTime(0.9999f);
CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001)); CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001));
} }
WHEN("Querying ratio at sync time at 1.001") { WHEN("Querying ratio at sync time at 1.001") {
float ratio = track_A.CalcRatioFromSyncTime(0.0001f); float ratio = track.CalcRatioFromSyncTime(1.0001f);
CHECK(ratio == Catch::Detail::Approx(0.9).epsilon(0.001)); CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001));
} }
WHEN("Querying ratio at sync time at 1.9999") { WHEN("Querying ratio at sync time at 1.9999") {
float ratio = track_A.CalcRatioFromSyncTime(0.9999f); float ratio = track.CalcRatioFromSyncTime(1.9999f);
CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001)); CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001));
} }
} }
WHEN("Blending sync track with 3 events") { WHEN("Querying SyncTime from Absolute Time") {
track_A.m_num_intervals = 3; WHEN("Querying absolute time at 0.9001s") {
track_A.m_duration = 2.0; float sync_time = track.CalcSyncFromAbsTime(0.9001f);
track_A.m_sync_markers[0] = 0.; CHECK_THAT(sync_time, Catch::WithinAbs(0.0, 0.001));
track_A.m_sync_markers[1] = 0.3; }
track_A.m_sync_markers[2] = 0.9;
track_A.CalcIntervals();
SyncTrack track_B; WHEN("Querying absolute time at 0.2001s") {
track_B.m_num_intervals = 3; float sync_time = track.CalcSyncFromAbsTime(0.2001f);
track_B.m_duration = 1.5; CHECK_THAT(sync_time, Catch::WithinAbs(1.0, 0.001));
track_B.m_sync_markers[0] = 0.7; }
track_B.m_sync_markers[1] = 0.9;
track_B.m_sync_markers[2] = 0.2; WHEN("Querying absolute time at 0.8999s") {
track_B.CalcIntervals(); float sync_time = track.CalcSyncFromAbsTime(0.8999f);
CHECK_THAT(sync_time, Catch::WithinAbs(1.999, 0.001));
}
WHEN("Querying absolute time at 1.9999s") {
float sync_time = track.CalcSyncFromAbsTime(1.9999f);
CHECK_THAT(sync_time, Catch::WithinAbs(0.84615384, 0.001));
}
}
}
TEST_CASE("Sync Track Blending", "[SyncTrack]") {
SyncTrack track_a = SyncTrack::CreateFromMarkers(2.0, {0., 0.6, 1.8});
SyncTrack track_b = SyncTrack::CreateFromMarkers(1.5f, {1.05, 1.35, 0.3});
WHEN("Calculating A's durations") { WHEN("Calculating A's durations") {
CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3)); CHECK(track_a.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.3));
CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.6)); CHECK(track_a.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.6));
CHECK(track_A.m_interval_ratio[2] == Catch::Detail::Approx(0.1)); CHECK(track_a.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.1));
} }
WHEN("Calculating B's durations") { WHEN("Calculating B's durations") {
CHECK(track_B.m_interval_ratio[0] == Catch::Detail::Approx(0.2)); CHECK(track_b.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.2));
CHECK(track_B.m_interval_ratio[1] == Catch::Detail::Approx(0.3)); CHECK(track_b.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.3));
CHECK(track_B.m_interval_ratio[2] == Catch::Detail::Approx(0.5)); CHECK(track_b.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.5));
} }
WHEN("Blending two synctracks with weight 0.") { WHEN("Blending two synctracks with weight 0.") {
SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B); SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b);
THEN("Result must equal track_A") { REQUIRE(track_A == blended); } THEN("Result must equal track_A") { REQUIRE(track_a == blended); }
} }
WHEN("Blending two synctracks with weight 1.") { WHEN("Blending two synctracks with weight 1.") {
SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B); SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b);
THEN("Result must equal track_B") { REQUIRE(track_B == blended); } THEN("Result must equal track_B") { REQUIRE(track_b == blended); }
} }
WHEN("Blending with weight 0.2") { WHEN("Blending with weight 0.2") {
float weight = 0.2f; float weight = 0.2f;
SyncTrack blended = SyncTrack::Blend(weight, track_A, track_B); SyncTrack blended = SyncTrack::Blend(weight, track_a, track_b);
REQUIRE( REQUIRE(
blended.m_duration blended.m_duration
== (1.0f - weight) * track_A.m_duration == (1.0f - weight) * track_a.m_duration + weight * track_b.m_duration);
+ weight * track_B.m_duration);
REQUIRE( REQUIRE(
blended.m_interval_start[0] blended.m_interval_start_ratio[0]
== fmodf( == fmodf(
(1.0f - weight) * (track_A.m_interval_start[0] + 1.0f) (1.0f - weight) * (track_a.m_interval_start_ratio[0] + 1.0f)
+ weight * (track_B.m_interval_start[0]), + weight * (track_b.m_interval_start_ratio[0]),
1.0f)); 1.0f));
REQUIRE( REQUIRE(
blended.m_interval_ratio[1] blended.m_interval_duration_ratio[1]
== (1.0f - weight) * (track_A.m_interval_ratio[1]) == (1.0f - weight) * (track_a.m_interval_duration_ratio[1])
+ weight * (track_B.m_interval_ratio[1]) + weight * (track_b.m_interval_duration_ratio[1]));
);
REQUIRE( REQUIRE(
blended.m_interval_ratio[2] blended.m_interval_duration_ratio[2]
== (1.0f - weight) * (track_A.m_interval_ratio[2]) == (1.0f - weight) * (track_a.m_interval_duration_ratio[2])
+ weight * (track_B.m_interval_ratio[2]) + weight * (track_b.m_interval_duration_ratio[2]));
);
} }
WHEN("Inverted blending with weight 0.2") { WHEN("Inverted blending with weight 0.2") {
float weight = 0.2f; float weight = 0.2f;
SyncTrack blended = SyncTrack::Blend(weight, track_B, track_A); SyncTrack blended = SyncTrack::Blend(weight, track_b, track_a);
REQUIRE( REQUIRE(
blended.m_duration blended.m_duration
== (1.0f - weight) * track_B.m_duration == (1.0f - weight) * track_b.m_duration + weight * track_a.m_duration);
+ weight * track_A.m_duration);
REQUIRE( REQUIRE(
blended.m_interval_start[0] blended.m_interval_start_ratio[0]
== fmodf( == fmodf(
(1.0f - weight) * (track_B.m_interval_start[0]) (1.0f - weight) * (track_b.m_interval_start_ratio[0])
+ weight * (track_A.m_interval_start[0] + 1.0f), + weight * (track_a.m_interval_start_ratio[0] + 1.0f),
1.0f)); 1.0f));
REQUIRE( REQUIRE(
blended.m_interval_ratio[1] blended.m_interval_duration_ratio[1]
== (1.0f - weight) * (track_B.m_interval_ratio[1]) == (1.0f - weight) * (track_b.m_interval_duration_ratio[1])
+ weight * (track_A.m_interval_ratio[1]) + weight * (track_a.m_interval_duration_ratio[1]));
);
REQUIRE( REQUIRE(
blended.m_interval_ratio[2] blended.m_interval_duration_ratio[2]
== (1.0f - weight) * (track_B.m_interval_ratio[2]) == (1.0f - weight) * (track_b.m_interval_duration_ratio[2])
+ weight * (track_A.m_interval_ratio[2]) + weight * (track_a.m_interval_duration_ratio[2]));
);
} }
}
TEST_CASE("Serialization", "[SyncTrack]") {
SyncTrack track;
track.m_num_intervals = 3;
track.m_duration = 2.0;
track.m_interval_start_ratio[0] = 0.f;
track.m_interval_duration_ratio[0] = 0.7;
track.m_interval_start_ratio[1] = 0.7f;
track.m_interval_duration_ratio[1] = 0.3;
track.m_interval_start_ratio[2] = 0.7f;
track.m_interval_duration_ratio[2] = 0.3;
nlohmann::json synctrack_json = track;
const SyncTrack synctrack_deserialized = synctrack_json;
CHECK(synctrack_deserialized.m_duration == track.m_duration);
CHECK(synctrack_deserialized.m_num_intervals == track.m_num_intervals);
for (int i = 0; i < track.m_num_intervals; i++) {
CHECK(
synctrack_deserialized.m_interval_start_ratio[i]
== track.m_interval_start_ratio[i]);
CHECK(
synctrack_deserialized.m_interval_duration_ratio[i]
== track.m_interval_duration_ratio[i]);
} }
} }
+117
View File
@@ -0,0 +1,117 @@
//
// Created by martin on 11.04.25.
//
#include "TestAnimData.h"
#include <iostream>
#include "ozz/animation/offline/animation_builder.h"
#include "ozz/animation/offline/raw_animation.h"
#include "ozz/animation/offline/raw_skeleton.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/log.h"
namespace TestAnimData {
SingleBoneSkeleton::SingleBoneSkeleton() {
using namespace ozz::animation::offline;
RawSkeleton raw_skeleton;
RawSkeleton::Joint raw_joint;
raw_joint.name = "Bone0";
raw_joint.transform.translation.x = 1.f;
raw_joint.transform.translation.y = 2.f;
raw_joint.transform.translation.z = 3.f;
raw_skeleton.roots.push_back(raw_joint);
SkeletonBuilder skeleton_builder;
skeleton = skeleton_builder(raw_skeleton);
// SingleBoneSkeleton Animations
ozz::animation::offline::RawAnimation raw_animation_translation_x;
raw_animation_translation_x.name = "TranslationX";
RawAnimation::JointTrack bone0_track;
RawAnimation::JointTrack::Translations bone0_translations;
// animation_translate_x
RawAnimation::TranslationKey translation_key;
translation_key.time = 0.f;
translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
translation_key.time = 1.f;
translation_key.value = ozz::math::Float3(1.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
bone0_track.translations = bone0_translations;
raw_animation_translation_x.tracks.push_back(bone0_track);
raw_animation_translation_x.duration = 1.f;
if (!raw_animation_translation_x.Validate()) {
std::cerr << "Error: could animation raw data invalid!" << std::endl;
}
AnimationBuilder animation_builder;
animation_translate_x = animation_builder(raw_animation_translation_x);
animation_translate_x_resource.m_animation = animation_translate_x.get();
SaveAnimation("single_bone_translation_z.ozz", animation_translate_x.get());
animation_translate_x_resource.m_name = "single_bone_translation_z";
animation_translate_x_resource.m_filename = "single_bone_translation_z.ozz";
// animation_translate_y
ozz::animation::offline::RawAnimation raw_animation_translation_y;
raw_animation_translation_y.name = "TranslationY";
bone0_translations.clear();
translation_key.time = 0.f;
translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
translation_key.time = 1.f;
translation_key.value = ozz::math::Float3(0.f, 1.f, 0.f);
bone0_translations.push_back(translation_key);
bone0_track.translations = bone0_translations;
raw_animation_translation_y.tracks.push_back(bone0_track);
raw_animation_translation_y.duration = 1.f;
if (!raw_animation_translation_y.Validate()) {
std::cerr << "Error: could animation raw data invalid!" << std::endl;
}
animation_translate_y = animation_builder(raw_animation_translation_y);
animation_translate_y_resource.m_animation = animation_translate_y.get();
SaveAnimation("single_bone_translation_y.ozz", animation_translate_y.get());
animation_translate_y_resource.m_name = "single_bone_translation_y";
animation_translate_y_resource.m_filename = "single_bone_translation_y.ozz";
}
bool SingleBoneSkeleton::SaveSkeleton(
const char* filename,
ozz::animation::Skeleton* skeleton) {
assert(false);
return false;
}
bool SingleBoneSkeleton::SaveAnimation(
const char* filename,
ozz::animation::Animation* animation) {
ozz::io::File file(filename, "wb");
if (!file.opened()) {
ozz::log::Err() << "Failed to create animation file " << filename << "."
<< std::endl;
delete animation;
return false;
}
ozz::io::OArchive archive(&file);
archive << *animation;
return true;
}
} // namespace TestAnimData
+37
View File
@@ -0,0 +1,37 @@
//
// Created by martin on 11.04.25.
//
#ifndef TESTANIMDATA_H
#define TESTANIMDATA_H
#include "AnimGraph/AnimGraphData.h"
#include "AnimGraph/SyncTrack.h"
#include "ozz/animation/offline/skeleton_builder.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/skeleton.h"
namespace TestAnimData {
struct SingleBoneSkeleton {
SingleBoneSkeleton();
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = nullptr;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_x = nullptr;
AnimationResource animation_translate_x_resource;
SyncTrack animation_translate_x_sync_track = {};
ozz::unique_ptr<ozz::animation::Animation> animation_translate_y = nullptr;
AnimationResource animation_translate_y_resource;
SyncTrack animation_translate_y_sync_track = {};
bool SaveSkeleton(const char* filename, ozz::animation::Skeleton* skeleton);
bool SaveAnimation(
const char* filename,
ozz::animation::Animation* animation);
};
} // namespace TestAnimData
#endif //TESTANIMDATA_H