diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..d45f152 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,78 @@ +name: Bug report +description: Something in the library behaves incorrectly +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a report. If this is a **security** + issue, please use private reporting instead — see [SECURITY.md](../blob/main/SECURITY.md). + + - type: textarea + id: summary + attributes: + label: What happened? + description: What did you expect, and what did you get instead? + placeholder: | + Evaluating a channel with HandleMode::Flat at t=... returns ..., but I expected ... + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Reproduction + description: > + The smallest program that shows the problem. A few lines using + `anim::Animation` and `create_keyframe` is usually enough, and it + will be rendered as C++ automatically. + render: cpp + placeholder: | + #include + + int main() { + anim::Animation animation("repro"); + anim::Channel& channel = animation.create_channel("value"); + channel.create_keyframe(0.0, 0.0); + channel.create_keyframe(1.0, 1.0); + // ... + } + validations: + required: true + + - type: input + id: version + attributes: + label: Version + description: Release tag or commit SHA. + placeholder: "v0.2.0, or 1a2b3c4" + validations: + required: true + + - type: input + id: environment + attributes: + label: Compiler and platform + placeholder: "MSVC 2022 / Windows 11, GCC 13 / Ubuntu 24.04, AppleClang 16 / macOS 15" + validations: + required: true + + - type: dropdown + id: build_type + attributes: + label: Build configuration + options: + - Release + - Debug + - RelWithDebInfo + - Other / not sure + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Anything else? + description: Stack traces, sanitizer output, or a failing test case. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c356ff0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: API documentation + url: https://actualize-interactive.github.io/anim/ + about: The full API reference and guide — worth checking before filing. + - name: Report a security vulnerability + url: https://github.com/Actualize-Interactive/anim/security/advisories/new + about: Please report security issues privately, not as a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9425afd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest a capability or API addition +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + description: > + Describe the use case rather than the implementation. What are you + trying to do that the library makes hard or impossible today? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed API + description: > + If you have a shape in mind, sketch it. Additive changes are much + easier to accept than changes to existing public signatures. + render: cpp + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Anything you tried, or workarounds you are using now. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ee39ea2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + +## What does this change? + + + +## Why? + + + +## Checklist + +- [ ] The build and full test suite pass locally (`./build.sh` or `.\build.ps1`). +- [ ] New or changed behavior is covered by tests. +- [ ] Public API changes are documented with Doxygen comments in `include/`. +- [ ] Naming follows the conventions in CONTRIBUTING.md (PascalCase types and + enumerators, lower_snake_case functions and variables). +- [ ] `CHANGELOG.md` has an entry under `[Unreleased]`, if user-visible. + +## Breaking changes + + + +None diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..75d56f5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +version: 2 + +updates: + # Keep the workflow actions current. The repository has no package-manager + # manifest for Dependabot to track: C++ dependencies (Catch2, GLFW, ImGui, + # ImPlot, glad) are pinned as FetchContent git tags in CMakeLists.txt, which + # Dependabot cannot parse, so those are bumped by hand. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + commit-message: + prefix: "ci" + groups: + github-actions: + patterns: + - "*" + + # Documentation toolchain (Sphinx, Breathe, Exhale, the RTD theme). + - package-ecosystem: "pip" + directory: "/docs/sphinx" + schedule: + interval: "monthly" + commit-message: + prefix: "docs" + groups: + sphinx: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8f350..84f4a49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,8 @@ on: branches: [ main ] workflow_dispatch: -# checkout/cache still ship on Node.js 20; opt their JS actions into Node 24 -# ahead of the forced cutover (2026-06-02). Remove once the actions ship Node 24. -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +permissions: + contents: read jobs: build: @@ -22,24 +20,24 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup cmake - uses: lukka/get-cmake@latest + uses: lukka/get-cmake@v4.4.0 with: - cmakeVersion: '3.26.0' + cmakeVersion: '4.4.0' - name: Cache CMake Build - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | build ~/.ccache ~/Library/Caches/ccache # macOS ~\AppData\Local\ccache # Windows - key: ${{ runner.os }}-cmake-${{ hashFiles('**/CMakeLists.txt') }} + key: ${{ runner.os }}-cmake4.4.0-${{ hashFiles('**/CMakeLists.txt') }} restore-keys: | - ${{ runner.os }}-cmake- + ${{ runner.os }}-cmake4.4.0- - name: Install system dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0c54cdc..2f22696 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,19 +18,14 @@ concurrency: group: pages cancel-in-progress: false -# checkout still ships on Node.js 20; opt JS actions into Node 24 ahead of the -# forced cutover (2026-06-02). Remove once the actions ship Node 24. -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -46,7 +41,7 @@ jobs: run: bash docs/build_docs.sh - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: docs/build/html @@ -61,4 +56,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6266bce..82542be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,10 +6,8 @@ on: - 'v*.*.*' workflow_dispatch: -# checkout/cache still ship on Node.js 20; opt their JS actions into Node 24 -# ahead of the forced cutover (2026-06-02). Remove once the actions ship Node 24. -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +permissions: + contents: read jobs: # Build and test on every platform as the release gate. @@ -22,26 +20,26 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup cmake - uses: lukka/get-cmake@latest + uses: lukka/get-cmake@v4.4.0 with: - cmakeVersion: '3.26.0' + cmakeVersion: '4.4.0' - name: Cache CMake Build - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | build ~/.ccache ~/Library/Caches/ccache # macOS ~\AppData\Local\ccache # Windows - key: ${{ runner.os }}-cmake-${{ hashFiles('**/CMakeLists.txt') }} + key: ${{ runner.os }}-cmake4.4.0-${{ hashFiles('**/CMakeLists.txt') }} restore-keys: | - ${{ runner.os }}-cmake- + ${{ runner.os }}-cmake4.4.0- - name: Install system dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' @@ -66,15 +64,15 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 # One platform-independent, buildable source archive (headers + sources + # CMake config). GitHub also attaches auto-generated "Source code" archives. - name: Package source archive - run: zip -r anim-${{ github.ref_name }}-src.zip include src cmake CMakeLists.txt README.md LICENSE CONTRIBUTING.md + run: zip -r anim-${{ github.ref_name }}-src.zip include src cmake CMakeLists.txt README.md LICENSE CONTRIBUTING.md CHANGELOG.md - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: anim-${{ github.ref_name }}-src.zip name: Release ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index c9cd3d2..8b11db9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,13 +43,17 @@ cmake-build-*/ *.swp *~ -# CMake generated files +# CMake generated files (from an accidental in-source build; out-of-source +# artifacts are already covered by the build directory rules above). +# Listed by name rather than as *.cmake so hand-written modules under cmake/ +# are not silently ignored. CMakeCache.txt CMakeFiles/ cmake_install.cmake +CTestTestfile.cmake +CPackConfig.cmake +CPackSourceConfig.cmake Makefile -*.cmake -!anim-config.cmake.in # Generated test outputs Testing/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3b28390 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +While the major version is `0`, breaking changes may land in a minor release. + +## [Unreleased] + +## [0.2.0] - 2026-07-25 + +First release prepared for the public repository. It contains two small +breaking renames that bring the public API in line with the project's naming +conventions; both are mechanical find-and-replace changes for callers. + +### Changed + +- **Breaking:** `Id::isValid()` is now `Id::is_valid()`, matching the + lower_snake_case convention used by every other method. +- **Breaking:** `GrabbedHandle` enumerators are now PascalCase, matching every + other enum in the library: + `GrabbedHandle::none` → `GrabbedHandle::None`, + `GrabbedHandle::in_handle` → `GrabbedHandle::InHandle`, + `GrabbedHandle::out_handle` → `GrabbedHandle::OutHandle`. +- `build.sh` and `build.ps1` now build the library and test suite only, run + from the repository root regardless of the working directory, and pass the + build configuration through to `ctest`. Use `examples/run_example.*` to build + and launch the viewer. +- The project version is now defined only in `CMakeLists.txt`; the Doxygen and + Sphinx configurations derive it from there instead of repeating it. +- Documentation dependencies are pinned to tested version ranges so an upstream + release cannot break the Pages deploy without a reviewed change. +- CI, docs, and release workflows updated to current action versions, with + explicit least-privilege `permissions` blocks. +- CI now builds with CMake 4.4. The `windows-latest` runner image ships Visual + Studio 2026, for which a generator first exists in CMake 4.2; the previously + pinned CMake 3.26 fell back to NMake and could not find a compiler at all. + +### Added + +- `CHANGELOG.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md`. +- Issue and pull request templates, and a Dependabot configuration that keeps + GitHub Actions up to date. +- Naming conventions documented in `CONTRIBUTING.md` (PascalCase for types and + enumerators, lower_snake_case for functions and variables). + +### Fixed + +- `build.ps1` called `Pop-Location` without a matching `Push-Location`, and ran + `ctest` without `-C`, which finds no tests on multi-config generators such as + Visual Studio. +- `.gitignore` matched `*.cmake` repository-wide, silently ignoring any + hand-written CMake module added under `cmake/`. + +### Removed + +- `examples/gl_loader.cpp`, which was unreferenced by any build file or source + since the examples moved to glad. + +## [0.1.2] - 2026-05-28 + +Repository prepared for publication: MIT license, contributor guide, Doxygen + +Sphinx API documentation published to GitHub Pages, and a release workflow that +publishes a single source archive. + +## [0.1.1] - 2025-07-05 + +Enumerators converted to PascalCase; per-channel `Extend` control for +out-of-range evaluation; equality operators for `Animation` and `Channel`; +explicit `Animation` copy methods; additional `Channel::create_keyframe` +overloads and a `keyframes()` accessor; stable per-channel `Id`. + +## [0.1.0] - 2025-05-29 + +Initial release: `Animation` and `Channel` containers, keyframes with +`Constant` / `Linear` / `Bezier` interpolation, the `HandleMode` family of +Bézier handle constraints, sampling helpers, and the Catch2 test suite. + + + +[Unreleased]: https://github.com/Actualize-Interactive/anim/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/Actualize-Interactive/anim/compare/v0.1.2...v0.2.0 +[0.1.2]: https://github.com/Actualize-Interactive/anim/releases/tag/v0.1.2 +[0.1.1]: https://github.com/Actualize-Interactive/anim/releases/tag/v0.1.1 +[0.1.0]: https://github.com/Actualize-Interactive/anim/releases/tag/v0.1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8721976..28059a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.25) -project(anim VERSION 0.1.2 LANGUAGES CXX) +project(anim VERSION 0.2.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a62559b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,135 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by [opening an issue](https://github.com/Actualize-Interactive/anim/issues/new/choose) +in this repository. + +Note that repository issues are public. If a report would expose you by being +public — or if it concerns a maintainer — use GitHub's +[report abuse](https://github.com/contact/report-abuse) form instead, which +goes privately to GitHub Support rather than to this project. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla]. + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. + +[homepage]: https://www.contributor-covenant.org +[mozilla]: https://github.com/mozilla/inclusion diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 753dd62..afa75eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,13 @@ automatically by CMake) and lives in `tests/`. ## Coding conventions - **C++20**, no compiler extensions. -- **Enumerators are PascalCase** (e.g. `Function::Bezier`, `HandleMode::Smooth`). +- **Naming:** + - Types, classes, structs, enums, and enumerators are **PascalCase** — + `Animation`, `Channel`, `Keyframe`, `Function::Bezier`, `HandleMode::Smooth`, + `GrabbedHandle::OutHandle`. + - Functions, methods, parameters, and variables are **lower_snake_case** — + `create_keyframe`, `evaluate_range_by_rate`, `is_valid`, `handle_mode`. + - Private data members are prefixed `m_` — `m_keyframes`, `m_channel_map`. - **Public interfaces are stable.** Avoid changing existing public signatures; prefer additive changes. Call out any unavoidable break in your PR. - **Document the public API in the headers** with Doxygen comments @@ -62,6 +68,8 @@ same change. - Use clear, imperative commit messages with a type prefix (`fix:`, `feat:`, `docs:`, `test:`, `chore:`, `ci:`). - Make sure the build and tests pass before opening the PR. +- Add an entry under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md) for any + user-visible change, and call out breaking changes explicitly. ## License diff --git a/README.md b/README.md index a19147e..33bc60d 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,17 @@ With `ANIM_BUILD_EXAMPLES` enabled, the `examples/` directory builds: - **`curve_visualization`** — an interactive ImGui/ImPlot viewer for inspecting curves and handle modes. +The helper scripts above build the library and tests only. To build and launch +the viewer (which fetches GLFW, ImGui and ImPlot on first configure): + +```bash +./examples/run_example.sh # Linux / macOS +``` + +```powershell +.\examples\run_example.ps1 # Windows (PowerShell) +``` + ## Documentation The full API reference and guide are published at @@ -184,7 +195,11 @@ The full API reference and guide are published at documentation with Doxygen and Sphinx. To build the docs locally: ```bash -bash docs/build_docs.sh # output in docs/build/html +bash docs/build_docs.sh # Linux / macOS — output in docs/build/html +``` + +```powershell +.\docs\build_docs.ps1 # Windows (PowerShell) ``` ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..590d160 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +## Supported versions + +`anim` is pre-1.0 and is developed on a single line. Security fixes are applied +to the latest release only; there are no maintained backport branches. + +| Version | Supported | +| --- | --- | +| 0.2.x | ✅ | +| < 0.2 | ❌ | + +## Reporting a vulnerability + +**Please do not report security issues through public GitHub issues.** + +Report privately through GitHub's +[private vulnerability reporting](https://github.com/Actualize-Interactive/anim/security/advisories/new). +The form is the only reporting channel; it lets us discuss and fix the issue +with you before anything becomes public, and it requires nothing more than a +GitHub account. + +Please include: + +- the affected version or commit, +- a description of the issue and its impact, +- the steps, input, or minimal program needed to reproduce it, +- and any suggested fix, if you have one. + +You can expect an acknowledgement within a few business days. We will keep you +informed as we investigate, and will credit you in the release notes when the +fix ships unless you prefer otherwise. + +## Scope + +`anim` is a library with no network, filesystem, or process boundary of its +own — it evaluates animation curves from data supplied by the calling +application. The issues most relevant here are memory-safety problems reachable +from library inputs, such as: + +- out-of-bounds reads or writes from keyframe, channel, or index arguments, +- crashes, unbounded loops, or excessive allocation triggered by unusual but + legitimate input (extreme times, NaN or infinite values, degenerate handles), +- undefined behavior surfaced by the sanitizers or by checked iterators. + +Because the library trusts its caller by design, a report that depends on the +application passing deliberately corrupt internal state is likely to be treated +as a normal bug rather than a vulnerability. Report it as a regular issue and we +will still fix it. diff --git a/build.ps1 b/build.ps1 index 61229ed..7779c20 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,27 +3,22 @@ param( [string]$Config = "Release" ) -# Store the current location to restore it later -$originalLocation = Get-Location +# Configure, build, and test the anim library (library + test suite). +# To build and run the examples, use examples\run_example.ps1 instead. +$ErrorActionPreference = "Stop" -try { - - # Create build directory - $null = New-Item -Path .\build -ItemType Directory -Force - Set-Location -Path .\build - - # Configure using CMake - # Explicitly enable tests and examples for CI/standalone builds - cmake .. -DANIM_BUILD_TESTS=ON -DANIM_BUILD_EXAMPLES=OFF - - # Build - cmake --build . --config $Config # --verbose +# Run from the repository root regardless of where the script was invoked from. +$RepoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path - # Run tests - ctest # --verbose +Push-Location $RepoRoot +try { + # CMAKE_BUILD_TYPE covers single-config generators (Ninja/Make); + # --config covers multi-config generators (Visual Studio). + cmake -B build -S . -DCMAKE_BUILD_TYPE="$Config" -DANIM_BUILD_TESTS=ON -DANIM_BUILD_EXAMPLES=OFF + cmake --build build --config $Config - Pop-Location + # -C is required for multi-config generators; without it ctest finds no tests. + ctest --test-dir build -C $Config --output-on-failure } finally { - # Ensure we always return to the original directory, even if errors occur - Set-Location -Path $originalLocation -} \ No newline at end of file + Pop-Location +} diff --git a/build.sh b/build.sh index 441c63b..a44c05a 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,18 @@ #!/bin/bash -# Simple build script for the anim library +# Configure, build, and test the anim library (library + test suite). +# To build and run the examples, use examples/run_example.sh instead. +set -e -# Create build directory -mkdir -p build -cd build +CONFIG="${1:-Release}" -# Configure using CMake -# Explicitly enable tests and examples for CI/standalone builds -cmake .. -DANIM_BUILD_TESTS=ON -DANIM_BUILD_EXAMPLES=ON +# Run from the repository root regardless of where the script was invoked from. +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +cd "$SCRIPT_DIR" -# Build -cmake --build . +# CMAKE_BUILD_TYPE covers single-config generators (Ninja/Make); +# --config covers multi-config generators (Xcode). +cmake -B build -S . -DCMAKE_BUILD_TYPE="$CONFIG" -DANIM_BUILD_TESTS=ON -DANIM_BUILD_EXAMPLES=OFF +cmake --build build --config "$CONFIG" -# Run tests -ctest +# -C is required for multi-config generators; without it ctest finds no tests. +ctest --test-dir build -C "$CONFIG" --output-on-failure diff --git a/docs/Doxyfile b/docs/Doxyfile index 4865401..4de1032 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -3,7 +3,7 @@ # Run from the docs/ directory: doxygen Doxyfile PROJECT_NAME = "anim" -PROJECT_NUMBER = 0.1.2 +PROJECT_NUMBER = $(ANIM_VERSION) PROJECT_BRIEF = "Animation curve library" OUTPUT_DIRECTORY = doxygen diff --git a/docs/build_docs.ps1 b/docs/build_docs.ps1 index 96cb84c..56d7c9e 100644 --- a/docs/build_docs.ps1 +++ b/docs/build_docs.ps1 @@ -4,6 +4,17 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ScriptDir +# The version lives only in CMakeLists.txt; Doxyfile reads it as $(ANIM_VERSION) +# and conf.py parses the same file, so there is one source of truth. +$cmakeLists = Join-Path (Split-Path -Parent $ScriptDir) "CMakeLists.txt" +$versionMatch = [regex]::Match((Get-Content -Raw $cmakeLists), 'project\s*\(\s*anim\s+VERSION\s+([0-9]+(?:\.[0-9]+)*)') +if (-not $versionMatch.Success) { + Write-Error "Could not parse the project version from $cmakeLists" + exit 1 +} +$env:ANIM_VERSION = $versionMatch.Groups[1].Value +Write-Host "Building docs for anim $($env:ANIM_VERSION)" + Write-Host "Running Doxygen..." doxygen Doxyfile diff --git a/docs/build_docs.sh b/docs/build_docs.sh index 2b36c69..b581083 100755 --- a/docs/build_docs.sh +++ b/docs/build_docs.sh @@ -5,6 +5,16 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd "$SCRIPT_DIR" +# The version lives only in CMakeLists.txt; Doxyfile reads it as $(ANIM_VERSION) +# and conf.py parses the same file, so there is one source of truth. +ANIM_VERSION="$(grep -oE 'project[[:space:]]*\([[:space:]]*anim[[:space:]]+VERSION[[:space:]]+[0-9.]+' ../CMakeLists.txt | grep -oE '[0-9.]+$')" +if [ -z "$ANIM_VERSION" ]; then + echo "Error: could not parse the project version from ../CMakeLists.txt" >&2 + exit 1 +fi +export ANIM_VERSION +echo "Building docs for anim $ANIM_VERSION" + echo "Running Doxygen..." doxygen Doxyfile diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index b0d4e92..ce2e957 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -3,10 +3,26 @@ # The API reference is generated from the Doxygen XML (produced by running # doxygen on docs/Doxyfile) via Breathe + Exhale. See docs/build_docs.sh. +import pathlib +import re + project = 'anim' -copyright = '2025, Actualize Interactive Inc.' +copyright = '2025-2026, Actualize Interactive Inc.' author = 'Actualize Interactive Inc.' -release = '0.1.2' + + +def _version_from_cmake(): + """Read the single source of truth for the version: project() in CMakeLists.txt.""" + cmakelists = pathlib.Path(__file__).resolve().parents[2] / 'CMakeLists.txt' + match = re.search(r'project\s*\(\s*anim\s+VERSION\s+([0-9]+(?:\.[0-9]+)*)', + cmakelists.read_text(encoding='utf-8')) + if not match: + raise RuntimeError(f'Could not parse the project version from {cmakelists}') + return match.group(1) + + +release = _version_from_cmake() +version = release extensions = [ 'breathe', diff --git a/docs/sphinx/requirements.txt b/docs/sphinx/requirements.txt index ef182b1..804e841 100644 --- a/docs/sphinx/requirements.txt +++ b/docs/sphinx/requirements.txt @@ -1,4 +1,11 @@ -sphinx -breathe -exhale -sphinx_rtd_theme +# Pinned so an upstream release cannot break the Pages deploy without a +# reviewed change. Compatible-release ranges allow patch/minor updates while +# holding majors back until they are tested; Dependabot proposes major bumps +# as reviewable pull requests that CI validates. +# +# Verified building the full site cleanly against: Sphinx 9.1.0, breathe 4.36.0, +# exhale 0.3.7, sphinx-rtd-theme 3.1.0, docutils 0.22.4. +sphinx~=9.1 +breathe~=4.36 +exhale~=0.3.7 +sphinx-rtd-theme~=3.1 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index cd0532a..7fb298e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -7,6 +7,13 @@ 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 diff --git a/examples/curve_visualization.cpp b/examples/curve_visualization.cpp index db06285..ce441f6 100644 --- a/examples/curve_visualization.cpp +++ b/examples/curve_visualization.cpp @@ -61,46 +61,30 @@ void CreateExampleCurves() { // Reset selection when curves change s_selection = Selection{}; - if (true) { // Placeholder for future curves, currently only sine wave - // Curve 1: Simple Sine Wave (8 points) - only curve for now - anim::Channel& sine_curve = animation.create_channel("Sine Wave"); - for (float t = 0; t <= 32.f; t += 8.f) { - sine_curve.create_keyframe(static_cast(t), static_cast(sin(t))); + // The same sine wave once per interpolation style, offset in value so the + // curves stack legibly in the plot rather than overlapping. + struct CurveSpec { + const char* name; + double value_offset; + anim::Function function; + anim::HandleMode handle_mode; + }; + static const CurveSpec curve_specs[] = { + {"Sine Wave", 0.00, anim::Function::Bezier, anim::HandleMode::Smooth}, + {"Sine Wave Linear", 0.25, anim::Function::Linear, anim::HandleMode::Smooth}, + {"Sine Wave Flat", 0.50, anim::Function::Bezier, anim::HandleMode::Flat}, + {"Sine Wave Aligned", 0.75, anim::Function::Bezier, anim::HandleMode::Aligned}, + {"Sine Wave Free", 1.00, anim::Function::Bezier, anim::HandleMode::Free}, + }; + + for (const CurveSpec& spec : curve_specs) { + anim::Channel& sine_curve = animation.create_channel(spec.name); + for (double t = 0.0; t <= 32.0; t += 8.0) { + sine_curve.create_keyframe(t, sin(t) + spec.value_offset, + spec.function, spec.handle_mode); } } - if (true){ - // Curve 2: Simple Sine Wave (8 points) - linear interpolation - anim::Channel& sine_curve = animation.create_channel("Sine Wave Linear"); - for (float t = 0; t <= 32.f; t += 8.f) { - sine_curve.create_keyframe(static_cast(t), static_cast(sin(t)) + .25, anim::Function::Linear); - } - } - - if (true){ - // Curve 3: Simple Sine Wave (8 points) - flat handles - anim::Channel& sine_curve = animation.create_channel("Sine Wave Flat"); - for (float t = 0; t <= 32.f; t += 8.f) { - sine_curve.create_keyframe(static_cast(t), static_cast(sin(t)) + .5, anim::Function::Bezier, anim::HandleMode::Flat); - } - } - - if (true){ - // Curve 4: Simple Sine Wave (8 points) - aligned handles - anim::Channel& sine_curve = animation.create_channel("Sine Wave Aligned"); - for (float t = 0; t <= 32.f; t += 8.f) { - sine_curve.create_keyframe(static_cast(t), static_cast(sin(t)) + .75, anim::Function::Bezier, anim::HandleMode::Aligned); - } - } - - if (true){ - // Curve 5: Simple Sine Wave (8 points) - free handles - anim::Channel& sine_curve = animation.create_channel("Sine Wave Free"); - for (float t = 0; t <= 32.f; t += 8.f) { - sine_curve.create_keyframe(static_cast(t), static_cast(sin(t)) + 1.0, anim::Function::Bezier, anim::HandleMode::Free); - } - } - // Initialize visibility data after curves are created s_keyframe_visibilities.resize(animation.num_channels()); for(size_t i = 0; i < animation.num_channels(); ++i) { diff --git a/examples/gl_loader.cpp b/examples/gl_loader.cpp deleted file mode 100644 index ed3b126..0000000 --- a/examples/gl_loader.cpp +++ /dev/null @@ -1,173 +0,0 @@ -// Simple OpenGL loader for Windows -#ifdef _WIN32 -#include -#include - -// Define the OpenGL constants and function pointers that ImGui needs -#define GL_ARRAY_BUFFER 0x8892 -#define GL_ELEMENT_ARRAY_BUFFER 0x8893 -#define GL_STATIC_DRAW 0x88E4 -#define GL_DYNAMIC_DRAW 0x88E8 -#define GL_VERTEX_SHADER 0x8B31 -#define GL_FRAGMENT_SHADER 0x8B30 -#define GL_COMPILE_STATUS 0x8B81 -#define GL_LINK_STATUS 0x8B82 -#define GL_TEXTURE0 0x84C0 -#define GL_BGRA 0x80E1 -#define GL_CLAMP_TO_EDGE 0x812F -#define GL_MAJOR_VERSION 0x821B -#define GL_MINOR_VERSION 0x821C -#define GL_NUM_EXTENSIONS 0x821D -#define GL_SCISSOR_TEST 0x0C11 -#define GL_BLEND 0x0BE2 -#define GL_SRC_ALPHA 0x0302 -#define GL_ONE_MINUS_SRC_ALPHA 0x0303 -#define GL_ONE 0x0001 -#define GL_FUNC_ADD 0x8006 -#define GL_CULL_FACE 0x0B44 -#define GL_DEPTH_TEST 0x0B71 -#define GL_STENCIL_TEST 0x0B90 -#define GL_FRONT_AND_BACK 0x0408 -#define GL_FILL 0x1B02 -#define GL_FALSE 0 -#define GL_TRUE 1 -#define GL_TEXTURE_BINDING_2D 0x8069 - -typedef unsigned int GLenum; -typedef unsigned char GLboolean; -typedef unsigned int GLbitfield; -typedef signed char GLbyte; -typedef short GLshort; -typedef int GLint; -typedef int GLsizei; -typedef unsigned char GLubyte; -typedef unsigned short GLushort; -typedef unsigned int GLuint; -typedef float GLfloat; -typedef float GLclampf; -typedef double GLdouble; -typedef double GLclampd; -typedef void GLvoid; -typedef char GLchar; - -// Function pointer types -typedef void (APIENTRY *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint *buffers); -typedef void (APIENTRY *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint *buffers); -typedef void (APIENTRY *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer); -typedef void (APIENTRY *PFNGLBUFFERDATAPROC)(GLenum target, ptrdiff_t size, const GLvoid *data, GLenum usage); -typedef GLuint (APIENTRY *PFNGLCREATESHADERPROC)(GLenum type); -typedef void (APIENTRY *PFNGLDELETESHADERPROC)(GLuint shader); -typedef void (APIENTRY *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar* const *string, const GLint *length); -typedef void (APIENTRY *PFNGLCOMPILESHADERPROC)(GLuint shader); -typedef void (APIENTRY *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint *params); -typedef GLuint (APIENTRY *PFNGLCREATEPROGRAMPROC)(void); -typedef void (APIENTRY *PFNGLDELETEPROGRAMPROC)(GLuint program); -typedef void (APIENTRY *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader); -typedef void (APIENTRY *PFNGLDETACHSHADERPROC)(GLuint program, GLuint shader); -typedef void (APIENTRY *PFNGLLINKPROGRAMPROC)(GLuint program); -typedef void (APIENTRY *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint *params); -typedef void (APIENTRY *PFNGLUSEPROGRAMPROC)(GLuint program); -typedef GLint (APIENTRY *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar *name); -typedef void (APIENTRY *PFNGLUNIFORM1IPROC)(GLint location, GLint v0); -typedef void (APIENTRY *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); -typedef GLint (APIENTRY *PFNGLGETATTRIBLOCATIONPROC)(GLuint program, const GLchar *name); -typedef void (APIENTRY *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays); -typedef void (APIENTRY *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint *arrays); -typedef void (APIENTRY *PFNGLBINDVERTEXARRAYPROC)(GLuint array); -typedef void (APIENTRY *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index); -typedef void (APIENTRY *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index); -typedef void (APIENTRY *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer); -typedef void (APIENTRY *PFNGLACTIVETEXTUREPROC)(GLenum texture); -typedef void (APIENTRY *PFNGLGETINTEGERVPROC)(GLenum pname, GLint *data); -typedef const GLubyte* (APIENTRY *PFNGLGETSTRINGPROC)(GLenum name); -typedef const GLubyte* (APIENTRY *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index); -typedef void (APIENTRY *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode); -typedef void (APIENTRY *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height); -typedef void (APIENTRY *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices); -typedef void (APIENTRY *PFNGLBLENDEQUATIONPROC)(GLenum mode); -typedef void (APIENTRY *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); - -// Function pointers -extern "C" { -PFNGLGENBUFFERSPROC glGenBuffers; -PFNGLDELETEBUFFERSPROC glDeleteBuffers; -PFNGLBINDBUFFERPROC glBindBuffer; -PFNGLBUFFERDATAPROC glBufferData; -PFNGLCREATESHADERPROC glCreateShader; -PFNGLDELETESHADERPROC glDeleteShader; -PFNGLSHADERSOURCEPROC glShaderSource; -PFNGLCOMPILESHADERPROC glCompileShader; -PFNGLGETSHADERIVPROC glGetShaderiv; -PFNGLCREATEPROGRAMPROC glCreateProgram; -PFNGLDELETEPROGRAMPROC glDeleteProgram; -PFNGLATTACHSHADERPROC glAttachShader; -PFNGLDETACHSHADERPROC glDetachShader; -PFNGLLINKPROGRAMPROC glLinkProgram; -PFNGLGETPROGRAMIVPROC glGetProgramiv; -PFNGLUSEPROGRAMPROC glUseProgram; -PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; -PFNGLUNIFORM1IPROC glUniform1i; -PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv; -PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation; -PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; -PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays; -PFNGLBINDVERTEXARRAYPROC glBindVertexArray; -PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray; -PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray; -PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer; -PFNGLACTIVETEXTUREPROC glActiveTexture; -PFNGLGETINTEGERVPROC glGetIntegerv; -PFNGLGETSTRINGPROC glGetString; -PFNGLGETSTRINGIPROC glGetStringi; -PFNGLPOLYGONMODEPROC glPolygonMode; -PFNGLSCISSORPROC glScissor; -PFNGLDRAWELEMENTSPROC glDrawElements; -PFNGLBLENDEQUATIONPROC glBlendEquation; -PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate; -} - -bool ImGui_ImplOpenGL3_Init(const char*) { - // Load OpenGL functions - HMODULE opengl32 = GetModuleHandle(L"opengl32.dll"); - if (!opengl32) return false; - - glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers"); - glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)wglGetProcAddress("glDeleteBuffers"); - glBindBuffer = (PFNGLBINDBUFFERPROC)wglGetProcAddress("glBindBuffer"); - glBufferData = (PFNGLBUFFERDATAPROC)wglGetProcAddress("glBufferData"); - glCreateShader = (PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"); - glDeleteShader = (PFNGLDELETESHADERPROC)wglGetProcAddress("glDeleteShader"); - glShaderSource = (PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource"); - glCompileShader = (PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader"); - glGetShaderiv = (PFNGLGETSHADERIVPROC)wglGetProcAddress("glGetShaderiv"); - glCreateProgram = (PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram"); - glDeleteProgram = (PFNGLDELETEPROGRAMPROC)wglGetProcAddress("glDeleteProgram"); - glAttachShader = (PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader"); - glDetachShader = (PFNGLDETACHSHADERPROC)wglGetProcAddress("glDetachShader"); - glLinkProgram = (PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram"); - glGetProgramiv = (PFNGLGETPROGRAMIVPROC)wglGetProcAddress("glGetProgramiv"); - glUseProgram = (PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram"); - glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation"); - glUniform1i = (PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i"); - glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)wglGetProcAddress("glUniformMatrix4fv"); - glGetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC)wglGetProcAddress("glGetAttribLocation"); - glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)wglGetProcAddress("glGenVertexArrays"); - glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)wglGetProcAddress("glDeleteVertexArrays"); - glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)wglGetProcAddress("glBindVertexArray"); - glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glEnableVertexAttribArray"); - glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glDisableVertexAttribArray"); - glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)wglGetProcAddress("glVertexAttribPointer"); - glActiveTexture = (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture"); - glGetIntegerv = (PFNGLGETINTEGERVPROC)GetProcAddress(opengl32, "glGetIntegerv"); - glGetString = (PFNGLGETSTRINGPROC)GetProcAddress(opengl32, "glGetString"); - glGetStringi = (PFNGLGETSTRINGIPROC)wglGetProcAddress("glGetStringi"); - glPolygonMode = (PFNGLPOLYGONMODEPROC)GetProcAddress(opengl32, "glPolygonMode"); - glScissor = (PFNGLSCISSORPROC)GetProcAddress(opengl32, "glScissor"); - glDrawElements = (PFNGLDRAWELEMENTSPROC)GetProcAddress(opengl32, "glDrawElements"); - glBlendEquation = (PFNGLBLENDEQUATIONPROC)wglGetProcAddress("glBlendEquation"); - glBlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEPROC)wglGetProcAddress("glBlendFuncSeparate"); - - return true; -} - -#endif // _WIN32 diff --git a/include/anim/channel.hpp b/include/anim/channel.hpp index 5c890f3..1409c94 100644 --- a/include/anim/channel.hpp +++ b/include/anim/channel.hpp @@ -215,17 +215,17 @@ class Channel { using KeyframeIt = std::vector::iterator; const Keyframe& create_default_keyframe(const Point& position, Function function, HandleMode handle_mode); - const Keyframe& insert_keyframe(Keyframe&& keyframe, GrabbedHandle grabbed_handle = GrabbedHandle::none); - const Keyframe& insert_keyframe(KeyframeIt it, Keyframe&& keyframe, GrabbedHandle grabbed_handle = GrabbedHandle::none); + const Keyframe& insert_keyframe(Keyframe&& keyframe, GrabbedHandle grabbed_handle = GrabbedHandle::None); + const Keyframe& insert_keyframe(KeyframeIt it, Keyframe&& keyframe, GrabbedHandle grabbed_handle = GrabbedHandle::None); void update_keyframe_position(KeyframeIt it, const Point& position); void clamp_keyframe_time(KeyframeIt it, double time); - void update_local_handles(KeyframeIt it, GrabbedHandle grabbed_handle = GrabbedHandle::none); + void update_local_handles(KeyframeIt it, GrabbedHandle grabbed_handle = GrabbedHandle::None); void update_handles( Keyframe& keyframe, Keyframe* prev_keyframe_ptr, Keyframe* next_keyframe_ptr, - GrabbedHandle grabbed_handle = GrabbedHandle::none); + GrabbedHandle grabbed_handle = GrabbedHandle::None); void apply_last_keyframe_inheritance(bool restore_cache = true) const; void invalidate_cache() const; diff --git a/include/anim/handle_utils.hpp b/include/anim/handle_utils.hpp index 58fec70..f80a10c 100644 --- a/include/anim/handle_utils.hpp +++ b/include/anim/handle_utils.hpp @@ -8,9 +8,9 @@ namespace anim { /// @brief Identifies which handle of a keyframe is being dragged, driving alignment behavior. enum class GrabbedHandle { - none, ///< No specific handle; the implementation picks a source automatically. - in_handle, ///< The incoming handle is the one being manipulated. - out_handle ///< The outgoing handle is the one being manipulated. + None, ///< No specific handle; the implementation picks a source automatically. + InHandle, ///< The incoming handle is the one being manipulated. + OutHandle ///< The outgoing handle is the one being manipulated. }; /** @@ -66,7 +66,7 @@ namespace anim { Vector reflect(const Vector& vec, const Vector& normal_unit_vector); /// @brief Returns @p vec with both components negated. - Vector invert(const Vector& vec) ; + Vector invert(const Vector& vec); /// @brief Clamps a keyframe's in-handle time into [prev keyframe time, keyframe time]. void constrain_in_handle_time(Keyframe& keyframe, const Keyframe& prev_keyframe); @@ -149,7 +149,7 @@ namespace anim { Keyframe& keyframe, const Keyframe* prev_keyframe_ptr, const Keyframe* next_keyframe_ptr, - GrabbedHandle grabbed_handle = GrabbedHandle::none); + GrabbedHandle grabbed_handle = GrabbedHandle::None); } // namespace anim diff --git a/include/anim/id.hpp b/include/anim/id.hpp index fc8a929..124b371 100644 --- a/include/anim/id.hpp +++ b/include/anim/id.hpp @@ -40,14 +40,14 @@ struct Id { /** * @brief Returns the sentinel "invalid" Id (the maximum uint64_t value). - * @return An Id for which isValid() is false. + * @return An Id for which is_valid() is false. */ static Id invalid() { return Id(static_cast(-1)); } /// @brief True unless this Id equals the invalid() sentinel. - bool isValid() const { + bool is_valid() const { return id != static_cast(-1); } }; diff --git a/src/channel.cpp b/src/channel.cpp index 1defed5..9aa88f8 100644 --- a/src/channel.cpp +++ b/src/channel.cpp @@ -200,7 +200,7 @@ void Channel::set_keyframe_in_handle(size_t index, const Point& in_handle) } auto it = m_keyframes.begin() + index; it->in_handle = in_handle; - update_local_handles(it, GrabbedHandle::in_handle); + update_local_handles(it, GrabbedHandle::InHandle); // Invalidate cache if we updated the last keyframe's handles if (index == m_keyframes.size() - 1) { @@ -215,7 +215,7 @@ void Channel::set_keyframe_out_handle(size_t index, const Point& out_handle) } auto it = m_keyframes.begin() + index; it->out_handle = out_handle; - update_local_handles(it, GrabbedHandle::out_handle); + update_local_handles(it, GrabbedHandle::OutHandle); // Invalidate cache if we updated the last keyframe's handles if (index == m_keyframes.size() - 1) { diff --git a/src/handle_utils.cpp b/src/handle_utils.cpp index 5019d56..1c54894 100644 --- a/src/handle_utils.cpp +++ b/src/handle_utils.cpp @@ -314,11 +314,11 @@ namespace anim { // Determine source and target handles - if (grabbed_handle == GrabbedHandle::out_handle) { + if (grabbed_handle == GrabbedHandle::OutHandle) { source_handle_ptr = &(keyframe.out_handle); target_handle_ptr = &(keyframe.in_handle); source_is_out = true; - } else if (grabbed_handle == GrabbedHandle::in_handle) { + } else if (grabbed_handle == GrabbedHandle::InHandle) { source_handle_ptr = &(keyframe.in_handle); target_handle_ptr = &(keyframe.out_handle); source_is_out = false; diff --git a/tests/test_handle_utils.cpp b/tests/test_handle_utils.cpp index a2e003d..d2ea772 100644 --- a/tests/test_handle_utils.cpp +++ b/tests/test_handle_utils.cpp @@ -265,7 +265,7 @@ TEST_CASE("Handle mode functions", "[handle_utils]") { kf.out_handle = Point(7.0, 7.0); kf.in_handle = Point(3.0, 4.0); - apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::out_handle); // out_handle is source + apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::OutHandle); // out_handle is source // in_handle should be aligned with out_handle Vector in_vec = vector(kf.in_handle, kf.position); @@ -510,15 +510,15 @@ TEST_CASE("apply_aligned_handles source selection and modes", "[handle_utils]") Keyframe kf(5.0, 5.0); kf.in_handle = Point(3.0, 4.0); kf.out_handle = Point(7.0, 7.0); - apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::in_handle); + apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::InHandle); REQUIRE(collinearity(kf) < 1e-9); } - SECTION("GrabbedHandle::none picks the larger handle as source and aligns") { + SECTION("GrabbedHandle::None picks the larger handle as source and aligns") { Keyframe kf(5.0, 5.0); kf.in_handle = Point(4.5, 4.8); // small magnitude kf.out_handle = Point(9.0, 9.0); // large magnitude -> chosen as source - apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::none); + apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::None); REQUIRE(collinearity(kf) < 1e-9); } @@ -527,7 +527,7 @@ TEST_CASE("apply_aligned_handles source selection and modes", "[handle_utils]") kf.handle_mode = HandleMode::AlignStrict; kf.out_handle = Point(7.0, 7.0); kf.in_handle = Point(4.0, 4.5); - apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::out_handle); + apply_aligned_handles(kf, nullptr, nullptr, GrabbedHandle::OutHandle); double in_mag = distance(kf.position, kf.in_handle); double out_mag = distance(kf.position, kf.out_handle); diff --git a/tests/test_id_functionality.cpp b/tests/test_id_functionality.cpp index 19c80f4..dcc40ca 100644 --- a/tests/test_id_functionality.cpp +++ b/tests/test_id_functionality.cpp @@ -5,6 +5,7 @@ #include #include #include +#include using namespace anim; @@ -22,9 +23,9 @@ TEST_CASE("Channel ID Management", "[Animation][Id]") { REQUIRE(ch2.id() != ch3.id()); // IDs should be valid - REQUIRE(ch1.id().isValid()); - REQUIRE(ch2.id().isValid()); - REQUIRE(ch3.id().isValid()); + REQUIRE(ch1.id().is_valid()); + REQUIRE(ch2.id().is_valid()); + REQUIRE(ch3.id().is_valid()); // IDs should be incrementing (implementation detail but good to check) REQUIRE(static_cast(ch2.id()) > static_cast(ch1.id())); @@ -50,7 +51,7 @@ TEST_CASE("Channel ID Management", "[Animation][Id]") { Animation animation("test"); Channel& ch = animation.create_channel("managed_channel"); - REQUIRE(ch.id().isValid()); + REQUIRE(ch.id().is_valid()); REQUIRE(ch.name() == "managed_channel"); } } @@ -104,9 +105,9 @@ TEST_CASE("Channel ID functionality", "[Channel][Id]") { Channel& ch3 = animation.create_channel("channel3"); // Check that IDs are assigned and unique - REQUIRE(ch1.id().isValid()); - REQUIRE(ch2.id().isValid()); - REQUIRE(ch3.id().isValid()); + REQUIRE(ch1.id().is_valid()); + REQUIRE(ch2.id().is_valid()); + REQUIRE(ch3.id().is_valid()); REQUIRE(ch1.id() != ch2.id()); REQUIRE(ch1.id() != ch3.id()); @@ -125,7 +126,7 @@ TEST_CASE("Channel ID functionality", "[Channel][Id]") { Channel& ch = animation.create_channel("test_channel"); Id channel_id = ch.id(); - REQUIRE(channel_id.isValid()); + REQUIRE(channel_id.is_valid()); REQUIRE(static_cast(channel_id) >= 1); } @@ -434,11 +435,16 @@ TEST_CASE("Static channel ID counter", "[Animation][Id]") { TEST_CASE("Edge cases and error conditions", "[Animation][Id]") { SECTION("Cannot create channel without ID") { - // This test verifies that the Channel() default constructor is deleted - // The test is implicit - if this compiles, the test passes - // If Channel() were available, this would not compile: - // Channel ch; // This should not compile - REQUIRE(true); // Placeholder assertion + // Channels may only be created by Animation, which is what guarantees + // every channel gets a unique Id. Enforce that contract at compile time + // rather than asserting it in prose. + static_assert(!std::is_default_constructible_v, + "Channel must not be default-constructible; Animation owns creation."); + static_assert(!std::is_copy_constructible_v, + "Channel must not be copy-constructible; a copy would duplicate its Id."); + static_assert(!std::is_copy_assignable_v, + "Channel must not be copy-assignable; assignment would overwrite its Id."); + SUCCEED("Channel construction is restricted to Animation"); } SECTION("Remove non-existent channel by ID throws exception") { @@ -528,10 +534,10 @@ TEST_CASE("Id ordering and hashing", "[Id]") { SECTION("explicit conversion and invalid() sentinel") { Id id(12345); REQUIRE(static_cast(id) == 12345); - REQUIRE(id.isValid()); + REQUIRE(id.is_valid()); Id invalid = Id::invalid(); - REQUIRE_FALSE(invalid.isValid()); + REQUIRE_FALSE(invalid.is_valid()); REQUIRE(invalid.id == static_cast(-1)); } }