Files

866 lines
25 KiB
C++

#pragma once
#include "PropertyTypeTraits.h"
#include <CesiumUtility/JsonValue.h>
#include <glm/common.hpp>
#include <cerrno>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#ifndef GLM_ENABLE_EXPERIMENTAL
// If we define GLM_ENABLE_EXPERIMENTAL here, we undefine it at the end of this
// header file.
#define GLM_ENABLE_EXPERIMENTAL
#define GLM_ENABLE_EXPERIMENTAL_defined_locally
#endif
#include <glm/gtx/string_cast.hpp>
namespace CesiumGltf {
/**
* @brief Default conversion between two types. No actual conversion is defined.
* This returns std::nullopt to indicate the conversion was not successful.
*/
template <typename TTo, typename TFrom, typename Enable = void>
struct MetadataConversions {
/**
* @brief Converts between `TFrom` and `TTo` where no actual conversion is
* defined, returning `std::nullopt`.
*/
static std::optional<TTo> convert(TFrom /*from*/) { return std::nullopt; }
};
/**
* @brief Trivially converts any type to itself.
*/
template <typename T> struct MetadataConversions<T, T> {
/**
* @brief Converts an instance of `T` to an instance of `T`, always returning
* the same value that was passed in.
*/
static std::optional<T> convert(T from) { return from; }
};
#pragma region Conversions to boolean
/**
* @brief Converts from a scalar to a bool.
*/
template <typename TFrom>
struct MetadataConversions<
bool,
TFrom,
std::enable_if_t<CesiumGltf::IsMetadataScalar<TFrom>::value>> {
/**
* @brief Converts a scalar to a boolean. Zero is converted to false, while
* nonzero values are converted to true.
*
* @param from The scalar to convert from.
*/
static std::optional<bool> convert(TFrom from) {
return from != static_cast<TFrom>(0);
}
};
/**
* @brief Converts from std::string_view to a bool.
*/
template <> struct MetadataConversions<bool, std::string_view> {
private:
static bool
isEqualCaseInsensitive(const std::string_view& a, const std::string_view& b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); i++) {
if (std::tolower(a[i]) != std::tolower(b[i])) {
return false;
}
}
return true;
}
public:
/**
* @brief Converts the contents of a std::string_view to a boolean.
*
* "0", "false", and "no" (case-insensitive) are converted to false, while
* "1", "true", and "yes" are converted to true. All other strings will return
* std::nullopt.
*
* @param from The std::string_view to convert from.
*/
static std::optional<bool> convert(const std::string_view& from) {
if (isEqualCaseInsensitive(from, "1") ||
isEqualCaseInsensitive(from, "true") ||
isEqualCaseInsensitive(from, "yes")) {
return true;
}
if (isEqualCaseInsensitive(from, "0") ||
isEqualCaseInsensitive(from, "false") ||
isEqualCaseInsensitive(from, "no")) {
return false;
}
return std::nullopt;
}
};
/**
* @brief Converts from std::string to a bool.
*/
template <> struct MetadataConversions<bool, std::string> {
public:
/**
* @brief Converts the contents of a std::string to a boolean.
*
* "0", "false", and "no" (case-insensitive) are converted to false, while
* "1", "true", and "yes" are converted to true. All other strings will return
* std::nullopt.
*
* @param from The std::string to convert from.
*/
static std::optional<bool> convert(const std::string& from) {
return MetadataConversions<bool, std::string_view>::convert(
std::string_view(from.data(), from.size()));
}
};
#pragma endregion
#pragma region Conversions to integer
/**
* @brief Converts from one integer type to another.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataInteger<TTo>::value &&
CesiumGltf::IsMetadataInteger<TFrom>::value &&
!std::is_same_v<TTo, TFrom>>> {
/**
* @brief Converts a value of the given integer to another integer type. If
* the integer cannot be losslessly converted to the desired type,
* std::nullopt is returned.
*
* @param from The integer value to convert from.
*/
static std::optional<TTo> convert(TFrom from) {
return CesiumUtility::losslessNarrow<TTo, TFrom>(from);
}
};
/**
* @brief Converts from a floating-point type to an integer.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataInteger<TTo>::value &&
CesiumGltf::IsMetadataFloating<TFrom>::value>> {
/**
* @brief Converts a floating-point value to an integer type. This truncates
* the floating-point value, rounding it towards zero.
*
* If the value is outside the range of the integer type, std::nullopt is
* returned.
*
* @param from The floating-point value to convert from.
*/
static std::optional<TTo> convert(TFrom from) {
if (double(std::numeric_limits<TTo>::max()) < from ||
double(std::numeric_limits<TTo>::lowest()) > from) {
// Floating-point number is outside the range of this integer type.
return std::nullopt;
}
return static_cast<TTo>(from);
}
};
/**
* @brief Converts from std::string to a signed integer.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
std::string,
std::enable_if_t<
CesiumGltf::IsMetadataInteger<TTo>::value && std::is_signed_v<TTo>>> {
/**
* @brief Converts the contents of a std::string to a signed integer.
* This assumes that the entire std::string represents the number, not
* just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string to parse from.
*/
static std::optional<TTo> convert(const std::string& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
errno = 0;
char* pLastUsed;
int64_t parsedValue = std::strtoll(from.c_str(), &pLastUsed, 10);
if (errno == 0 && pLastUsed == from.c_str() + from.size()) {
// Successfully parsed the entire string as an integer of this type.
return CesiumUtility::losslessNarrow<TTo, int64_t>(parsedValue);
}
errno = 0;
// Failed to parse as an integer. Maybe we can parse as a double and
// truncate it?
double parsedDouble = std::strtod(from.c_str(), &pLastUsed);
if (errno == 0 && pLastUsed == from.c_str() + from.size()) {
// Successfully parsed the entire string as a double.
// Convert it to an integer if we can.
double truncated = glm::trunc(parsedDouble);
int64_t asInteger = static_cast<int64_t>(truncated);
double roundTrip = static_cast<double>(asInteger);
if (roundTrip == truncated) {
return CesiumUtility::losslessNarrow<TTo, int64_t>(asInteger);
}
}
return std::nullopt;
}
}; // namespace CesiumGltf
/**
* @brief Converts from std::string to an unsigned integer.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
std::string,
std::enable_if_t<
CesiumGltf::IsMetadataInteger<TTo>::value && !std::is_signed_v<TTo>>> {
/**
* @brief Converts the contents of a std::string to an unsigned integer.
* This assumes that the entire std::string represents the number, not
* just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string to parse from.
*/
static std::optional<TTo> convert(const std::string& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
if (from.find('-') != std::string::npos) {
// The string must be manually checked for a negative sign because for
// std::strtoull accepts negative numbers and bitcasts them, which is not
// desired!
return std::nullopt;
}
errno = 0;
char* pLastUsed;
uint64_t parsedValue = std::strtoull(from.c_str(), &pLastUsed, 10);
if (errno == 0 && pLastUsed == from.c_str() + from.size()) {
// Successfully parsed the entire string as an integer of this type.
return CesiumUtility::losslessNarrow<TTo, uint64_t>(parsedValue);
}
// Failed to parse as an integer. Maybe we can parse as a double and
// truncate it?
errno = 0;
double parsedDouble = std::strtod(from.c_str(), &pLastUsed);
if (errno == 0 && pLastUsed == from.c_str() + from.size()) {
// Successfully parsed the entire string as a double.
// Convert it to an integer if we can.
double truncated = glm::trunc(parsedDouble);
uint64_t asInteger = static_cast<uint64_t>(truncated);
double roundTrip = static_cast<double>(asInteger);
if (roundTrip == truncated) {
return CesiumUtility::losslessNarrow<TTo, uint64_t>(asInteger);
}
}
return std::nullopt;
}
};
/**
* @brief Converts from std::string_view to an integer.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
std::string_view,
std::enable_if_t<CesiumGltf::IsMetadataInteger<TTo>::value>> {
/**
* @brief Converts the contents of a std::string_view to an integer.
* This assumes that the entire std::string_view represents the number, not
* just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string_view to parse from.
*/
static std::optional<TTo> convert(const std::string_view& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
// Amazingly, C++ has no* string parsing functions that work with strings
// that might not be null-terminated. So we have to copy to a std::string
// (which _is_ guaranteed to be null terminated) before parsing.
// * except std::from_chars, but compiler/library support for the
// floating-point version of that method is spotty at best.
return MetadataConversions<TTo, std::string>::convert(std::string(from));
}
};
/**
* @brief Converts from a boolean to an integer type.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
bool,
std::enable_if_t<CesiumGltf::IsMetadataInteger<TTo>::value>> {
/**
* @brief Converts a boolean to an integer. This returns 1 for true, 0 for
* false.
*
* @param from The boolean value to be converted.
*/
static std::optional<TTo> convert(bool from) { return from ? 1 : 0; }
};
#pragma endregion
#pragma region Conversions to float
/**
* @brief Converts from a boolean to a float.
*/
template <> struct MetadataConversions<float, bool> {
/**
* @brief Converts a boolean to a float. This returns 1.0f for true, 0.0f for
* false.
*
* @param from The boolean value to be converted.
*/
static std::optional<float> convert(bool from) { return from ? 1.0f : 0.0f; }
};
/**
* @brief Converts from an integer type to a float.
*/
template <typename TFrom>
struct MetadataConversions<
float,
TFrom,
std::enable_if_t<CesiumGltf::IsMetadataInteger<TFrom>::value>> {
/**
* @brief Converts an integer to a float. The value may lose precision during
* conversion.
*
* @param from The integer value to be converted.
*/
static std::optional<float> convert(TFrom from) {
return static_cast<float>(from);
}
};
/**
* @brief Converts from a double to a float.
*/
template <> struct MetadataConversions<float, double> {
/**
* @brief Converts a double to a float. The value may lose precision during
* conversion.
*
* If the value is outside the range of a float, std::nullopt is returned.
*
* @param from The double value to be converted.
*/
static std::optional<float> convert(double from) {
if (from > std::numeric_limits<float>::max() ||
from < std::numeric_limits<float>::lowest()) {
return std::nullopt;
}
return static_cast<float>(from);
}
};
/**
* @brief Converts from a std::string to a float.
*/
template <> struct MetadataConversions<float, std::string> {
/**
* @brief Converts a std::string to a float. This assumes that the entire
* std::string represents the number, not just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string to parse from.
*/
static std::optional<float> convert(const std::string& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
errno = 0;
char* pLastUsed;
float parsedValue = std::strtof(from.c_str(), &pLastUsed);
if (errno == 0 && pLastUsed == from.c_str() + from.size() &&
!std::isinf(parsedValue)) {
// Successfully parsed the entire string as a float.
return parsedValue;
}
return std::nullopt;
}
};
/**
* @brief Converts from a std::string_view to a float.
*/
template <> struct MetadataConversions<float, std::string_view> {
/**
* @brief Converts a std::string_view to a float. This assumes that the entire
* std::string_view represents the number, not just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string_view to parse from.
*/
static std::optional<float> convert(const std::string_view& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
// Amazingly, C++ has no* string parsing functions that work with strings
// that might not be null-terminated. So we have to copy to a std::string
// (which _is_ guaranteed to be null terminated) before parsing.
// * except std::from_chars, but compiler/library support for the
// floating-point version of that method is spotty at best.
return MetadataConversions<float, std::string>::convert(
std::string(from.data(), from.size()));
}
};
#pragma endregion
#pragma region Conversions to double
/**
* @brief Converts from a boolean to a double.
*/
template <> struct MetadataConversions<double, bool> {
/**
* @brief Converts a boolean to a double. This returns 1.0 for true, 0.0 for
* false.
*
* @param from The boolean value to be converted.
*/
static std::optional<double> convert(bool from) { return from ? 1.0 : 0.0; }
};
/**
* @brief Converts from any integer type to a double.
*/
template <typename TFrom>
struct MetadataConversions<
double,
TFrom,
std::enable_if_t<CesiumGltf::IsMetadataInteger<TFrom>::value>> {
/**
* @brief Converts any integer type to a double. The value may lose precision
* during conversion.
*
* @param from The boolean value to be converted.
*/
static std::optional<double> convert(TFrom from) {
return static_cast<double>(from);
}
};
/**
* @brief Converts from a float to a double.
*/
template <> struct MetadataConversions<double, float> {
/**
* @brief Converts from a float to a double.
*
* @param from The float value to be converted.
*/
static std::optional<double> convert(float from) {
return static_cast<double>(from);
}
};
/**
* @brief Converts from std::string to a double.
*/
template <> struct MetadataConversions<double, std::string> {
/**
* Converts a std::string to a double. This assumes that the entire
* std::string represents the number, not just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string to parse from.
*/
static std::optional<double> convert(const std::string& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
errno = 0;
char* pLastUsed;
double parsedValue = std::strtod(from.c_str(), &pLastUsed);
if (errno == 0 && pLastUsed == from.c_str() + from.size() &&
!std::isinf(parsedValue)) {
// Successfully parsed the entire string as a double.
return parsedValue;
}
return std::nullopt;
}
};
/**
* @brief Converts from std::string_view to a double.
*/
template <> struct MetadataConversions<double, std::string_view> {
/**
* Converts a std::string_view to a double. This assumes that the entire
* std::string_view represents the number, not just a part of it.
*
* This returns std::nullopt if no number is parsed from the string.
*
* @param from The std::string_view to parse from.
*/
static std::optional<double> convert(const std::string_view& from) {
if (from.size() == 0) {
// Return early. Otherwise, empty strings will be parsed as 0, which is
// misleading.
return std::nullopt;
}
// Amazingly, C++ has no* string parsing functions that work with strings
// that might not be null-terminated. So we have to copy to a std::string
// (which _is_ guaranteed to be null terminated) before parsing.
// * except std::from_chars, but compiler/library support for the
// floating-point version of that method is spotty at best.
return MetadataConversions<double, std::string>::convert(std::string(from));
}
};
#pragma endregion
#pragma region Conversions to string
/**
* @brief Converts from a boolean to a string.
*/
template <> struct MetadataConversions<std::string, bool> {
/**
* @brief Converts a boolean to a std::string. Returns "true" for true and
* "false" for false.
*
* @param from The boolean to be converted.
*/
static std::optional<std::string> convert(bool from) {
return from ? "true" : "false";
}
};
/**
* @brief Converts from a scalar to a string.
*/
template <typename TFrom>
struct MetadataConversions<
std::string,
TFrom,
std::enable_if_t<IsMetadataScalar<TFrom>::value>> {
/**
* @brief Converts a scalar to a std::string.
*
* @param from The scalar to be converted.
*/
static std::optional<std::string> convert(TFrom from) {
return std::to_string(from);
}
};
/**
* @brief Converts from a glm::vecN or glm::matN to a string.
*/
template <typename TFrom>
struct MetadataConversions<
std::string,
TFrom,
std::enable_if_t<
IsMetadataVecN<TFrom>::value || IsMetadataMatN<TFrom>::value>> {
/**
* @brief Converts a glm::vecN or glm::matN to a std::string. This uses the
* format that glm::to_string() outputs for vecNs or matNs respectively.
*
* @param from The glm::vecN or glm::matN to be converted.
*/
static std::optional<std::string> convert(const TFrom& from) {
return glm::to_string(from);
}
};
/**
* @brief Converts from a std::string_view to a std::string.
*/
template <> struct MetadataConversions<std::string, std::string_view> {
/**
* @brief Converts from a std::string_view to a std::string.
*/
static std::optional<std::string> convert(const std::string_view& from) {
return std::string(from.data(), from.size());
}
};
#pragma endregion
#pragma region Conversions to glm::vecN
/**
* @brief Converts from a boolean to a vecN.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
bool,
std::enable_if_t<IsMetadataVecN<TTo>::value>> {
/**
* @brief Converts a boolean to a vecN. The boolean is converted to an integer
* value of 1 for true or 0 for false. The returned vector is initialized with
* this value in both of its components.
*
* @param from The boolean to be converted.
*/
static std::optional<TTo> convert(bool from) {
return from ? TTo(1) : TTo(0);
}
};
/**
* @brief Converts from a scalar type to a vecN.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataVecN<TTo>::value &&
CesiumGltf::IsMetadataScalar<TFrom>::value>> {
/**
* @brief Converts a scalar to a vecN. The returned vector is initialized
* with the value in all of its components. The value may lose precision
* during conversion depending on the type of the scalar and the component
* type of the matrix.
*
* If the scalar cannot be reasonably converted to the component type of the
* vecN, std::nullopt is returned.
*
* @param from The scalar value to be converted.
*/
static std::optional<TTo> convert(TFrom from) {
using ToValueType = typename TTo::value_type;
std::optional<ToValueType> maybeValue =
MetadataConversions<ToValueType, TFrom>::convert(from);
if (maybeValue) {
ToValueType value = *maybeValue;
return TTo(value);
}
return std::nullopt;
}
};
/**
* @brief Converts from a vecN type to another vecN type.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataVecN<TTo>::value &&
CesiumGltf::IsMetadataVecN<TFrom>::value &&
!std::is_same_v<TTo, TFrom>>> {
/**
* @brief Converts a value of the given vecN to another vecN type.
*
* If the given vector has more components than the target vecN type, then
* only its first N components will be used, where N is the dimension of the
* target vecN type. Otherwise, if the target vecN type has more components,
* its extra components will be initialized to zero.
*
* If any of the relevant components cannot be converted to the target vecN
* component type, std::nullopt is returned.
*
* @param from The vecN value to convert from.
*/
static std::optional<TTo> convert(TFrom from) {
TTo result = TTo(0);
constexpr glm::length_t validLength =
glm::min(TTo::length(), TFrom::length());
using ToValueType = typename TTo::value_type;
using FromValueType = typename TFrom::value_type;
for (glm::length_t i = 0; i < validLength; i++) {
auto maybeValue =
MetadataConversions<ToValueType, FromValueType>::convert(from[i]);
if (!maybeValue) {
return std::nullopt;
}
result[i] = *maybeValue;
}
return result;
}
};
#pragma endregion
#pragma region Conversions to glm::matN
/**
* @brief Converts from a boolean to a matN.
*/
template <typename TTo>
struct MetadataConversions<
TTo,
bool,
std::enable_if_t<IsMetadataMatN<TTo>::value>> {
/**
* @brief Converts a boolean to a matN. The boolean is converted to an integer
* value of 1 for true or 0 for false. The returned matrix is initialized with
* this value in all of its components.
*
* @param from The boolean to be converted.
*/
static std::optional<TTo> convert(bool from) {
return from ? TTo(1) : TTo(0);
}
};
/**
* Converts from a scalar type to a matN.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataMatN<TTo>::value &&
CesiumGltf::IsMetadataScalar<TFrom>::value>> {
/**
* Converts a scalar to a matN. The returned vector is initialized
* with the value in all components. The value may lose precision during
* conversion depending on the type of the scalar and the component type of
* the matrix.
*
* If the scalar cannot be reasonably converted to the component type of the
* matN, std::nullopt is returned.
*
* @param from The scalar value to be converted.
*/
static std::optional<TTo> convert(TFrom from) {
using ToValueType = typename TTo::value_type;
std::optional<ToValueType> maybeValue =
MetadataConversions<ToValueType, TFrom>::convert(from);
if (!maybeValue) {
return std::nullopt;
}
ToValueType value = *maybeValue;
return TTo(value);
}
};
/**
* @brief Converts from a matN type to another matN type.
*/
template <typename TTo, typename TFrom>
struct MetadataConversions<
TTo,
TFrom,
std::enable_if_t<
CesiumGltf::IsMetadataMatN<TTo>::value &&
CesiumGltf::IsMetadataMatN<TFrom>::value &&
!std::is_same_v<TTo, TFrom>>> {
/**
* @brief Converts a value of the given matN to another matN type.
*
* Let M be the length of the given matN, and N be the length of the target
* matN. If M > N, then only the first N components of the first N columns
* will be used. Otherwise, if M < N, all other elements in the N x N matrix
* will be initialized to zero.
*
* If any of the relevant components cannot be converted to the target matN
* component type, std::nullopt is returned.
*
* @param from The matN value to convert from.
*/
static std::optional<TTo> convert(TFrom from) {
TTo result = TTo(0);
constexpr glm::length_t validLength =
glm::min(TTo::length(), TFrom::length());
using ToValueType = typename TTo::value_type;
using FromValueType = typename TFrom::value_type;
for (glm::length_t c = 0; c < validLength; c++) {
for (glm::length_t r = 0; r < validLength; r++) {
auto maybeValue =
MetadataConversions<ToValueType, FromValueType>::convert(
from[c][r]);
if (!maybeValue) {
return std::nullopt;
}
result[c][r] = *maybeValue;
}
}
return result;
}
};
#pragma endregion
} // namespace CesiumGltf
#ifdef GLM_ENABLE_EXPERIMENTAL_defined_locally
#undef GLM_ENABLE_EXPERIMENTAL
#undef GLM_ENABLE_EXPERIMENTAL_defined_locally
#endif