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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

# Build directories
build/
build-*/
out/
cmake-build-*/

Expand Down Expand Up @@ -63,6 +64,10 @@ docs/doxygen/
docs/build/
docs/sphinx/api/

# Dear ImGui writes its window layout next to the working directory when the
# curve_visualization example is run.
imgui.ini

# OS-specific files
.DS_Store
Thumbs.db
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,51 @@ While the major version is `0`, breaking changes may land in a minor release.

## [Unreleased]

### Changed

- **Breaking:** the `Id` lookups on `Animation` now return references rather
than pointers, matching the index and name overloads —
`channel(Id)` and `operator[](Id)`, in both the const and non-const forms.
They never returned null: the underlying `unordered_map::at` throws
`std::out_of_range` on a miss, so the pointer return only invited dead null
checks. Behavior on a miss is unchanged; callers replace `->` with `.` ([#52]).

### Added

- `Animation::sort_channels()`, sorting channels by name, and an overload
taking a comparator for any other ordering. Both are stable. Only the index
order changes: the channels themselves are not moved, so ids keep resolving
and references taken beforehand stay valid.

### Removed

- **Breaking:** `Id`'s constructor is now private, so ids can only originate
from the library — obtain them from `Channel::id()`, or use `Id::invalid()`
for a sentinel. A fabricated id was never able to do anything a real one
could not, but because ids are handed out from one counter shared by every
`Animation`, a hand-made id could silently resolve to an unrelated channel.
- The `glad` dependency. The examples now rely on the loader that Dear ImGui
already bundles, and the handful of direct GL calls in
`curve_visualization` are OpenGL 1.1 core, resolved by linking `OpenGL::GL`.
This also removes the `CMAKE_POLICY_VERSION_MINIMUM` workaround that glad
0.1.36 required under CMake 4 ([#53]).

### Fixed

- The `curve_visualization` example showed no plot on a first run. Its plot
window was opened without a size, so it auto-fitted to its content — but that
content is a plot sized `ImVec2(-1, -1)`, meaning "fill the available space".
On the first frame the two resolved to nothing, the window collapsed to a few
pixels behind the curve editor, and ImGui persisted that size to `imgui.ini`
from then on. Both windows now get a first-run position and size derived from
the viewport, using `ImGuiCond_FirstUseEver` so an arranged layout is kept.
- `imgui.ini`, which the `curve_visualization` example writes to the working
directory, is now ignored rather than showing up as untracked noise in the
repository root. Alternate build directories (`build-*/`) are ignored too.

[#52]: https://github.com/Actualize-Interactive/anim/issues/52
[#53]: https://github.com/Actualize-Interactive/anim/issues/53

## [0.2.0] - 2026-07-25

First release prepared for the public repository. It contains two small
Expand Down
22 changes: 3 additions & 19 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps CACHE PATH "Base directory for FetchContent downloads")

# glad 0.1.36 declares cmake_minimum_required(VERSION 3.0), and CMake 4.0
# removed compatibility with anything below 3.5, so configuring the examples
# fails outright without this. Scoped to the examples: the library and the test
# suite do not depend on it, and it is not inherited by anyone consuming anim.
# Remove once glad is bumped to a release that requires 3.5 or newer.
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)

# Fetch GLFW first (required by ImGui)
FetchContent_Declare(
glfw
Expand All @@ -35,15 +28,6 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(imgui)

# Fetch glad (OpenGL loader)
FetchContent_Declare(
glad
GIT_REPOSITORY https://github.com/Dav1dde/glad.git
GIT_TAG v0.1.36
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glad)

# Fetch ImPlot
FetchContent_Declare(
implot
Expand All @@ -69,14 +53,14 @@ target_include_directories(imgui_lib PUBLIC
${imgui_SOURCE_DIR}/backends
)

target_link_libraries(imgui_lib PUBLIC glfw glad)
target_link_libraries(imgui_lib PUBLIC glfw)

# Find OpenGL
find_package(OpenGL REQUIRED)
target_link_libraries(imgui_lib PUBLIC OpenGL::GL)

# Use glad as OpenGL loader for all platforms
target_compile_definitions(imgui_lib PUBLIC IMGUI_IMPL_OPENGL_LOADER_GLAD)
# No loader definition: with none set, the ImGui OpenGL3 backend falls back to
# its own bundled loader (backends/imgui_impl_opengl3_loader.h).

# Create ImPlot library target
add_library(implot_lib STATIC
Expand Down
26 changes: 19 additions & 7 deletions examples/curve_visualization.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include <anim.hpp>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
Expand Down Expand Up @@ -159,12 +158,6 @@ int main() {
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // Enable vsync

// Initialize OpenGL loader
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cerr << "Failed to initialize OpenGL loader" << std::endl;
return 1;
}

// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
Expand Down Expand Up @@ -235,6 +228,21 @@ int main() {
}


// Give both windows a sensible first-run size and position. Without
// this the plot window auto-fits to its content, but its content is a
// plot sized ImVec2(-1,-1) ("fill the available space"), so on the
// first frame the two resolve to nothing and the window collapses to a
// few pixels — which ImGui then persists to imgui.ini. FirstUseEver
// means a layout the user has arranged is still respected.
const ImGuiViewport* viewport = ImGui::GetMainViewport();
const ImVec2 work_pos = viewport->WorkPos;
const ImVec2 work_size = viewport->WorkSize;
const float editor_width = std::max(320.0f, work_size.x * 0.22f);
const float pad = 12.0f;

ImGui::SetNextWindowPos(ImVec2(work_pos.x + pad, work_pos.y + pad), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(work_size.x - editor_width - pad * 3.0f,
work_size.y - pad * 2.0f), ImGuiCond_FirstUseEver);
ImGui::Begin("Curves Plot — Ctrl-click to move keyframes & handles (panning disabled)###CurvesPlot");

// Disable ImPlot's click-drag panning so it doesn't fight with dragging
Expand Down Expand Up @@ -515,6 +523,10 @@ int main() {
}
ImGui::End();

ImGui::SetNextWindowPos(ImVec2(work_pos.x + work_size.x - editor_width - pad,
work_pos.y + pad), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(editor_width, work_size.y - pad * 2.0f),
ImGuiCond_FirstUseEver);
ImGui::Begin("Curve Editor");
// No CollapsingHeader for "Edit Curves", content is directly in the curve's TreeNode
for (size_t i = 0; i < animation.size(); ++i) {
Expand Down
26 changes: 22 additions & 4 deletions include/anim/animation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ class Animation {
/// @brief Name lookup. @throws std::out_of_range if none matches.
Channel& operator[](const std::string& channel_name);
/// @brief Returns the channel with @p channel_id. @throws std::out_of_range if none matches.
Channel* channel(Id channel_id);
Channel& channel(Id channel_id);
/// @brief Id lookup. @throws std::out_of_range if none matches.
Channel* operator[](Id channel_id);
Channel& operator[](Id channel_id);

/// @brief Returns the first channel named @p channel_name. @throws std::out_of_range if none matches.
const Channel& channel(const std::string& channel_name) const;
/// @brief Name lookup. @throws std::out_of_range if none matches.
const Channel& operator[](const std::string& channel_name) const;
/// @brief Returns the channel with @p channel_id. @throws std::out_of_range if none matches.
const Channel* channel(Id channel_id) const;
const Channel& channel(Id channel_id) const;
/// @brief Id lookup. @throws std::out_of_range if none matches.
const Channel* operator[](Id channel_id) const;
const Channel& operator[](Id channel_id) const;

/// @brief Number of channels.
inline size_t size() const { return m_channels.size(); }
Expand All @@ -105,6 +105,24 @@ class Animation {
*/
void reorder_channel(Id channel_id, size_t to_index);

/**
* @brief Sorts the channels by name, ascending.
*
* The sort is stable, so channels sharing a name keep their relative order.
* Only the index order changes: ids, names and keyframes are untouched, and
* because the channels themselves are not moved, references and pointers
* obtained before the sort — including those from channel(Id) — stay valid.
*/
void sort_channels();

/**
* @brief Sorts the channels using a custom ordering.
* @param comparator A strict weak ordering; returns true when the first
* channel should be placed before the second.
* @see sort_channels()
*/
void sort_channels(const std::function<bool(const Channel&, const Channel&)>& comparator);

/// @brief Removes all channels.
void clear();
/// @brief Removes the channel at @p index. @throws std::out_of_range if out of range.
Expand Down
17 changes: 14 additions & 3 deletions include/anim/id.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ namespace anim {
struct Id {
const uint64_t id; ///< The underlying identifier value (immutable).

/// @brief Constructs an Id wrapping @p value.
explicit Id(uint64_t value) : id(value) {}

/// @brief Explicit conversion back to the underlying integer value.
explicit operator uint64_t() const { return id; }

Expand Down Expand Up @@ -50,6 +47,20 @@ struct Id {
bool is_valid() const {
return id != static_cast<uint64_t>(-1);
}

private:
/**
* @brief Wraps a raw identifier value.
*
* Private so that ids can only originate from the library. An Id fabricated
* by a caller would either fail to resolve or, because ids are handed out
* from one counter shared by every Animation, resolve to some unrelated
* channel. Obtain ids from Channel::id(); use invalid() for a sentinel.
*/
explicit Id(uint64_t value) : id(value) {}

friend class Animation; ///< Mints ids for the channels it creates.
friend class Channel; ///< Stores the id it was created with.
};

} // namespace anim
Expand Down
33 changes: 25 additions & 8 deletions src/animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ Channel& Animation::operator[](const std::string& channel_name) {
return channel(channel_name);
}

Channel* Animation::channel(Id channel_id) {
return m_channel_map.at(channel_id);
Channel& Animation::channel(Id channel_id) {
return *m_channel_map.at(channel_id);
}

Channel* Animation::operator[](Id channel_id) {
Channel& Animation::operator[](Id channel_id) {
return channel(channel_id);
}

Expand All @@ -120,11 +120,11 @@ const Channel& Animation::operator[](const std::string& channel_name) const {
return channel(channel_name);
}

const Channel* Animation::channel(Id channel_id) const {
return m_channel_map.at(channel_id);
const Channel& Animation::channel(Id channel_id) const {
return *m_channel_map.at(channel_id);
}

const Channel* Animation::operator[](Id channel_id) const {
const Channel& Animation::operator[](Id channel_id) const {
return channel(channel_id);
}

Expand Down Expand Up @@ -175,9 +175,26 @@ void Animation::reorder_channel(Id channel_id, size_t to_index) {
reorder_channel(from_index, to_index);
}

void Animation::clear() {
void Animation::sort_channels() {
sort_channels([](const Channel& lhs, const Channel& rhs) {
return lhs.name() < rhs.name();
});
}

void Animation::sort_channels(const std::function<bool(const Channel&, const Channel&)>& comparator) {
// Reordering the owning pointers leaves the Channel objects themselves in
// place, so m_channel_map stays valid and so does anything the caller is
// already holding a reference to.
std::stable_sort(m_channels.begin(), m_channels.end(),
[&comparator](const std::unique_ptr<Channel>& lhs,
const std::unique_ptr<Channel>& rhs) {
return comparator(*lhs, *rhs);
});
}

void Animation::clear() {
m_channel_map.clear();
m_channels.clear();
m_channels.clear();
}

void Animation::remove_channel(size_t index) {
Expand Down
Loading