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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 48 additions & 0 deletions core/include/balsa/json/annotations.hpp
Original file line number Diff line number Diff line change
@@ -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 <version>
#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<int> 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
17 changes: 17 additions & 0 deletions core/include/balsa/json/raw_json.hpp
Original file line number Diff line number Diff line change
@@ -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 <string>

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
194 changes: 194 additions & 0 deletions core/include/balsa/json/reflection.hpp
Original file line number Diff line number Diff line change
@@ -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 <version>
#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 <nlohmann/json.hpp>

#include <meta>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>

// C++26 makes std::optional a range, which sends nlohmann's built-in
// optional support down its container path. Handle it explicitly.
template <typename T>
struct nlohmann::adl_serializer<std::optional<T>> {
static void to_json(json &j, const std::optional<T> &opt) {
if (opt) {
j = *opt;
} else {
j = nullptr;
}
}
static void from_json(const json &j, std::optional<T> &opt) {
if (j.is_null()) {
opt.reset();
} else {
opt = j.get<T>();
}
}
};

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 <typename T>
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 <emptiable T>
constexpr auto is_empty(const T &value) -> bool {
if constexpr (requires { value.has_value(); }) {
return !value.has_value();
} else {
return value.empty();
}
}

template <typename T, typename U = T>
requires std::is_convertible_v<U, T>
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 <typename A>
consteval auto annotation(std::meta::info entity) -> std::optional<A> {
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<A>(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<policy>(member)) { return p->kind; }
if (auto m = annotation<members_t>(type)) {
return m->default_policy.kind;
}
return policy_kind::required;
}

// Write member `m` of `v` under policy `kind`.
template <policy_kind kind, std::meta::info m, typename T>
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<typename[:std::meta::type_of(m):]>,
"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 <typename T>
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<detail::policy_of(^^T, m), m>(j, v);
}
}

// Write only the given members, e.g. from code handling json::manual_write
// members: `json::write_members<json::skip_default, ^^Node::scale>(j, v);`.
// Without a policy the members are always written.
template <policy P, std::meta::info... Members, typename T>
requires(P.kind != policy_kind::manual_write)
auto write_members(nlohmann::json &j, const T &v) -> void {
(detail::write_member<P.kind, Members>(j, v), ...);
}
template <std::meta::info... Members, typename T>
auto write_members(nlohmann::json &j, const T &v) -> void {
write_members<required, Members...>(j, v);
}

template <typename T>
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 <typename E>
requires std::is_enum_v<E>
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 <typename E>
requires std::is_enum_v<E>
auto enum_from_json(const nlohmann::json &j, E &e) -> void {
const auto &name = j.get_ref<const std::string &>();
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
20 changes: 19 additions & 1 deletion core/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>
#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.
Expand Down
2 changes: 1 addition & 1 deletion core/tests/eigen/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion core/tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 0 additions & 13 deletions geometry/include/balsa/geometry/bounding_box.hpp
Original file line number Diff line number Diff line change
@@ -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 <Eigen/Geometry>
#include <zipper/Matrix.hpp>

namespace balsa::geometry {

template <eigen::concepts::ColVecsDCompatible VType>
auto bounding_box(const VType &V) {
using BBox =
Eigen::AlignedBox<typename VType::Scalar, VType::RowsAtCompileTime>;
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;
Expand Down
Loading
Loading