diff --git a/.gitignore b/.gitignore index d3a5943..c723d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,21 +48,6 @@ build* # meson wrap likes to add this file .meson-subproject-wrap-hash.txt -# Ignore auto-promoted wrap-redirect files from subprojects. -# Only directly-needed wraps are whitelisted below. -subprojects/*.wrap -!subprojects/catch2.wrap -!subprojects/cli11.wrap -!subprojects/colormap_shaders.wrap -!subprojects/eigen.wrap -!subprojects/fmt.wrap -!subprojects/imgui.wrap -!subprojects/mshio.wrap -!subprojects/nlohmann_json.wrap -!subprojects/partio.wrap -!subprojects/spdlog.wrap -!subprojects/zipper.wrap - docs/ imgui.ini diff --git a/core/include/balsa/json/annotations.hpp b/core/include/balsa/json/annotations.hpp new file mode 100644 index 0000000..063ff65 --- /dev/null +++ b/core/include/balsa/json/annotations.hpp @@ -0,0 +1,48 @@ +#pragma once +// Member/type annotations for balsa's reflection-driven JSON +// (balsa/json/reflection.hpp). Lightweight: no JSON library is included, so +// headers declaring annotated types can include this cheaply. +// +// Applying annotations (`[[=json::required]]`) requires C++26 reflection +// annotations (P2996 + P3394) +#include +#if !defined(__cpp_impl_reflection) || !defined(__cpp_lib_reflection) +#error "balsa/json/annotations.hpp requires C++26 reflection" +#endif + +namespace balsa::json { + +// How a member is written and read. Annotate a member to pick its policy, +// e.g. `[[=json::omit_empty]] std::vector ids;`, or annotate a type to +// change the policy of its unannotated members, e.g. +// `struct [[=json::members(json::omit_empty)]] Foo { ... };`. +// Unannotated members of unannotated types are json::required. +enum class policy_kind { + // Always written; reading throws json::out_of_range when missing. + required, + // Skipped on write when empty (unset optional, empty container/string, + // empty RawJson); missing on read means default-constructed. + omit_empty, + // Skipped on write when equal to the value in a default-constructed + // object; missing on read means that default. + skip_default, + // Not written by to_json_members; the caller writes it by hand (e.g. + // "only one of" rules). Missing on read means default-constructed. + manual_write, +}; + +struct policy { + policy_kind kind; +}; +inline constexpr policy required{policy_kind::required}; +inline constexpr policy omit_empty{policy_kind::omit_empty}; +inline constexpr policy skip_default{policy_kind::skip_default}; +inline constexpr policy manual_write{policy_kind::manual_write}; + +// Type annotation setting the default policy of its unannotated members. +struct members_t { + policy default_policy; +}; +consteval auto members(policy p) -> members_t { return {p}; } + +} // namespace balsa::json diff --git a/core/include/balsa/json/raw_json.hpp b/core/include/balsa/json/raw_json.hpp new file mode 100644 index 0000000..0b7041f --- /dev/null +++ b/core/include/balsa/json/raw_json.hpp @@ -0,0 +1,17 @@ +#pragma once +// Unparsed JSON text, for values whose structure is open-ended (e.g. glTF +// extensions/extras). Lightweight: no JSON library is included; conversion +// to and from parsed JSON lives in balsa/json/reflection.hpp. +#include + +namespace balsa::json { + +struct RawJson { + // Serialized JSON value; empty means absent. + std::string text; + + auto empty() const -> bool { return text.empty(); } + auto operator==(const RawJson &) const -> bool = default; +}; + +} // namespace balsa::json diff --git a/core/include/balsa/json/reflection.hpp b/core/include/balsa/json/reflection.hpp new file mode 100644 index 0000000..d39d9e8 --- /dev/null +++ b/core/include/balsa/json/reflection.hpp @@ -0,0 +1,194 @@ +#pragma once +// Reflection-driven nlohmann::json (de)serialization. Heavy (includes +// nlohmann/json.hpp): include it from .cpp files; headers declaring annotated +// types only need balsa/json/annotations.hpp. +// +// Requires C++26 static reflection with annotations (P2996 + P3394). +#include +#if !defined(__cpp_impl_reflection) || !defined(__cpp_lib_reflection) +#error "balsa/json/reflection.hpp requires C++26 reflection" +#endif + +#include "annotations.hpp" +#include "raw_json.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +// C++26 makes std::optional a range, which sends nlohmann's built-in +// optional support down its container path. Handle it explicitly. +template +struct nlohmann::adl_serializer> { + static void to_json(json &j, const std::optional &opt) { + if (opt) { + j = *opt; + } else { + j = nullptr; + } + } + static void from_json(const json &j, std::optional &opt) { + if (j.is_null()) { + opt.reset(); + } else { + opt = j.get(); + } + } +}; + +namespace balsa::json { + +// to_json / from_json for RawJson: parse on write, serialize on read. +inline auto to_json(nlohmann::json &j, const RawJson &raw) -> void { + j = nlohmann::json::parse(raw.text); +} +inline auto from_json(const nlohmann::json &j, RawJson &raw) -> void { + raw.text = j.dump(); +} + +namespace detail { + template + concept emptiable = requires(const T &v) { v.has_value(); } + || requires(const T &v) { v.empty(); }; + + // Unset optionals, empty containers/strings and null or empty JSON values. + template + constexpr auto is_empty(const T &value) -> bool { + if constexpr (requires { value.has_value(); }) { + return !value.has_value(); + } else { + return value.empty(); + } + } + + template + requires std::is_convertible_v + auto from_with_default(const nlohmann::json &j, + std::string_view key, + T &value, + const U &default_value) -> void { + if (auto it = j.find(key); it != j.end()) { + it->get_to(value); + } else { + value = default_value; + } + } + + // Annotation of type A on `entity`, if any; at most one is allowed. + template + consteval auto annotation(std::meta::info entity) -> std::optional { + auto found = std::meta::annotations_of_with_type(entity, ^^A); + if (found.empty()) { return std::nullopt; } + if (found.size() > 1) { + throw std::meta::exception( + u8"multiple balsa::json annotations on one entity", entity); + } + return std::meta::extract(found.front()); + } + + // The member's own policy, else its type's default, else required. + consteval auto policy_of(std::meta::info type, std::meta::info member) + -> policy_kind { + if (auto p = annotation(member)) { return p->kind; } + if (auto m = annotation(type)) { + return m->default_policy.kind; + } + return policy_kind::required; + } + + // Write member `m` of `v` under policy `kind`. + template + auto write_member(nlohmann::json &j, const T &v) -> void { + static_assert(std::meta::parent_of(m) == ^^T, + "member does not belong to this type"); + constexpr std::string_view key = std::meta::identifier_of(m); + if constexpr (kind == policy_kind::required) { + j[key] = v.[:m:]; + } else if constexpr (kind == policy_kind::omit_empty) { + static_assert(detail::emptiable, + "json::omit_empty members must be optional or have " + "empty(); use json::required or json::skip_default"); + if (!detail::is_empty(v.[:m:])) { j[key] = v.[:m:]; } + } else if constexpr (kind == policy_kind::skip_default) { + static const T default_value{}; + if (!(v.[:m:] == default_value.[:m:])) { j[key] = v.[:m:]; } + } + } +} // namespace detail + +template +auto to_json_members(nlohmann::json &j, const T &v) -> void { + static constexpr auto data_members = + std::define_static_array(std::meta::nonstatic_data_members_of( + ^^T, std::meta::access_context::unprivileged())); + + template for (constexpr auto m : data_members) { + detail::write_member(j, v); + } +} + +// Write only the given members, e.g. from code handling json::manual_write +// members: `json::write_members(j, v);`. +// Without a policy the members are always written. +template + requires(P.kind != policy_kind::manual_write) +auto write_members(nlohmann::json &j, const T &v) -> void { + (detail::write_member(j, v), ...); +} +template +auto write_members(nlohmann::json &j, const T &v) -> void { + write_members(j, v); +} + +template +auto from_json_members(const nlohmann::json &j, T &v) -> void { + static const T default_value{}; + static constexpr auto data_members = + std::define_static_array(std::meta::nonstatic_data_members_of( + ^^T, std::meta::access_context::unprivileged())); + template for (constexpr auto m : data_members) { + constexpr std::string_view key = std::meta::identifier_of(m); + if constexpr (detail::policy_of(^^T, m) == policy_kind::required) { + j.at(key).get_to(v.[:m:]); + } else { + detail::from_with_default(j, key, v.[:m:], default_value.[:m:]); + } + } +} + +// Serialize an enum as its enumerator name. +template + requires std::is_enum_v +auto enum_to_json(nlohmann::json &j, const E &e) -> void { + template for (constexpr auto en : + std::define_static_array(std::meta::enumerators_of(^^E))) { + if (e == [:en:]) { + j = std::meta::identifier_of(en); + return; + } + } + throw std::invalid_argument( + "enum_to_json: value is not a named enumerator"); +} + +template + requires std::is_enum_v +auto enum_from_json(const nlohmann::json &j, E &e) -> void { + const auto &name = j.get_ref(); + template for (constexpr auto en : + std::define_static_array(std::meta::enumerators_of(^^E))) { + if (name == std::meta::identifier_of(en)) { + e = [:en:]; + return; + } + } + throw std::invalid_argument("enum_from_json: unknown enumerator '" + name + + "'"); +} + +} // namespace balsa::json diff --git a/core/meson.build b/core/meson.build index b6b1ccc..c86a016 100644 --- a/core/meson.build +++ b/core/meson.build @@ -33,8 +33,26 @@ endif if get_option('json') + # balsa's JSON (de)serialization is driven by C++26 static reflection with + # annotations (balsa/json/reflection.hpp). As of 2026 only GCC 16+ supports + # it, and only with -freflection. Disable with -Djson=false otherwise. + reflection_args = ['-freflection'] + reflection_probe = ''' +#include +#if !defined(__cpp_impl_reflection) || !defined(__cpp_lib_reflection) +#error "no C++26 reflection" +#endif +''' + if not cc.compiles(reflection_probe, args: reflection_args, + name: 'C++26 reflection (__cpp_impl_reflection, __cpp_lib_reflection)') + error('-Djson=true requires C++26 reflection (__cpp_impl_reflection / ' + + '__cpp_lib_reflection), e.g. GCC 16+ with -freflection; ' + + 'disable it with -Djson=false') + endif nlohmann_json_dep = dependency('nlohmann_json') - core_required_deps += nlohmann_json_dep + # Propagated: headers using balsa::json annotations need -freflection too. + core_required_deps += [nlohmann_json_dep, + declare_dependency(compile_args: reflection_args)] endif # balsa::parallel dispatches to std::execution::par when TBB is available. diff --git a/core/tests/eigen/meson.build b/core/tests/eigen/meson.build index c406c97..0db52e2 100644 --- a/core/tests/eigen/meson.build +++ b/core/tests/eigen/meson.build @@ -4,5 +4,5 @@ eigen_test_sources = [ 'test_eigen_eigen2span.cpp', 'test_zipper_compat.cpp' ] -test_eigen = executable('test_eigen', eigen_test_sources , dependencies: [core_dep,catch2_with_main_dep]) +test_eigen = executable('test_eigen', eigen_test_sources , dependencies: [core_dep,eigen_dep,catch2_with_main_dep]) test('eigen test', test_eigen) diff --git a/core/tests/meson.build b/core/tests/meson.build index 0e6c799..1f181f0 100644 --- a/core/tests/meson.build +++ b/core/tests/meson.build @@ -2,4 +2,6 @@ catch2_with_main_dep = dependency('catch2-with-main', allow_fallback: true) test_iterators= executable('test_iterators', 'test_iterators.cpp', dependencies: [core_dep,catch2_with_main_dep]) test('iterator test', test_iterators) -subdir('eigen') +if eigen_dep.found() + subdir('eigen') +endif diff --git a/geometry/include/balsa/geometry/bounding_box.hpp b/geometry/include/balsa/geometry/bounding_box.hpp index ecdfc2b..a1551ed 100644 --- a/geometry/include/balsa/geometry/bounding_box.hpp +++ b/geometry/include/balsa/geometry/bounding_box.hpp @@ -1,23 +1,10 @@ #if !defined(BALSA_GEOMETRY_BOUNDING_BOX) #define BALSA_GEOMETRY_BOUNDING_BOX #include "BoundingBox.hpp" -#include "balsa/eigen/concepts/shape_types.hpp" -#include "balsa/eigen/types.hpp" -#include #include namespace balsa::geometry { -template -auto bounding_box(const VType &V) { - using BBox = - Eigen::AlignedBox; - if (V.cols() > 0) { - return BBox{V.rowwise().minCoeff(), V.rowwise().maxCoeff()}; - } - return BBox{}; -} - template <::zipper::concepts::Matrix VType> auto bounding_box(const VType &V) { using T = typename VType::value_type; diff --git a/geometry/include/balsa/geometry/curves/volume.hpp b/geometry/include/balsa/geometry/curves/volume.hpp index 6b006a9..ca555ed 100644 --- a/geometry/include/balsa/geometry/curves/volume.hpp +++ b/geometry/include/balsa/geometry/curves/volume.hpp @@ -5,64 +5,12 @@ #include #include -#include "balsa/eigen/types.hpp" -#include "balsa/eigen/concepts/shape_types.hpp" -#include "balsa/eigen/shape_checks.hpp" #include "balsa/zipper/concepts/shape_types.hpp" #include namespace balsa::geometry::curves { -// ════════════════════════════════════════════════════════════════════════════ -// Eigen overloads -// ════════════════════════════════════════════════════════════════════════════ - -// Signed area enclosed by a 2D closed polygon (shoelace formula). -// V is a 2xN matrix of column-vector vertices, traversed in order. -// Positive = counter-clockwise winding, negative = clockwise. -template -auto curve_volume(const Eigen::MatrixBase &V) -> - typename Derived::Scalar { - typename Derived::Scalar ret = 0; - static_assert(Derived::RowsAtCompileTime == 2 || Derived::RowsAtCompileTime == Eigen::Dynamic); - if (V.rows() != 2) { - throw std::invalid_argument("curve_volume only works on ColVecs2d-like inputs"); - } - for (int i = 0; i < V.cols(); ++i) { - auto a = V.col(i); - auto b = V.col((i + 1) % V.cols()); - ret += a.x() * b.y() - a.y() * b.x(); - } - return .5 * ret; -} - -// Signed area of a 2D polygon with vertices indexed by [beginit, endit). -template -auto curve_volume(const Derived &V, const BeginIt &beginit, const EndIt &endit) -> typename Derived::Scalar { - auto it = beginit; - auto it1 = beginit; - it1++; - typename Derived::Scalar ret = 0; - eigen::row_check_with_throw<2>(V); - for (; it != endit; ++it, ++it1) { - if (it1 == endit) { - it1 = beginit; - } - auto a = V.col(*it); - auto b = V.col(*it1); - ret += a.x() * b.y() - a.y() * b.x(); - } - return .5 * ret; -} - -// Signed area of a 2D polygon with vertices indexed by a container. -template -auto curve_volume(const Eigen::MatrixBase &V, const Container &C) -> - typename Derived::Scalar { - return curve_volume(V, C.begin(), C.end()); -} - // ════════════════════════════════════════════════════════════════════════════ // Zipper overloads // ════════════════════════════════════════════════════════════════════════════ diff --git a/geometry/include/balsa/geometry/get_angle.hpp b/geometry/include/balsa/geometry/get_angle.hpp index cf64970..9447291 100644 --- a/geometry/include/balsa/geometry/get_angle.hpp +++ b/geometry/include/balsa/geometry/get_angle.hpp @@ -3,8 +3,6 @@ #include #include -#include "balsa/eigen/concepts/shape_types.hpp" - #include #include #include @@ -40,39 +38,6 @@ S get_angle_scalar(S cos, S sin) { return std::atan2(sin, cos); } -// ── Eigen overloads ──────────────────────────────────────────────────── - -template -typename T::Scalar get_angle(const T &v) { - return get_angle_scalar(v.x(), v.y()); -} - -template -typename T::Scalar get_angle(const T &a, const U &b) { - return get_angle(b) - get_angle(a); -} - -// Returns the angle for each vector in a ColVector and returns a rowvector -template -auto get_angle_from_colvectors(const T &P) { - using S = typename T::Scalar; - Eigen::Matrix A(1, P.cols()); - - auto a = P.row(0); - auto b = P.row(1); - for (int i = 0; i < P.cols(); ++i) { - A(i) = get_angle_scalar(a(i), b(i)); - } - return A; -} - -// Returns the angle of between each col from A to B clockwise -template -auto get_angle_from_colvectors(const Derived &A, const Derived1 &B) { - auto R = (get_angle(B) - get_angle(A)).eval(); - return R; -} - // ── Zipper overloads ─────────────────────────────────────────────────── template<::zipper::concepts::Vector T> diff --git a/geometry/include/balsa/geometry/point_cloud/uniformly_sample_simplex.hpp b/geometry/include/balsa/geometry/point_cloud/uniformly_sample_simplex.hpp index 091e60a..f7a9819 100644 --- a/geometry/include/balsa/geometry/point_cloud/uniformly_sample_simplex.hpp +++ b/geometry/include/balsa/geometry/point_cloud/uniformly_sample_simplex.hpp @@ -6,13 +6,9 @@ // // Wraps balsa::geometry::simplex::uniformly_sample_simplex with a // (vertices, rng) argument order. -// Eigen overloads convert to zipper and forward. #include "balsa/geometry/simplex/uniformly_sample_simplex.hpp" -#include "balsa/eigen/concepts/matrix_types.hpp" -#include "balsa/eigen/zipper_compat.hpp" - #include #include @@ -34,27 +30,6 @@ auto uniformly_sample_simplex(const MatType &V, std::size_t num_samples, RNG &ge return simplex::uniformly_sample_simplex(gen, V, num_samples); } -// ── Eigen overloads (convert to zipper, then delegate) ───────────────── - -/// Uniformly sample a single point from an N-simplex (Eigen). -/// V: (dim × (N+1)) matrix of column-vector vertices. -template -auto uniformly_sample_simplex(const Derived &V, RNG &gen) - -> Eigen::Vector { - auto result = simplex::uniformly_sample_simplex(gen, eigen::as_zipper(V)); - return eigen::as_eigen(result); -} - -/// Sample multiple uniform random points from an N-simplex (Eigen). -/// Returns a (dim × num_samples) matrix. -template -auto uniformly_sample_simplex(const Derived &V, int num_samples, RNG &gen) - -> Eigen::Matrix { - auto result = simplex::uniformly_sample_simplex( - gen, eigen::as_zipper(V), static_cast(num_samples)); - return eigen::as_eigen(result); -} - }// namespace balsa::geometry::point_cloud #endif diff --git a/geometry/include/balsa/geometry/simplex/circumcenter.hpp b/geometry/include/balsa/geometry/simplex/circumcenter.hpp index 5809431..ba7e440 100644 --- a/geometry/include/balsa/geometry/simplex/circumcenter.hpp +++ b/geometry/include/balsa/geometry/simplex/circumcenter.hpp @@ -36,9 +36,6 @@ #include #include -#include "balsa/eigen/concepts/matrix_types.hpp" -#include "balsa/eigen/zipper_compat.hpp" - namespace balsa::geometry::simplex { // ============================================================================ @@ -249,22 +246,5 @@ auto circumcenter_with_radius(const ID &id) { id.as_tuple()); } -// ── Eigen overloads (convert to zipper) ───────────────────────────── - -template -auto circumcenter(const M &V) { - return circumcenter(eigen::as_zipper(V)); -} - -template -auto circumcenter_with_squared_radius(const M &V) { - return circumcenter_with_squared_radius(eigen::as_zipper(V)); -} - -template -auto circumcenter_with_radius(const M &V) { - return circumcenter_with_radius(eigen::as_zipper(V)); -} - } // namespace balsa::geometry::simplex #endif diff --git a/geometry/include/balsa/geometry/simplex/point_in_circumcircle.hpp b/geometry/include/balsa/geometry/simplex/point_in_circumcircle.hpp index 137d958..ad8a808 100644 --- a/geometry/include/balsa/geometry/simplex/point_in_circumcircle.hpp +++ b/geometry/include/balsa/geometry/simplex/point_in_circumcircle.hpp @@ -21,9 +21,6 @@ #include #include -#include "balsa/eigen/concepts/matrix_types.hpp" -#include "balsa/eigen/zipper_compat.hpp" - namespace balsa::geometry::simplex { // ============================================================================ @@ -75,13 +72,5 @@ auto point_in_circumsphere(const V &P, const M &pts) -> bool { return diff.template norm_powered<2>() < r2; } -// ── Eigen overloads (convert to zipper) ───────────────────────────── - -template -auto point_in_circumsphere(const P &p, const M &S) -> bool { - return point_in_circumsphere(eigen::as_zipper(p), eigen::as_zipper(S)); -} - } // namespace balsa::geometry::simplex #endif diff --git a/geometry/include/balsa/geometry/simplex/volume.hpp b/geometry/include/balsa/geometry/simplex/volume.hpp index 347b7fb..1fa7311 100644 --- a/geometry/include/balsa/geometry/simplex/volume.hpp +++ b/geometry/include/balsa/geometry/simplex/volume.hpp @@ -35,9 +35,6 @@ #include #include -#include "balsa/eigen/concepts/matrix_types.hpp" -#include "balsa/eigen/zipper_compat.hpp" - namespace balsa::geometry::simplex { // ============================================================================ @@ -155,22 +152,5 @@ auto volume(const M &V) -> typename M::value_type { return volume_unsigned(V); } -// ── Eigen overloads (convert to zipper) ───────────────────────────── - -template -auto volume_signed(const M &V) -> typename M::Scalar { - return volume_signed(eigen::as_zipper(V)); -} - -template -auto volume_unsigned(const M &V) -> typename M::Scalar { - return volume_unsigned(eigen::as_zipper(V)); -} - -template -auto volume(const M &V) -> typename M::Scalar { - return volume(eigen::as_zipper(V)); -} - } // namespace balsa::geometry::simplex #endif diff --git a/geometry/include/balsa/geometry/simplicial_complex/boundaries.hpp b/geometry/include/balsa/geometry/simplicial_complex/boundaries.hpp index 50b0e26..303efca 100644 --- a/geometry/include/balsa/geometry/simplicial_complex/boundaries.hpp +++ b/geometry/include/balsa/geometry/simplicial_complex/boundaries.hpp @@ -1,69 +1,21 @@ #if !defined(BALSA_GEOMETRY_SIMPLICIAL_COMPLEX_BOUNDARIES) #define BALSA_GEOMETRY_SIMPLICIAL_COMPLEX_BOUNDARIES -#include "balsa/eigen/types.hpp" #include "balsa/tensor_types.hpp" -#include "balsa/eigen/concepts/shape_types.hpp" #include "balsa/zipper/types.hpp" #include #include #include - namespace balsa::geometry::simplicial_complex { // returns column vectors of simplices one dimension lower than that of hte input // Only for simplices // return type for a k-simplex is a ColVectorsr type; -template -auto boundaries(const CellType &C) { - - - constexpr static int CellSize = CellType::RowsAtCompileTime; - constexpr static int FacetSize = (CellSize < 1) ? CellSize : (CellSize - 1); - using FacetType = balsa::eigen::ColVectors; - - using Index = typename CellType::Scalar; - - using FacetArray = typename std::array; - using CellArray = typename std::array; - - using FacetVector = typename balsa::eigen::Vector; - using CellVector = typename balsa::eigen::Vector; - std::set> facet_set; - - FacetArray f; - Eigen::Map fm(f.data()); - - FacetArray f2; - Eigen::Map fm2(f2.data()); - CellArray c; - Eigen::Map cm(c.data()); - for (int i = 0; i < C.cols(); ++i) { - auto c = C.col(i); - fm = c.template topRows(); - for (int j = 0; j < CellSize; ++j) { - fm2 = fm; - std::sort(f2.begin(), f2.end()); - facet_set.insert(f2); - - auto unused = (j + FacetSize) % CellSize; - fm(j % FacetSize) = c(unused); - } - } - - FacetType F(C.rows() - 1, facet_set.size()); - int idx = 0; - for (auto &&f : facet_set) { - F.col(idx++) = Eigen::Map(f.begin()); - } - return F; -} template<::zipper::concepts::Matrix CellType> auto boundaries(const CellType &C) { - constexpr static int CellSize = CellType::extents_type::static_extent(0); constexpr static int FacetSize = ::zipper::utils::extents::minus(CellSize, 1); using FacetType = balsa::zipper::ColVectors; @@ -106,6 +58,5 @@ auto boundaries(const CellType &C) { return F; } - }// namespace balsa::geometry::simplicial_complex #endif diff --git a/geometry/include/balsa/geometry/triangle_mesh/earclipping.hpp b/geometry/include/balsa/geometry/triangle_mesh/earclipping.hpp index c2fc6c9..edb6b40 100644 --- a/geometry/include/balsa/geometry/triangle_mesh/earclipping.hpp +++ b/geometry/include/balsa/geometry/triangle_mesh/earclipping.hpp @@ -4,8 +4,6 @@ #include #include -#include "balsa/eigen/stl2eigen.hpp" -#include #include "balsa/geometry/winding_number.hpp" #include "balsa/geometry/get_angle.hpp" #include "balsa/zipper/concepts/shape_types.hpp" @@ -17,124 +15,6 @@ namespace balsa::geometry::triangle_mesh { -// ════════════════════════════════════════════════════════════════════════════ -// Eigen earclipping_stl -// ════════════════════════════════════════════════════════════════════════════ - -template - requires std::is_integral_v -std::vector> earclipping_stl( - const VDerived &V, - std::span indices) { - using Index = IndexType; - using Face = std::array; - std::vector stlF; - - - size_t size = indices.size(); - stlF.reserve(size - 2); - double inner_ang_sum = 0; - double outer_ang_sum = 0; - for (size_t i = 0; i < size; ++i) { - Face f{ { indices[i], indices[(i + 1) % size], indices[(i + 2) % size] } }; - const auto &[ai, bi, ci] = f; - auto a = V.col(ai); - auto b = V.col(bi); - auto c = V.col(ci); - double ang = get_positive_clamped_angle(get_angle(c - b, a - b)); - inner_ang_sum += ang; - outer_ang_sum += 2 * std::numbers::pi_v - ang; - } - - bool reverse_orientation = outer_ang_sum < inner_ang_sum; - if (reverse_orientation) { - } - - std::list CL(indices.begin(), indices.end()); - - auto is_earclip = [&](const Face &f) -> bool { - auto a = V.col(f[0]); - auto b = V.col(f[1]); - auto c = V.col(f[2]); - auto cb = c - b; - auto ab = a - b; - if (cb.x() * ab.y() - cb.y() * ab.x() < 1e-10) { - return false; - } - double ang = get_positive_clamped_angle(get_angle(c - b, a - b)); - if (ang > std::numbers::pi_v || ang < 0) { - return false; - } - - for (auto &&i : indices) { - if (i == f[0] || i == f[1] || i == f[2]) { - continue; - } - auto v = V.col(i); - if (interior_winding_number(V, f, v)) { - return false; - } - } - return true; - }; - - while (CL.size() > 3) { - bool earclipped = false; - - for (auto it = CL.begin(); it != CL.end(); ++it) { - auto it1 = it; - it1++; - if (it1 == CL.end()) { - it1 = CL.begin(); - } - auto it2 = it1; - it2++; - if (it2 == CL.end()) { - it2 = CL.begin(); - } - Face f{ { *it, *it1, *it2 } }; - if (reverse_orientation) { - std::swap(f[0], f[2]); - } - if (is_earclip(f)) { - stlF.push_back(f); - CL.erase(it1); - earclipped = true; - break; - } - } - if (!earclipped) { - auto it = CL.begin(); - auto it1 = it; - it1++; - auto it2 = it1; - it2++; - Face f{ { *it, *it1, *it2 } }; - - - stlF.push_back(f); - CL.erase(it1); - } - } - - Face f; - std::copy(CL.begin(), CL.end(), f.begin()); - auto a = V.col(f[0]); - auto b = V.col(f[1]); - auto c = V.col(f[2]); - auto cb = c - b; - auto ab = a - b; - auto cross = cb.x() * ab.y() - cb.y() * ab.x(); - if (cross < -1e-10) { - std::swap(f[0], f[1]); - } else if (std::abs(cross) <= 1e-10) { - // degenerate (collinear) final triangle — skip it - return stlF; - } - stlF.push_back(f); - return stlF; -} - // ════════════════════════════════════════════════════════════════════════════ // Zipper earclipping_stl — native, no Eigen roundtrip // ════════════════════════════════════════════════════════════════════════════ @@ -232,7 +112,6 @@ std::vector> earclipping_stl( it2++; Face f{ { *it, *it1, *it2 } }; - stlF.push_back(f); CL.erase(it1); } @@ -256,17 +135,6 @@ std::vector> earclipping_stl( return stlF; } -// ════════════════════════════════════════════════════════════════════════════ -// Public API — Eigen -// ════════════════════════════════════════════════════════════════════════════ - -// returns balsa::eigen::ColVectors -template -auto earclipping(const VDerived &V, - const Container &C) { - return eigen::stl2eigen(earclipping_stl(V, C)); -} - // ════════════════════════════════════════════════════════════════════════════ // Public API — Zipper (native, no as_eigen roundtrip) // ════════════════════════════════════════════════════════════════════════════ diff --git a/geometry/include/balsa/geometry/winding_number.hpp b/geometry/include/balsa/geometry/winding_number.hpp index 169d579..7d413bd 100644 --- a/geometry/include/balsa/geometry/winding_number.hpp +++ b/geometry/include/balsa/geometry/winding_number.hpp @@ -5,8 +5,6 @@ #include #include -#include "balsa/eigen/types.hpp" -#include "balsa/eigen/shape_checks.hpp" #include "balsa/geometry/get_angle.hpp" #include "balsa/zipper/concepts/shape_types.hpp" @@ -21,59 +19,6 @@ namespace balsa::geometry { // ════════════════════════════════════════════════════════════════════════════ namespace internal::winding_number { - // ── Eigen ────────────────────────────────────────────────────────────── - - // angle formed between A - P to B - P - template - typename ADerived::Scalar three_point_angle( - const ADerived &A, - const BDerived &B, - const PDerived &P) { - using S = typename ADerived::Scalar; - auto a = (A - P).eval(); - auto b = (B - P).eval(); - - S aa = std::atan2(a.y(), a.x()); - S ba = std::atan2(b.y(), b.x()); - return get_symmetric_clamped_angle(ba - aa); - } - - template - typename VDerived::Scalar winding_number_iterator( - const VDerived &V, - const BeginIt &beginit, - const EndIt &endit, - const PDerived &p) { - using S = typename VDerived::Scalar; - S value = 0; - auto it = beginit; - for (; it != endit; ++it) { - auto it1 = it; - it1++; - if (it1 == endit) { - it1 = beginit; - } - - auto a = V.col(*it); - auto b = V.col(*it1); - - auto v = three_point_angle(a, b, p); - - value += v; - } - value *= 0.5 * std::numbers::inv_pi_v; - return value; - } - - template - bool interior_winding_number_iterator(const VDerived &V, - const BeginIt &beginit, - const EndIt &endit, - const Eigen::MatrixBase &p) { - auto v = winding_number_iterator(V, beginit, endit, p); - return std::abs(v) > .5; - } - // ── Zipper ───────────────────────────────────────────────────────────── // angle formed between A - P to B - P (zipper) @@ -133,106 +78,6 @@ namespace internal::winding_number { }// namespace internal::winding_number - -// ════════════════════════════════════════════════════════════════════════════ -// Public API — Eigen overloads -// ════════════════════════════════════════════════════════════════════════════ - -template -typename VDerived::Scalar winding_number(const VDerived &V, - const Container &C, - const Eigen::MatrixBase &p) { - return internal::winding_number::winding_number_iterator(V, C.begin(), C.end(), p); -} - -template -typename VDerived::Scalar winding_number(const VDerived &V, - const std::initializer_list &C, - const Eigen::MatrixBase &p) { - return internal::winding_number::winding_number_iterator(V, C.begin(), C.end(), p); -} - - -template -bool interior_winding_number(const VDerived &V, - const Container &C, - const PDerived &p) { - return internal::winding_number::interior_winding_number_iterator(V, std::begin(C), std::end(C), p); -} - -template -auto mesh_winding_number(const VDerived &V, - const EDerived &E, - const Eigen::MatrixBase &p) { - using S = typename VDerived::Scalar; - if constexpr (PDerived::ColsAtCompileTime == 1) { - S value = 0; - for (int j = 0; j < E.cols(); ++j) { - auto e = E.col(j); - auto a = V.col(e(0)); - auto b = V.col(e(1)); - - value += internal::winding_number::three_point_angle(a, b, p); - } - value *= .5 * std::numbers::inv_pi_v; - return value; - } else { - balsa::eigen::Vector R(p.cols()); - for (int j = 0; j < p.cols(); ++j) { - R(j) = mesh_winding_number(V, E, p.col(j)); - } - - return R; - } -} - -template -double mesh_winding_number(const VDerived &V, - const EDerived &E, - const std::map &boundary_map, - const Eigen::MatrixBase &p) { - using S = typename VDerived::Scalar; - S value = 0; - for (auto &&[idx, sgn] : boundary_map) { - auto e = E.col(idx); - int ai, bi; - if (sgn) { - ai = e(0); - bi = e(1); - } else { - ai = e(1); - bi = e(0); - } - auto a = V.col(ai); - auto b = V.col(bi); - - value += internal::winding_number::three_point_angle(a, b, p); - } - value *= .5 * std::numbers::inv_pi_v; - return value; -} -template -bool interior_mesh_winding_number(const VDerived &V, - const EDerived &E, - const std::map &boundary_map, - const Eigen::MatrixBase &p) { - auto v = mesh_winding_number(V, E, boundary_map, p); - return std::abs(v) > .5; -} - -template -auto interior_mesh_winding_number(const VDerived &V, - const EDerived &E, - const Eigen::MatrixBase &p) { - if constexpr (PDerived::ColsAtCompileTime == 1) { - auto v = mesh_winding_number(V, E, p); - return std::abs(v) > .5; - } else { - return (mesh_winding_number(V, E, p).array().abs() > .5).eval(); - } -} - - // ════════════════════════════════════════════════════════════════════════════ // Public API — Zipper overloads // ════════════════════════════════════════════════════════════════════════════ diff --git a/geometry/tests/test_winding_number.cpp b/geometry/tests/test_winding_number.cpp index 9aeb1d1..f0d07e7 100644 --- a/geometry/tests/test_winding_number.cpp +++ b/geometry/tests/test_winding_number.cpp @@ -1,27 +1,30 @@ #include #include #include -#include +#include +#include +#include +#include using namespace balsa::geometry; TEST_CASE("winding numbers", "[winding_number]") { // Unit square vertices: (0,0), (1,0), (1,1), (0,1) - balsa::eigen::ColVecs2d V(2, 4); - V.col(0) = balsa::eigen::Vec2d(0.0, 0.0); - V.col(1) = balsa::eigen::Vec2d(1.0, 0.0); - V.col(2) = balsa::eigen::Vec2d(1.0, 1.0); - V.col(3) = balsa::eigen::Vec2d(0.0, 1.0); + balsa::zipper::ColVecs2d V(2, 4); + V.col(0) = balsa::zipper::Vec2d{0.0, 0.0}; + V.col(1) = balsa::zipper::Vec2d{1.0, 0.0}; + V.col(2) = balsa::zipper::Vec2d{1.0, 1.0}; + V.col(3) = balsa::zipper::Vec2d{0.0, 1.0}; // CCW curve through vertex indices 0->1->2->3 (wraps back to 0) std::vector curve{ 0, 1, 2, 3 }; // Edge list for the same square - balsa::eigen::ColVecs2i E(2, 4); - E.col(0) << 0, 1; - E.col(1) << 1, 2; - E.col(2) << 2, 3; - E.col(3) << 3, 0; + balsa::zipper::ColVecs2i E(2, 4); + E.col(0) = balsa::zipper::Vec2i{0, 1}; + E.col(1) = balsa::zipper::Vec2i{1, 2}; + E.col(2) = balsa::zipper::Vec2i{2, 3}; + E.col(3) = balsa::zipper::Vec2i{3, 0}; // Boundary map: all edges with positive orientation std::map B; @@ -31,7 +34,7 @@ TEST_CASE("winding numbers", "[winding_number]") { B[3] = true; SECTION("interior point has winding number ~1") { - balsa::eigen::Vec2d p(0.5, 0.5); + balsa::zipper::Vec2d p{0.5, 0.5}; auto wn_curve = winding_number(V, curve, p); CHECK(std::abs(wn_curve - 1.0) < 1e-10); @@ -47,7 +50,7 @@ TEST_CASE("winding numbers", "[winding_number]") { } SECTION("exterior point has winding number ~0") { - balsa::eigen::Vec2d p(2.0, 2.0); + balsa::zipper::Vec2d p{2.0, 2.0}; auto wn_curve = winding_number(V, curve, p); CHECK(std::abs(wn_curve) < 1e-10); @@ -63,7 +66,7 @@ TEST_CASE("winding numbers", "[winding_number]") { } SECTION("another exterior point - negative quadrant") { - balsa::eigen::Vec2d p(-1.0, -1.0); + balsa::zipper::Vec2d p{-1.0, -1.0}; auto wn_curve = winding_number(V, curve, p); CHECK(std::abs(wn_curve) < 1e-10); @@ -72,7 +75,7 @@ TEST_CASE("winding numbers", "[winding_number]") { } SECTION("point near edge - just inside") { - balsa::eigen::Vec2d p(0.5, 0.01); + balsa::zipper::Vec2d p{0.5, 0.01}; auto wn = mesh_winding_number(V, E, p); CHECK(std::abs(wn - 1.0) < 1e-5); @@ -80,7 +83,7 @@ TEST_CASE("winding numbers", "[winding_number]") { } SECTION("point near edge - just outside") { - balsa::eigen::Vec2d p(0.5, -0.01); + balsa::zipper::Vec2d p{0.5, -0.01}; auto wn = mesh_winding_number(V, E, p); CHECK(std::abs(wn) < 1e-5); @@ -88,9 +91,11 @@ TEST_CASE("winding numbers", "[winding_number]") { } SECTION("consistency across all three methods for random points") { + std::mt19937 gen(0); + std::uniform_real_distribution dist(-1.0, 2.0); for (int i = 0; i < 20; ++i) { // Random points in [-1, 2] range (some inside, some outside) - balsa::eigen::Vec2d p = balsa::eigen::Vec2d::Random() * 1.5 + balsa::eigen::Vec2d(0.5, 0.5); + balsa::zipper::Vec2d p{dist(gen), dist(gen)}; auto wn_curve = winding_number(V, curve, p); auto wn_mesh = mesh_winding_number(V, E, p); diff --git a/meson.build b/meson.build index d05d935..0661dde 100644 --- a/meson.build +++ b/meson.build @@ -10,11 +10,14 @@ dl_lib = cc.find_library('dl', required: false) # leaks -DFMT_EXPORT into interface compile_args when built as shared with std::format, # which breaks fmt's own headers for any target that also depends on fmt directly. spdlog_dep = dependency('spdlog', version: '>=1.9.2', default_options: ['tests=disabled', 'default_library=static']) -eigen_dep = dependency('eigen3', version: '>=3.4.0') - zipper_dep = dependency('zipper', default_options: ['testing=false']) -core_required_deps = [spdlog_dep, eigen_dep, zipper_dep] +core_required_deps = [spdlog_dep, zipper_dep] + +# Eigen is optional: balsa/eigen/* headers (e.g. zipper_compat.hpp) are +# shipped for consumers that bring their own Eigen; their tests only build +# when Eigen is found. +eigen_dep = dependency('eigen3', version: '>=3.4.0', required: false) # ── Optional dependencies (system packages) ──────────────────────────────── if get_option('protobuf') diff --git a/meson_options.txt b/meson_options.txt index 1493e22..4b14f9c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -9,6 +9,7 @@ option('partio', type : 'boolean', value : false, description : 'Use partio') option('imgui', type : 'boolean', value : true, description : 'Build the ImGui UI frontend (currently requires GLFW)') option('alembic', type : 'boolean', value : false, description : 'Use alembic') option('perfetto', type : 'boolean', value : false, description : 'Use perfetto') +option('draco', type : 'boolean', value : false, description : 'Use draco for mesh compression') option('visualization', type : 'boolean', value : true, description : 'Build visualization library') option('glfw', type : 'boolean', value : true, description : 'Build the GLFW window backend') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 0068da2..9341ed3 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -3,8 +3,21 @@ !packagefiles/ !packagefiles/colormap_shaders/ !packagefiles/colormap_shaders/meson.build +!draco/ +!draco/meson.build -# Auto-generated wrap-redirects from transitive deps -lua.wrap -mdspan.wrap -sol2.wrap +# Ignore auto-promoted wrap-redirect files from transitive deps. +# Only directly-needed wraps are whitelisted below. +*.wrap +!catch2.wrap +!cli11.wrap +!colormap_shaders.wrap +!draco-src.wrap +!eigen.wrap +!fmt.wrap +!imgui.wrap +!mshio.wrap +!nlohmann_json.wrap +!partio.wrap +!spdlog.wrap +!zipper.wrap diff --git a/subprojects/draco-src.wrap b/subprojects/draco-src.wrap new file mode 100644 index 0000000..bbadcb8 --- /dev/null +++ b/subprojects/draco-src.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://github.com/google/draco +revision = 1.5.9 diff --git a/subprojects/draco/meson.build b/subprojects/draco/meson.build new file mode 100644 index 0000000..72d9f72 --- /dev/null +++ b/subprojects/draco/meson.build @@ -0,0 +1,18 @@ +# Thin meson wrapper around draco's CMake build. Provides `draco_dep` so +# consumers can simply call dependency('draco'). +project('draco', 'cpp') + +cmake = import('cmake') +draco_opts = cmake.subproject_options() +draco_opts.add_cmake_defines({ + 'BUILD_SHARED_LIBS' : false, + 'DRACO_TESTS' : false, + 'DRACO_TRANSCODER_SUPPORTED' : false, +}) +draco_sub = cmake.subproject('draco-src', options : draco_opts) + +# draco's CMake names its library target `draco` under MSVC and +# `draco_static` elsewhere; meson ignores the draco::draco alias. +draco_target = meson.get_compiler('cpp').get_id() == 'msvc' ? 'draco' : 'draco_static' +draco_dep = draco_sub.dependency(draco_target) +meson.override_dependency('draco', draco_dep) diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Accessor.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Accessor.hpp new file mode 100644 index 0000000..b664d16 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Accessor.hpp @@ -0,0 +1,124 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The datatype of the accessor’s components. +enum class ComponentType : int64_t { + BYTE = 5120, + UNSIGNED_BYTE = 5121, + SHORT = 5122, + UNSIGNED_SHORT = 5123, + UNSIGNED_INT = 5125, + FLOAT = 5126, +}; + +// Specifies if the accessor’s elements are scalars, vectors, or matrices. +enum class AccessorType { + SCALAR, + VEC2, + VEC3, + VEC4, + MAT2, + MAT3, + MAT4, +}; + +// An object pointing to a buffer view containing the indices of deviating +// accessor values. The number of indices is equal to accessor.sparse.count. +// Indices MUST strictly increase. +struct [[=object]] AccessorSparseIndices { + // The index of the buffer view with sparse indices. The referenced buffer + // view MUST NOT have its target or byteStride properties defined. The + // buffer view and the optional byteOffset MUST be aligned to the + // componentType byte length. + [[=json::required]] int64_t bufferView; + // The offset relative to the start of the buffer view in bytes. + [[=json::skip_default]] int64_t byteOffset = 0; + // The indices data type. + [[=json::required]] ComponentType componentType; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// An object pointing to a buffer view containing the deviating accessor values. +// The number of elements is equal to accessor.sparse.count times number of +// components. The elements have the same component type as the base accessor. +// The elements are tightly packed. Data MUST be aligned following the same +// rules as the base accessor. +struct [[=object]] AccessorSparseValues { + // The index of the bufferView with sparse values. The referenced buffer + // view MUST NOT have its target or byteStride properties defined. + [[=json::required]] int64_t bufferView; + // The offset relative to the start of the bufferView in bytes. + [[=json::skip_default]] int64_t byteOffset = 0; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// Sparse storage of accessor values that deviate from their initialization +// value. +struct [[=object]] AccessorSparse { + // Number of deviating accessor values stored in the sparse array. + [[=json::required]] int64_t count; + // An object pointing to a buffer view containing the indices of deviating + // accessor values. The number of indices is equal to count. Indices MUST + // strictly increase. + [[=json::required]] AccessorSparseIndices indices; + // An object pointing to a buffer view containing the deviating accessor + // values. + [[=json::required]] AccessorSparseValues values; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// A typed view into a buffer view that contains raw binary data. +struct [[=object]] Accessor { + // The index of the bufferView. + std::optional bufferView; + // The offset relative to the start of the buffer view in bytes. + [[=json::manual_write]] int64_t byteOffset = 0; + // The datatype of the accessor’s components. + [[=json::required]] ComponentType componentType; + // Specifies whether integer data values are normalized before usage. + [[=json::skip_default]] bool normalized = false; + // The number of elements referenced by this accessor. + [[=json::required]] int64_t count; + // Specifies if the accessor’s elements are scalars, vectors, or matrices. + [[=json::required]] AccessorType type; + // Maximum value of each component in this accessor. + std::vector max; + // Minimum value of each component in this accessor. + std::vector min; + // Sparse storage of elements that deviate from their initialization value. + std::optional sparse; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const AccessorType &v) -> void; +auto from_json(const nlohmann::json &j, AccessorType &v) -> void; +auto to_json(nlohmann::json &j, const AccessorSparseIndices &v) -> void; +auto from_json(const nlohmann::json &j, AccessorSparseIndices &v) -> void; +auto to_json(nlohmann::json &j, const AccessorSparseValues &v) -> void; +auto from_json(const nlohmann::json &j, AccessorSparseValues &v) -> void; +auto to_json(nlohmann::json &j, const AccessorSparse &v) -> void; +auto from_json(const nlohmann::json &j, AccessorSparse &v) -> void; +auto to_json(nlohmann::json &j, const Accessor &v) -> void; +auto from_json(const nlohmann::json &j, Accessor &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Animation.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Animation.hpp new file mode 100644 index 0000000..55b6b6a --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Animation.hpp @@ -0,0 +1,107 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The name of the node’s TRS property to animate, or the "weights" of the Morph +// Targets it instantiates. For the "translation" property, the values that are +// provided by the sampler are the translation along the X, Y, and Z axes. For +// the "rotation" property, the values are a quaternion in the order (x, y, z, +// w), where w is the scalar. For the "scale" property, the values are the +// scaling factors along the X, Y, and Z axes. +enum class AnimationPath { + translation, + rotation, + scale, + weights, +}; + +// Interpolation algorithm. +enum class AnimationInterpolation { + LINEAR, + STEP, + CUBICSPLINE, +}; + +// The descriptor of the animated property. +struct [[=object]] AnimationChannelTarget { + // The index of the node to animate. When undefined, the animated object MAY + // be defined by an extension. + std::optional node; + // The name of the node’s TRS property to animate, or the "weights" of the + // Morph Targets it instantiates. For the "translation" property, the values + // that are provided by the sampler are the translation along the X, Y, and + // Z axes. For the "rotation" property, the values are a quaternion in the + // order (x, y, z, w), where w is the scalar. For the "scale" property, the + // values are the scaling factors along the X, Y, and Z axes. + [[=json::required]] AnimationPath path; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// An animation channel combines an animation sampler with a target property +// being animated. +struct [[=object]] AnimationChannel { + // The index of a sampler in this animation used to compute the value for + // the target. + [[=json::required]] int64_t sampler; + // The descriptor of the animated property. + [[=json::required]] AnimationChannelTarget target; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// An animation sampler combines timestamps with a sequence of output values and +// defines an interpolation algorithm. +struct [[=object]] AnimationSampler { + // The index of an accessor containing keyframe timestamps. + [[=json::required]] int64_t input; + // Interpolation algorithm. + [[=json::skip_default]] AnimationInterpolation interpolation = AnimationInterpolation::LINEAR; + // The index of an accessor, containing keyframe output values. + [[=json::required]] int64_t output; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// A keyframe animation. +struct [[=object]] Animation { + // An array of animation channels. An animation channel combines an + // animation sampler with a target property being animated. Different + // channels of the same animation MUST NOT have the same targets. + [[=json::required]] std::vector channels; + // An array of animation samplers. An animation sampler combines timestamps + // with a sequence of output values and defines an interpolation algorithm. + [[=json::required]] std::vector samplers; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const AnimationPath &v) -> void; +auto from_json(const nlohmann::json &j, AnimationPath &v) -> void; +auto to_json(nlohmann::json &j, const AnimationInterpolation &v) -> void; +auto from_json(const nlohmann::json &j, AnimationInterpolation &v) -> void; +auto to_json(nlohmann::json &j, const AnimationChannelTarget &v) -> void; +auto from_json(const nlohmann::json &j, AnimationChannelTarget &v) -> void; +auto to_json(nlohmann::json &j, const AnimationChannel &v) -> void; +auto from_json(const nlohmann::json &j, AnimationChannel &v) -> void; +auto to_json(nlohmann::json &j, const AnimationSampler &v) -> void; +auto from_json(const nlohmann::json &j, AnimationSampler &v) -> void; +auto to_json(nlohmann::json &j, const Animation &v) -> void; +auto from_json(const nlohmann::json &j, Animation &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Asset.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Asset.hpp new file mode 100644 index 0000000..e102736 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Asset.hpp @@ -0,0 +1,28 @@ +#pragma once +#include "Extension.hpp" +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// Metadata about the glTF asset. +struct [[=object]] Asset { + // A copyright message suitable for display to credit the content creator. + std::optional copyright; + // Tool that generated this glTF model. Useful for debugging. + std::optional generator; + // The glTF version in the form of . that this asset targets. + [[=json::required]] std::string version; + // The minimum glTF version in the form of . that this asset + // targets. This property MUST NOT be greater than the asset version. + std::optional minVersion; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Asset &v) -> void; +auto from_json(const nlohmann::json &j, Asset &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Buffer.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Buffer.hpp new file mode 100644 index 0000000..5e79167 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Buffer.hpp @@ -0,0 +1,26 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// A buffer points to binary geometry, animation, or skins. +struct [[=object]] Buffer { + // The URI (or IRI) of the buffer. + std::optional uri; + // The length of the buffer in bytes. + [[=json::required]] int64_t byteLength; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Buffer &v) -> void; +auto from_json(const nlohmann::json &j, Buffer &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/BufferView.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/BufferView.hpp new file mode 100644 index 0000000..95d03e7 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/BufferView.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The hint representing the intended GPU buffer type to use with this buffer +// view. +enum class BufferViewTarget : int64_t { + ARRAY_BUFFER = 34962, + ELEMENT_ARRAY_BUFFER = 34963, +}; + +// A view into a buffer generally representing a subset of the buffer. +struct [[=object]] BufferView { + // The index of the buffer. + [[=json::required]] int64_t buffer; + // The offset into the buffer in bytes. + [[=json::skip_default]] int64_t byteOffset = 0; + // The length of the bufferView in bytes. + [[=json::required]] int64_t byteLength; + // The stride, in bytes. + std::optional byteStride; + // The hint representing the intended GPU buffer type to use with this + // buffer view. + std::optional target; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const BufferView &v) -> void; +auto from_json(const nlohmann::json &j, BufferView &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Camera.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Camera.hpp new file mode 100644 index 0000000..3b755a7 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Camera.hpp @@ -0,0 +1,82 @@ +#pragma once +#include "Extension.hpp" +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// An orthographic camera containing properties to create an orthographic +// projection matrix. +struct [[=object]] CameraOrthographic { + // The floating-point horizontal magnification of the view. This value MUST + // NOT be equal to zero. This value SHOULD NOT be negative. + [[=json::required]] double xmag; + // The floating-point vertical magnification of the view. This value MUST + // NOT be equal to zero. This value SHOULD NOT be negative. + [[=json::required]] double ymag; + // The floating-point distance to the far clipping plane. This value MUST + // NOT be equal to zero. zfar MUST be greater than znear. + [[=json::required]] double zfar; + // The floating-point distance to the near clipping plane. + [[=json::required]] double znear; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// A perspective camera containing properties to create a perspective projection +// matrix. +struct [[=object]] CameraPerspective { + // The floating-point aspect ratio of the field of view. + std::optional aspectRatio; + // The floating-point vertical field of view in radians. This value SHOULD + // be less than π. + [[=json::required]] double yfov; + // The floating-point distance to the far clipping plane. + std::optional zfar; + // The floating-point distance to the near clipping plane. + [[=json::required]] double znear; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// Specifies if the camera uses a perspective or orthographic projection. +enum class CameraType { + perspective, + orthographic, +}; + +// A camera’s projection. A node MAY reference a camera to apply a transform to +// place the camera in the scene. +struct [[=object]] Camera { + // An orthographic camera containing properties to create an orthographic + // projection matrix. This property MUST NOT be defined when perspective is + // defined. + [[=json::manual_write]] std::optional orthographic; + // A perspective camera containing properties to create a perspective + // projection matrix. This property MUST NOT be defined when orthographic is + // defined. + [[=json::manual_write]] std::optional perspective; + // Specifies if the camera uses a perspective or orthographic projection. + [[=json::required]] CameraType type; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const CameraType &v) -> void; +auto from_json(const nlohmann::json &j, CameraType &v) -> void; +auto to_json(nlohmann::json &j, const CameraOrthographic &v) -> void; +auto from_json(const nlohmann::json &j, CameraOrthographic &v) -> void; +auto to_json(nlohmann::json &j, const CameraPerspective &v) -> void; +auto from_json(const nlohmann::json &j, CameraPerspective &v) -> void; +auto to_json(nlohmann::json &j, const Camera &v) -> void; +auto from_json(const nlohmann::json &j, Camera &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Extension.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Extension.hpp new file mode 100644 index 0000000..0462307 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Extension.hpp @@ -0,0 +1,20 @@ +#pragma once +// The glTF structs carry balsa::json annotations, which need C++26 +// reflection (GCC 16+ with -freflection); see balsa/json/annotations.hpp. +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// glTF objects: properties are optional unless marked Required, arrays have +// minItems 1, and an absent property means its default. So unannotated +// members are omitted when empty. +inline constexpr auto object = json::members(json::omit_empty); + +// JSON object with extension-specific objects. +using Extension = json::RawJson; +// Application-specific data. +using Extras = json::RawJson; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Image.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Image.hpp new file mode 100644 index 0000000..4f2b741 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Image.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// Image data used to create a texture. Image MAY be referenced by an URI (or +// IRI) or a buffer view index. +struct [[=object]] Image { + // The URI (or IRI) of the image. + std::optional uri; + // The image’s media type. This field MUST be defined when bufferView is + // defined. + std::optional mimeType; + // The index of the bufferView that contains the image. This field MUST NOT + // be defined when uri is defined. + std::optional bufferView; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Image &v) -> void; +auto from_json(const nlohmann::json &j, Image &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Material.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Material.hpp new file mode 100644 index 0000000..96d0f57 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Material.hpp @@ -0,0 +1,120 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// Reference to a texture. +struct [[=object]] TextureInfo { + // The index of the texture. + [[=json::required]] int64_t index; + // The set index of texture’s TEXCOORD attribute used for texture coordinate + // mapping. + [[=json::skip_default]] int64_t texCoord = 0; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// Reference to a texture. +struct [[=object]] MaterialNormalTextureInfo { + // The index of the texture. + [[=json::required]] int64_t index; + // The set index of texture’s TEXCOORD attribute used for texture coordinate + // mapping. + [[=json::skip_default]] int64_t texCoord = 0; + // The scalar parameter applied to each normal vector of the normal texture. + [[=json::skip_default]] double scale = 1.0; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// Reference to a texture. +struct [[=object]] MaterialOcclusionTextureInfo { + // The index of the texture. + [[=json::required]] int64_t index; + // The set index of texture’s TEXCOORD attribute used for texture coordinate + // mapping. + [[=json::skip_default]] int64_t texCoord = 0; + // A scalar multiplier controlling the amount of occlusion applied. + [[=json::skip_default]] double strength = 1.0; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// A set of parameter values that are used to define the metallic-roughness +// material model from Physically-Based Rendering (PBR) methodology. +struct [[=object]] MaterialPBRMetallicRoughness { + // The factors for the base color of the material. + [[=json::skip_default]] std::array baseColorFactor = {1.0, 1.0, 1.0, 1.0}; + // The base color texture. + std::optional baseColorTexture; + // The factor for the metalness of the material. + [[=json::skip_default]] double metallicFactor = 1.0; + // The factor for the roughness of the material. + [[=json::skip_default]] double roughnessFactor = 1.0; + // The metallic-roughness texture. + std::optional metallicRoughnessTexture; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// The alpha rendering mode of the material. +enum class AlphaMode { + OPAQUE, + MASK, + BLEND, +}; + +// The material appearance of a primitive. +struct [[=object]] Material { + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; + // A set of parameter values that are used to define the metallic-roughness + // material model from Physically Based Rendering (PBR) methodology. When + // undefined, all the default values of pbrMetallicRoughness MUST apply. + std::optional pbrMetallicRoughness; + // The tangent space normal texture. + std::optional normalTexture; + // The occlusion texture. + std::optional occlusionTexture; + // The emissive texture. + std::optional emissiveTexture; + // The factors for the emissive color of the material. + [[=json::skip_default]] std::array emissiveFactor = {0.0, 0.0, 0.0}; + // The alpha rendering mode of the material. + [[=json::skip_default]] AlphaMode alphaMode = AlphaMode::OPAQUE; + // The alpha cutoff value of the material. + [[=json::manual_write]] double alphaCutoff = 0.5; + // Specifies whether the material is double sided. + [[=json::skip_default]] bool doubleSided = false; +}; + +auto to_json(nlohmann::json &j, const AlphaMode &v) -> void; +auto from_json(const nlohmann::json &j, AlphaMode &v) -> void; +auto to_json(nlohmann::json &j, const TextureInfo &v) -> void; +auto from_json(const nlohmann::json &j, TextureInfo &v) -> void; +auto to_json(nlohmann::json &j, const MaterialNormalTextureInfo &v) -> void; +auto from_json(const nlohmann::json &j, MaterialNormalTextureInfo &v) -> void; +auto to_json(nlohmann::json &j, const MaterialOcclusionTextureInfo &v) -> void; +auto from_json(const nlohmann::json &j, MaterialOcclusionTextureInfo &v) -> void; +auto to_json(nlohmann::json &j, const MaterialPBRMetallicRoughness &v) -> void; +auto from_json(const nlohmann::json &j, MaterialPBRMetallicRoughness &v) -> void; +auto to_json(nlohmann::json &j, const Material &v) -> void; +auto from_json(const nlohmann::json &j, Material &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Mesh.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Mesh.hpp new file mode 100644 index 0000000..d63734f --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Mesh.hpp @@ -0,0 +1,63 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The topology type of primitives to render. +enum class PrimitiveMode : int64_t { + POINTS = 0, + LINES = 1, + LINE_LOOP = 2, + LINE_STRIP = 3, + TRIANGLES = 4, + TRIANGLE_STRIP = 5, + TRIANGLE_FAN = 6, +}; + +// Geometry to be rendered with the given material. +struct [[=object]] MeshPrimitive { + // A plain JSON object, where each key corresponds to a mesh attribute + // semantic and each value is the index of the accessor containing + // attribute’s data. + [[=json::required]] std::map attributes; + // The index of the accessor that contains the vertex indices. + std::optional indices; + // The index of the material to apply to this primitive when rendering. + std::optional material; + // The topology type of primitives to render. + [[=json::skip_default]] PrimitiveMode mode = PrimitiveMode::TRIANGLES; + // An array of morph targets. + std::vector> targets; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +// A set of primitives to be rendered. Its global transform is defined by a node +// that references it. +struct [[=object]] Mesh { + // An array of primitives, each defining geometry to be rendered. + [[=json::required]] std::vector primitives; + // Array of weights to be applied to the morph targets. The number of array + // elements MUST match the number of morph targets. + std::vector weights; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const MeshPrimitive &v) -> void; +auto from_json(const nlohmann::json &j, MeshPrimitive &v) -> void; +auto to_json(nlohmann::json &j, const Mesh &v) -> void; +auto from_json(const nlohmann::json &j, Mesh &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Node.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Node.hpp new file mode 100644 index 0000000..ff715a2 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Node.hpp @@ -0,0 +1,58 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// A node in the node hierarchy. When the node contains skin, all +// mesh.primitives MUST contain JOINTS_0 and WEIGHTS_0 attributes. A node MAY +// have either a matrix or any combination of translation/rotation/scale (TRS) +// properties. TRS properties are converted to matrices and postmultiplied in +// the T * R * S order to compose the transformation matrix; first the scale is +// applied to the vertices, then the rotation, and then the translation. If none +// are provided, the transform is the identity. When a node is targeted for +// animation (referenced by an animation.channel.target), matrix MUST NOT be +// present. +struct [[=object]] Node { + // The index of the camera referenced by this node. + std::optional camera; + // The indices of this node’s children. + std::vector children; + // The index of the skin referenced by this node. + std::optional skin; + // A floating-point 4x4 transformation matrix stored in column-major + // order. + [[=json::manual_write]] std::array matrix = {1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 0.0, 0.0, 1.0}; + // The index of the mesh in this node. + std::optional mesh; + // The node’s unit quaternion rotation in the order (x, y, z, w), where w is + // the scalar. + [[=json::manual_write]] std::array rotation = {0.0, 0.0, 0.0, 1.0}; + // The node’s non-uniform scale, given as the scaling factors along the x, + // y, and z axes. + [[=json::manual_write]] std::array scale = {1.0, 1.0, 1.0}; + // The node’s translation along the x, y, and z axes. + [[=json::manual_write]] std::array translation = {0.0, 0.0, 0.0}; + // The weights of the instantiated morph target. The number of array + // elements MUST match the number of morph targets of the referenced mesh. + // When defined, mesh MUST also be defined. + std::vector weights; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Node &v) -> void; +auto from_json(const nlohmann::json &j, Node &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Sampler.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Sampler.hpp new file mode 100644 index 0000000..a777f4b --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Sampler.hpp @@ -0,0 +1,53 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// Magnification filter. +enum class MagFilter : int64_t { + NEAREST = 9728, + LINEAR = 9729, +}; + +// Minification filter. +enum class MinFilter : int64_t { + NEAREST = 9728, + LINEAR = 9729, + NEAREST_MIPMAP_NEAREST = 9984, + LINEAR_MIPMAP_NEAREST = 9985, + NEAREST_MIPMAP_LINEAR = 9986, + LINEAR_MIPMAP_LINEAR = 9987, +}; + +// Texture coordinate wrapping mode. +enum class WrapMode : int64_t { + CLAMP_TO_EDGE = 33071, + MIRRORED_REPEAT = 33648, + REPEAT = 10497, +}; + +// Texture sampler properties for filtering and wrapping modes. +struct [[=object]] Sampler { + // Magnification filter. + std::optional magFilter; + // Minification filter. + std::optional minFilter; + // S (U) wrapping mode. + [[=json::skip_default]] WrapMode wrapS = WrapMode::REPEAT; + // T (V) wrapping mode. + [[=json::skip_default]] WrapMode wrapT = WrapMode::REPEAT; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Sampler &v) -> void; +auto from_json(const nlohmann::json &j, Sampler &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Scene.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Scene.hpp new file mode 100644 index 0000000..be2ca35 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Scene.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The root nodes of a scene. +struct [[=object]] Scene { + // The indices of each root node. + std::vector nodes; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Scene &v) -> void; +auto from_json(const nlohmann::json &j, Scene &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Skin.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Skin.hpp new file mode 100644 index 0000000..6296506 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Skin.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// Joints and matrices defining a skin. +struct [[=object]] Skin { + // The index of the accessor containing the floating-point 4x4 inverse-bind + // matrices. + std::optional inverseBindMatrices; + // The index of the node used as a skeleton root. + std::optional skeleton; + // Indices of skeleton nodes, used as joints in this skin. + [[=json::required]] std::vector joints; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Skin &v) -> void; +auto from_json(const nlohmann::json &j, Skin &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/Texture.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/Texture.hpp new file mode 100644 index 0000000..08cfe62 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/Texture.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "Extension.hpp" +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// A texture and its sampler. +struct [[=object]] Texture { + // The index of the sampler used by this texture. When undefined, a sampler + // with repeat wrapping and auto filtering SHOULD be used. + std::optional sampler; + // The index of the image used by this texture. When undefined, an extension + // or other mechanism SHOULD supply an alternate texture source, otherwise + // behavior is undefined. + std::optional source; + // The user-defined name of this object. + std::optional name; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const Texture &v) -> void; +auto from_json(const nlohmann::json &j, Texture &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/include/balsa/visualization/scene_graph/gltf/glTF.hpp b/visualization/include/balsa/visualization/scene_graph/gltf/glTF.hpp new file mode 100644 index 0000000..3bfb558 --- /dev/null +++ b/visualization/include/balsa/visualization/scene_graph/gltf/glTF.hpp @@ -0,0 +1,69 @@ +#pragma once +#include "Accessor.hpp" +#include "Animation.hpp" +#include "Asset.hpp" +#include "Buffer.hpp" +#include "BufferView.hpp" +#include "Camera.hpp" +#include "Extension.hpp" +#include "Image.hpp" +#include "Material.hpp" +#include "Mesh.hpp" +#include "Node.hpp" +#include "Sampler.hpp" +#include "Scene.hpp" +#include "Skin.hpp" +#include "Texture.hpp" +#include +#include +#include +#include + +namespace balsa::visualization::scene_graph::gltf { + +// The root object for a glTF asset. +struct [[=object]] glTF { + // Names of glTF extensions used in this asset. + std::vector extensionsUsed; + // Names of glTF extensions required to properly load this asset. + std::vector extensionsRequired; + // An array of accessors. + std::vector accessors; + // An array of keyframe animations. + std::vector animations; + // Metadata about the glTF asset. + [[=json::required]] Asset asset; + // An array of buffers. + std::vector buffers; + // An array of bufferViews. + std::vector bufferViews; + // An array of cameras. + std::vector cameras; + // An array of images. + std::vector images; + // An array of materials. + std::vector materials; + // An array of meshes. + std::vector meshes; + // An array of nodes. + std::vector nodes; + // An array of samplers. + std::vector samplers; + // The index of the default scene. + std::optional scene; + // An array of scenes. + std::vector scenes; + // An array of skins. + std::vector skins; + // An array of textures. + std::vector textures; + // JSON object with extension-specific objects. + Extension extensions; + // Application-specific data. + Extras extras; +}; + +auto to_json(nlohmann::json &j, const glTF &v) -> void; +auto from_json(const nlohmann::json &j, glTF &v) -> void; + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/meson.build b/visualization/meson.build index c4fcda9..a81b54e 100644 --- a/visualization/meson.build +++ b/visualization/meson.build @@ -10,6 +10,11 @@ if get_option('imgui') and not get_option('glfw') error('The ImGui frontend currently requires GLFW; enable -Dglfw=true or disable -Dimgui') endif +if get_option('draco') + draco_dep = dependency('draco') + geometry_required_deps += draco_dep +endif + # Shader source files are part of the dependency contract, so a binary system # package cannot satisfy it. Always use the pinned source subproject. colormap_proj = subproject('colormap_shaders') @@ -117,6 +122,32 @@ visualization_sources = [ 'src/normalize_mesh.cpp', ] +# ── glTF ────────────────────────────────────────────────────────────────── +# glTF uses balsa's reflection-based JSON, so it is built whenever -Djson=true +# (core checks for reflection and propagates -freflection). +# BALSA_VISUALIZATION_GLTF marks support. +visualization_public_args = [] +if get_option('json') + visualization_public_args += ['-DBALSA_VISUALIZATION_GLTF'] + visualization_sources += [ + 'src/scene_graph/gltf/Accessor.cpp', + 'src/scene_graph/gltf/Animation.cpp', + 'src/scene_graph/gltf/Asset.cpp', + 'src/scene_graph/gltf/Buffer.cpp', + 'src/scene_graph/gltf/BufferView.cpp', + 'src/scene_graph/gltf/Camera.cpp', + 'src/scene_graph/gltf/glTF.cpp', + 'src/scene_graph/gltf/Image.cpp', + 'src/scene_graph/gltf/Material.cpp', + 'src/scene_graph/gltf/Mesh.cpp', + 'src/scene_graph/gltf/Node.cpp', + 'src/scene_graph/gltf/Sampler.cpp', + 'src/scene_graph/gltf/Scene.cpp', + 'src/scene_graph/gltf/Skin.cpp', + 'src/scene_graph/gltf/Texture.cpp', + ] +endif + if get_option('qt') visualization_public_deps += qt_dep visualization_qt_sources = [ @@ -172,7 +203,7 @@ visualization_lib = library('balsaVisualization', visualization_sources, cpp_args: embed_args) # For consumers: propagate public deps (including imgui shared library). -visualization_dep = declare_dependency(link_with: visualization_lib, dependencies: visualization_public_deps, include_directories: include_dir) +visualization_dep = declare_dependency(link_with: visualization_lib, dependencies: visualization_public_deps, include_directories: include_dir, compile_args: visualization_public_args) meson.override_dependency('balsaVisualization', visualization_dep) if get_option('testing') diff --git a/visualization/src/scene_graph/gltf/Accessor.cpp b/visualization/src/scene_graph/gltf/Accessor.cpp new file mode 100644 index 0000000..b6d2599 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Accessor.cpp @@ -0,0 +1,45 @@ +#include "balsa/visualization/scene_graph/gltf/Accessor.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const AccessorType &v) -> void { + json::enum_to_json(j, v); +} +auto from_json(const nlohmann::json &j, AccessorType &v) -> void { + json::enum_from_json(j, v); +} + +auto to_json(nlohmann::json &j, const AccessorSparseIndices &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AccessorSparseIndices &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const AccessorSparseValues &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AccessorSparseValues &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const AccessorSparse &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AccessorSparse &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const Accessor &v) -> void { + json::to_json_members(j, v); + // byteOffset MUST NOT be defined when bufferView is undefined. + if (v.bufferView) { + json::write_members(j, v); + } +} +auto from_json(const nlohmann::json &j, Accessor &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Animation.cpp b/visualization/src/scene_graph/gltf/Animation.cpp new file mode 100644 index 0000000..1b7d6c6 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Animation.cpp @@ -0,0 +1,48 @@ +#include "balsa/visualization/scene_graph/gltf/Animation.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const AnimationPath &v) -> void { + json::enum_to_json(j, v); +} +auto from_json(const nlohmann::json &j, AnimationPath &v) -> void { + json::enum_from_json(j, v); +} + +auto to_json(nlohmann::json &j, const AnimationInterpolation &v) -> void { + json::enum_to_json(j, v); +} +auto from_json(const nlohmann::json &j, AnimationInterpolation &v) -> void { + json::enum_from_json(j, v); +} + +auto to_json(nlohmann::json &j, const AnimationChannelTarget &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AnimationChannelTarget &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const AnimationChannel &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AnimationChannel &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const AnimationSampler &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, AnimationSampler &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const Animation &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Animation &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Asset.cpp b/visualization/src/scene_graph/gltf/Asset.cpp new file mode 100644 index 0000000..bd21498 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Asset.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Asset.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Asset &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Asset &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Buffer.cpp b/visualization/src/scene_graph/gltf/Buffer.cpp new file mode 100644 index 0000000..f8bf2fd --- /dev/null +++ b/visualization/src/scene_graph/gltf/Buffer.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Buffer.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Buffer &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Buffer &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/BufferView.cpp b/visualization/src/scene_graph/gltf/BufferView.cpp new file mode 100644 index 0000000..f136c96 --- /dev/null +++ b/visualization/src/scene_graph/gltf/BufferView.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/BufferView.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const BufferView &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, BufferView &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Camera.cpp b/visualization/src/scene_graph/gltf/Camera.cpp new file mode 100644 index 0000000..25fe215 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Camera.cpp @@ -0,0 +1,44 @@ +#include "balsa/visualization/scene_graph/gltf/Camera.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const CameraType &v) -> void { + json::enum_to_json(j, v); +} +auto from_json(const nlohmann::json &j, CameraType &v) -> void { + json::enum_from_json(j, v); +} + +auto to_json(nlohmann::json &j, const CameraOrthographic &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, CameraOrthographic &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const CameraPerspective &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, CameraPerspective &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const Camera &v) -> void { + json::to_json_members(j, v); + // perspective and orthographic MUST NOT both be defined; write the one + // selected by type. + switch (v.type) { + case CameraType::perspective: + json::write_members(j, v); + break; + case CameraType::orthographic: + json::write_members(j, v); + break; + } +} +auto from_json(const nlohmann::json &j, Camera &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Image.cpp b/visualization/src/scene_graph/gltf/Image.cpp new file mode 100644 index 0000000..bb47ae8 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Image.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Image.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Image &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Image &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Material.cpp b/visualization/src/scene_graph/gltf/Material.cpp new file mode 100644 index 0000000..bad0e78 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Material.cpp @@ -0,0 +1,53 @@ +#include "balsa/visualization/scene_graph/gltf/Material.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const AlphaMode &v) -> void { + json::enum_to_json(j, v); +} +auto from_json(const nlohmann::json &j, AlphaMode &v) -> void { + json::enum_from_json(j, v); +} + +auto to_json(nlohmann::json &j, const TextureInfo &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, TextureInfo &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const MaterialNormalTextureInfo &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, MaterialNormalTextureInfo &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const MaterialOcclusionTextureInfo &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, MaterialOcclusionTextureInfo &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const MaterialPBRMetallicRoughness &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, MaterialPBRMetallicRoughness &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const Material &v) -> void { + json::to_json_members(j, v); + // alphaCutoff MUST NOT be defined unless alphaMode is; it is only + // meaningful (and only written) in MASK mode. + if (v.alphaMode == AlphaMode::MASK) { + json::write_members(j, v); + } +} +auto from_json(const nlohmann::json &j, Material &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Mesh.cpp b/visualization/src/scene_graph/gltf/Mesh.cpp new file mode 100644 index 0000000..bf23457 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Mesh.cpp @@ -0,0 +1,20 @@ +#include "balsa/visualization/scene_graph/gltf/Mesh.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const MeshPrimitive &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, MeshPrimitive &v) -> void { + json::from_json_members(j, v); +} + +auto to_json(nlohmann::json &j, const Mesh &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Mesh &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Node.cpp b/visualization/src/scene_graph/gltf/Node.cpp new file mode 100644 index 0000000..3a51377 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Node.cpp @@ -0,0 +1,22 @@ +#include "balsa/visualization/scene_graph/gltf/Node.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Node &v) -> void { + json::to_json_members(j, v); + // matrix MUST NOT be defined alongside translation/rotation/scale. A + // non-identity matrix takes precedence; otherwise write the TRS. + static const Node default_node{}; + if (v.matrix != default_node.matrix) { + json::write_members<^^Node::matrix>(j, v); + } else { + json::write_members(j, v); + } +} +auto from_json(const nlohmann::json &j, Node &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Sampler.cpp b/visualization/src/scene_graph/gltf/Sampler.cpp new file mode 100644 index 0000000..22e443b --- /dev/null +++ b/visualization/src/scene_graph/gltf/Sampler.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Sampler.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Sampler &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Sampler &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Scene.cpp b/visualization/src/scene_graph/gltf/Scene.cpp new file mode 100644 index 0000000..c750906 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Scene.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Scene.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Scene &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Scene &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Skin.cpp b/visualization/src/scene_graph/gltf/Skin.cpp new file mode 100644 index 0000000..b91036e --- /dev/null +++ b/visualization/src/scene_graph/gltf/Skin.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Skin.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Skin &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Skin &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/Texture.cpp b/visualization/src/scene_graph/gltf/Texture.cpp new file mode 100644 index 0000000..4fad4c3 --- /dev/null +++ b/visualization/src/scene_graph/gltf/Texture.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/Texture.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const Texture &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, Texture &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf diff --git a/visualization/src/scene_graph/gltf/glTF.cpp b/visualization/src/scene_graph/gltf/glTF.cpp new file mode 100644 index 0000000..8b9eec9 --- /dev/null +++ b/visualization/src/scene_graph/gltf/glTF.cpp @@ -0,0 +1,13 @@ +#include "balsa/visualization/scene_graph/gltf/glTF.hpp" +#include + +namespace balsa::visualization::scene_graph::gltf { + +auto to_json(nlohmann::json &j, const glTF &v) -> void { + json::to_json_members(j, v); +} +auto from_json(const nlohmann::json &j, glTF &v) -> void { + json::from_json_members(j, v); +} + +} // namespace balsa::visualization::scene_graph::gltf