2876 lines
87 KiB
C++
2876 lines
87 KiB
C++
#include "BatchTableToGltfStructuralMetadata.h"
|
|
#include "ConvertTileToGltf.h"
|
|
|
|
#include <Cesium3DTilesContent/GltfConverterResult.h>
|
|
#include <CesiumGltf/Accessor.h>
|
|
#include <CesiumGltf/Class.h>
|
|
#include <CesiumGltf/ClassProperty.h>
|
|
#include <CesiumGltf/ExtensionExtMeshFeatures.h>
|
|
#include <CesiumGltf/ExtensionKhrDracoMeshCompression.h>
|
|
#include <CesiumGltf/ExtensionModelExtStructuralMetadata.h>
|
|
#include <CesiumGltf/FeatureId.h>
|
|
#include <CesiumGltf/Mesh.h>
|
|
#include <CesiumGltf/MeshPrimitive.h>
|
|
#include <CesiumGltf/Model.h>
|
|
#include <CesiumGltf/PropertyArrayView.h>
|
|
#include <CesiumGltf/PropertyTable.h>
|
|
#include <CesiumGltf/PropertyTablePropertyView.h>
|
|
#include <CesiumGltf/PropertyTableView.h>
|
|
#include <CesiumGltf/Schema.h>
|
|
#include <CesiumGltfReader/GltfReader.h>
|
|
#include <CesiumUtility/IntrusivePointer.h>
|
|
#include <CesiumUtility/Math.h>
|
|
|
|
#include <doctest/doctest.h>
|
|
#include <glm/ext/vector_double3.hpp>
|
|
#include <glm/ext/vector_float3.hpp>
|
|
#include <rapidjson/document.h>
|
|
#include <rapidjson/rapidjson.h>
|
|
#include <spdlog/sinks/ringbuffer_sink.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace doctest;
|
|
using namespace CesiumGltf;
|
|
using namespace Cesium3DTilesContent;
|
|
using namespace CesiumUtility;
|
|
|
|
template <typename ExpectedType, typename PropertyViewType = ExpectedType>
|
|
static void checkNonArrayProperty(
|
|
const Model& model,
|
|
const PropertyTable& propertyTable,
|
|
const Class& metaClass,
|
|
const std::string& propertyName,
|
|
const std::string& expectedType,
|
|
const std::optional<std::string>& expectedComponentType,
|
|
const std::vector<ExpectedType>& expected,
|
|
size_t expectedTotalInstances,
|
|
const std::optional<PropertyViewType>& noDataValue = std::nullopt) {
|
|
const ClassProperty& property = metaClass.properties.at(propertyName);
|
|
REQUIRE(property.type == expectedType);
|
|
REQUIRE(property.componentType == expectedComponentType);
|
|
REQUIRE(!property.array);
|
|
REQUIRE(!property.count);
|
|
|
|
PropertyTableView view(model, propertyTable);
|
|
REQUIRE(view.status() == PropertyTableViewStatus::Valid);
|
|
REQUIRE(view.size() == propertyTable.count);
|
|
|
|
PropertyTablePropertyView<PropertyViewType> propertyView =
|
|
view.getPropertyView<PropertyViewType>(propertyName);
|
|
REQUIRE(propertyView.status() == PropertyTablePropertyViewStatus::Valid);
|
|
REQUIRE(propertyView.size() == propertyTable.count);
|
|
REQUIRE(propertyView.size() == static_cast<int64_t>(expectedTotalInstances));
|
|
for (int64_t i = 0; i < propertyView.size(); ++i) {
|
|
if constexpr (std::is_same_v<PropertyViewType, glm::vec3>) {
|
|
REQUIRE(Math::equalsEpsilon(
|
|
static_cast<glm::dvec3>(propertyView.getRaw(i)),
|
|
static_cast<glm::dvec3>(expected[static_cast<size_t>(i)]),
|
|
Math::Epsilon6));
|
|
} else if constexpr (
|
|
std::is_same_v<PropertyViewType, float> ||
|
|
std::is_same_v<PropertyViewType, double>) {
|
|
REQUIRE(
|
|
propertyView.getRaw(i) == Approx(expected[static_cast<size_t>(i)]));
|
|
} else {
|
|
REQUIRE(
|
|
static_cast<ExpectedType>(propertyView.getRaw(i)) ==
|
|
expected[static_cast<size_t>(i)]);
|
|
}
|
|
|
|
if (noDataValue && propertyView.getRaw(i) == noDataValue) {
|
|
REQUIRE(!propertyView.get(i));
|
|
} else {
|
|
REQUIRE(propertyView.get(i) == propertyView.getRaw(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename ExpectedType, typename PropertyViewType = ExpectedType>
|
|
static void checkArrayProperty(
|
|
const Model& model,
|
|
const PropertyTable& propertyTable,
|
|
const Class& metaClass,
|
|
const std::string& propertyName,
|
|
int64_t expectedCount,
|
|
const std::string& expectedType,
|
|
const std::optional<std::string>& expectedComponentType,
|
|
const std::vector<std::vector<ExpectedType>>& expected,
|
|
size_t expectedTotalInstances) {
|
|
const ClassProperty& property = metaClass.properties.at(propertyName);
|
|
REQUIRE(property.type == expectedType);
|
|
REQUIRE(property.componentType == expectedComponentType);
|
|
REQUIRE(property.array);
|
|
REQUIRE(property.count.value_or(0) == expectedCount);
|
|
|
|
PropertyTableView view(model, propertyTable);
|
|
REQUIRE(view.status() == PropertyTableViewStatus::Valid);
|
|
REQUIRE(view.size() == propertyTable.count);
|
|
|
|
PropertyTablePropertyView<PropertyArrayView<PropertyViewType>> propertyView =
|
|
view.getPropertyView<PropertyArrayView<PropertyViewType>>(propertyName);
|
|
REQUIRE(propertyView.status() == PropertyTablePropertyViewStatus::Valid);
|
|
REQUIRE(propertyView.size() == propertyTable.count);
|
|
REQUIRE(propertyView.size() == static_cast<int64_t>(expectedTotalInstances));
|
|
for (size_t i = 0; i < expectedTotalInstances; ++i) {
|
|
PropertyArrayView<PropertyViewType> value =
|
|
propertyView.getRaw(static_cast<int64_t>(i));
|
|
if (expectedCount > 0) {
|
|
REQUIRE(value.size() == expectedCount);
|
|
}
|
|
|
|
for (size_t j = 0; j < expected[i].size(); ++j) {
|
|
if constexpr (
|
|
std::is_same_v<ExpectedType, float> ||
|
|
std::is_same_v<ExpectedType, double>) {
|
|
REQUIRE(value[static_cast<int64_t>(j)] == Approx(expected[i][j]));
|
|
} else {
|
|
REQUIRE(value[static_cast<int64_t>(j)] == expected[i][j]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename ExpectedType, typename PropertyViewType = ExpectedType>
|
|
static void createTestForNonArrayJson(
|
|
const std::vector<ExpectedType>& expected,
|
|
const std::string& expectedType,
|
|
const std::optional<std::string>& expectedComponentType,
|
|
size_t totalInstances,
|
|
const std::optional<PropertyViewType> expectedNoData) {
|
|
Model model;
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetUint64(totalInstances);
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value scalarProperty(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
if (static_cast<PropertyViewType>(expected[i]) == expectedNoData) {
|
|
rapidjson::Value nullValue;
|
|
nullValue.SetNull();
|
|
scalarProperty.PushBack(nullValue, batchTableJson.GetAllocator());
|
|
continue;
|
|
}
|
|
|
|
if constexpr (std::is_same_v<ExpectedType, std::string>) {
|
|
rapidjson::Value value(rapidjson::kStringType);
|
|
value.SetString(
|
|
expected[i].c_str(),
|
|
static_cast<rapidjson::SizeType>(expected[i].size()),
|
|
batchTableJson.GetAllocator());
|
|
scalarProperty.PushBack(value, batchTableJson.GetAllocator());
|
|
} else {
|
|
scalarProperty.PushBack(
|
|
ExpectedType(expected[i]),
|
|
batchTableJson.GetAllocator());
|
|
}
|
|
}
|
|
|
|
batchTableJson.AddMember(
|
|
"scalarProperty",
|
|
scalarProperty,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema> schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 1);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
checkNonArrayProperty<ExpectedType, PropertyViewType>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"scalarProperty",
|
|
expectedType,
|
|
expectedComponentType,
|
|
expected,
|
|
totalInstances,
|
|
expectedNoData);
|
|
}
|
|
|
|
template <typename ExpectedType, typename PropertyViewType = ExpectedType>
|
|
static void createTestForNonArrayJson(
|
|
const std::vector<ExpectedType>& expected,
|
|
const std::string& expectedType,
|
|
const std::optional<std::string>& expectedComponentType,
|
|
size_t totalInstances) {
|
|
Model model;
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetUint64(totalInstances);
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value scalarProperty(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
if constexpr (std::is_same_v<ExpectedType, std::string>) {
|
|
rapidjson::Value value(rapidjson::kStringType);
|
|
value.SetString(
|
|
expected[i].c_str(),
|
|
static_cast<rapidjson::SizeType>(expected[i].size()),
|
|
batchTableJson.GetAllocator());
|
|
scalarProperty.PushBack(value, batchTableJson.GetAllocator());
|
|
} else {
|
|
scalarProperty.PushBack(
|
|
ExpectedType(expected[i]),
|
|
batchTableJson.GetAllocator());
|
|
}
|
|
}
|
|
|
|
batchTableJson.AddMember(
|
|
"scalarProperty",
|
|
scalarProperty,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema> schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 1);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
checkNonArrayProperty<ExpectedType, PropertyViewType>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"scalarProperty",
|
|
expectedType,
|
|
expectedComponentType,
|
|
expected,
|
|
totalInstances);
|
|
}
|
|
|
|
template <typename ExpectedType, typename PropertyViewType = ExpectedType>
|
|
static void createTestForArrayJson(
|
|
const std::vector<std::vector<ExpectedType>>& expected,
|
|
const std::string& expectedType,
|
|
const std::optional<std::string>& expectedComponentType,
|
|
int64_t arrayCount,
|
|
size_t totalInstances) {
|
|
Model model;
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetUint64(totalInstances);
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value fixedArrayProperties(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
rapidjson::Value innerArray(rapidjson::kArrayType);
|
|
for (size_t j = 0; j < expected[i].size(); ++j) {
|
|
if constexpr (std::is_same_v<ExpectedType, std::string>) {
|
|
rapidjson::Value value(rapidjson::kStringType);
|
|
value.SetString(
|
|
expected[i][j].c_str(),
|
|
static_cast<rapidjson::SizeType>(expected[i][j].size()),
|
|
batchTableJson.GetAllocator());
|
|
innerArray.PushBack(value, batchTableJson.GetAllocator());
|
|
} else {
|
|
innerArray.PushBack(
|
|
ExpectedType(expected[i][j]),
|
|
batchTableJson.GetAllocator());
|
|
}
|
|
}
|
|
fixedArrayProperties.PushBack(innerArray, batchTableJson.GetAllocator());
|
|
}
|
|
|
|
batchTableJson.AddMember(
|
|
"fixedLengthArrayProperty",
|
|
fixedArrayProperties,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema>& schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
REQUIRE(schema->classes.find("default") != schema->classes.end());
|
|
|
|
const Class& defaultClass = schema->classes.at("default");
|
|
REQUIRE(defaultClass.properties.size() == 1);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
|
|
checkArrayProperty<ExpectedType, PropertyViewType>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"fixedLengthArrayProperty",
|
|
arrayCount,
|
|
expectedType,
|
|
expectedComponentType,
|
|
expected,
|
|
totalInstances);
|
|
}
|
|
|
|
std::set<int32_t> getUniqueBufferViewIds(
|
|
const std::vector<Accessor>& accessors,
|
|
const PropertyTable& propertyTable) {
|
|
std::set<int32_t> result;
|
|
for (auto it = accessors.begin(); it != accessors.end(); it++) {
|
|
result.insert(it->bufferView);
|
|
}
|
|
|
|
auto& properties = propertyTable.properties;
|
|
for (auto it = properties.begin(); it != properties.end(); it++) {
|
|
auto& property = it->second;
|
|
result.insert(property.values);
|
|
if (property.arrayOffsets >= 0) {
|
|
result.insert(property.arrayOffsets);
|
|
}
|
|
if (property.stringOffsets >= 0) {
|
|
result.insert(property.stringOffsets);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
TEST_CASE("Converts JSON B3DM batch table to EXT_structural_metadata") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath = testFilePath / "BatchTables" / "batchedWithJson.b3dm";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath);
|
|
|
|
REQUIRE(result.model);
|
|
|
|
Model& gltf = *result.model;
|
|
|
|
ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 4);
|
|
|
|
auto idIt = defaultClass.properties.find("id");
|
|
REQUIRE(idIt != defaultClass.properties.end());
|
|
auto longitudeIt = defaultClass.properties.find("Longitude");
|
|
REQUIRE(longitudeIt != defaultClass.properties.end());
|
|
auto latitudeIt = defaultClass.properties.find("Latitude");
|
|
REQUIRE(latitudeIt != defaultClass.properties.end());
|
|
auto heightIt = defaultClass.properties.find("Height");
|
|
REQUIRE(heightIt != defaultClass.properties.end());
|
|
|
|
CHECK(idIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(longitudeIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(latitudeIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(heightIt->second.type == ClassProperty::Type::SCALAR);
|
|
|
|
CHECK(idIt->second.componentType == ClassProperty::ComponentType::INT8);
|
|
CHECK(
|
|
longitudeIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT64);
|
|
CHECK(
|
|
latitudeIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT64);
|
|
CHECK(
|
|
heightIt->second.componentType == ClassProperty::ComponentType::FLOAT64);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 4);
|
|
|
|
auto idIt2 = propertyTable.properties.find("id");
|
|
REQUIRE(idIt2 != propertyTable.properties.end());
|
|
auto longitudeIt2 = propertyTable.properties.find("Longitude");
|
|
REQUIRE(longitudeIt2 != propertyTable.properties.end());
|
|
auto latitudeIt2 = propertyTable.properties.find("Latitude");
|
|
REQUIRE(latitudeIt2 != propertyTable.properties.end());
|
|
auto heightIt2 = propertyTable.properties.find("Height");
|
|
REQUIRE(heightIt2 != propertyTable.properties.end());
|
|
|
|
REQUIRE(idIt2->second.values >= 0);
|
|
REQUIRE(idIt2->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(longitudeIt2->second.values >= 0);
|
|
REQUIRE(
|
|
longitudeIt2->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(latitudeIt2->second.values >= 0);
|
|
REQUIRE(
|
|
latitudeIt2->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(heightIt2->second.values >= 0);
|
|
REQUIRE(
|
|
heightIt2->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
|
|
// Make sure all property bufferViews are unique
|
|
std::set<int32_t> bufferViews{
|
|
idIt2->second.values,
|
|
longitudeIt2->second.values,
|
|
latitudeIt2->second.values,
|
|
heightIt2->second.values};
|
|
CHECK(bufferViews.size() == 4);
|
|
|
|
// Check the mesh primitives
|
|
CHECK(!gltf.meshes.empty());
|
|
|
|
for (Mesh& mesh : gltf.meshes) {
|
|
CHECK(!mesh.primitives.empty());
|
|
for (MeshPrimitive& primitive : mesh.primitives) {
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") !=
|
|
primitive.attributes.end());
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_1") ==
|
|
primitive.attributes.end());
|
|
CHECK(
|
|
primitive.attributes.find("_BATCH_ID") == primitive.attributes.end());
|
|
|
|
ExtensionExtMeshFeatures* pPrimitiveExtension =
|
|
primitive.getExtension<ExtensionExtMeshFeatures>();
|
|
REQUIRE(pPrimitiveExtension);
|
|
CHECK(gltf.isExtensionUsed(ExtensionExtMeshFeatures::ExtensionName));
|
|
REQUIRE(pPrimitiveExtension->featureIds.size() == 1);
|
|
const FeatureId& featureId = pPrimitiveExtension->featureIds[0];
|
|
CHECK(featureId.featureCount == 10);
|
|
CHECK(featureId.attribute == 0);
|
|
CHECK(featureId.propertyTable == 0);
|
|
}
|
|
}
|
|
|
|
// Check metadata values
|
|
{
|
|
std::vector<int8_t> expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
|
checkNonArrayProperty<int8_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"id",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
11.762595914304256,
|
|
13.992324123159051,
|
|
7.490081690251827,
|
|
13.484312580898404,
|
|
11.481756005436182,
|
|
7.836617760360241,
|
|
9.338438434526324,
|
|
13.513022359460592,
|
|
13.74609257467091,
|
|
10.145220385864377};
|
|
checkNonArrayProperty<double>(
|
|
*result.model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Height",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
-1.3196595204101946,
|
|
-1.3196739888070643,
|
|
-1.3196641114334025,
|
|
-1.3196579305297966,
|
|
-1.3196585149509301,
|
|
-1.319678877969692,
|
|
-1.3196612732428445,
|
|
-1.3196718857616954,
|
|
-1.3196471198757775,
|
|
-1.319644104024109};
|
|
checkNonArrayProperty<double>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Longitude",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
0.6988582109,
|
|
0.6988498770649103,
|
|
0.6988533339856887,
|
|
0.6988691467754378,
|
|
0.698848878034009,
|
|
0.6988592976292447,
|
|
0.6988600642191055,
|
|
0.6988670019309562,
|
|
0.6988523191715889,
|
|
0.6988697375823105};
|
|
checkNonArrayProperty<double>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Latitude",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Convert binary B3DM batch table to EXT_structural_metadata") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath =
|
|
testFilePath / "BatchTables" / "batchedWithBatchTableBinary.b3dm";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath);
|
|
|
|
REQUIRE(!result.errors);
|
|
REQUIRE(result.model);
|
|
|
|
const Model& model = *result.model;
|
|
|
|
const ExtensionModelExtStructuralMetadata* metadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(metadata);
|
|
|
|
CesiumUtility::IntrusivePointer<Schema> schema = metadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 6);
|
|
|
|
REQUIRE(metadata->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = metadata->propertyTables[0];
|
|
|
|
// Check that batch IDs were converted to EXT_mesh_features
|
|
CHECK(!model.meshes.empty());
|
|
|
|
for (const Mesh& mesh : model.meshes) {
|
|
CHECK(!mesh.primitives.empty());
|
|
for (const MeshPrimitive& primitive : mesh.primitives) {
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") !=
|
|
primitive.attributes.end());
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_1") ==
|
|
primitive.attributes.end());
|
|
CHECK(
|
|
primitive.attributes.find("_BATCH_ID") == primitive.attributes.end());
|
|
|
|
const ExtensionExtMeshFeatures* pPrimitiveExtension =
|
|
primitive.getExtension<ExtensionExtMeshFeatures>();
|
|
REQUIRE(pPrimitiveExtension);
|
|
CHECK(model.isExtensionUsed(ExtensionExtMeshFeatures::ExtensionName));
|
|
REQUIRE(pPrimitiveExtension->featureIds.size() == 1);
|
|
const FeatureId& featureId = pPrimitiveExtension->featureIds[0];
|
|
CHECK(featureId.featureCount == 10);
|
|
CHECK(featureId.attribute == 0);
|
|
CHECK(featureId.propertyTable == 0);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::vector<int8_t> expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
|
checkNonArrayProperty<int8_t>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"id",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
6.155801922082901,
|
|
13.410263679921627,
|
|
6.1022464875131845,
|
|
6.742499912157655,
|
|
6.869888566434383,
|
|
10.701326800510287,
|
|
6.163868889212608,
|
|
12.224825594574213,
|
|
12.546202838420868,
|
|
7.632075032219291};
|
|
checkNonArrayProperty<double>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Height",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
-1.31968,
|
|
-1.3196832683949145,
|
|
-1.3196637662080655,
|
|
-1.3196656317210846,
|
|
-1.319679266890895,
|
|
-1.319693717777418,
|
|
-1.3196607462778132,
|
|
-1.3196940116311096,
|
|
-1.319683648959897,
|
|
-1.3196959060375169};
|
|
checkNonArrayProperty<double>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Longitude",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<double> expected = {
|
|
0.698874,
|
|
0.6988615321420496,
|
|
0.6988736012180136,
|
|
0.6988863062831799,
|
|
0.6988864387845588,
|
|
0.6988814788613282,
|
|
0.6988618972526105,
|
|
0.6988590050687061,
|
|
0.6988690935212543,
|
|
0.6988854945986224};
|
|
checkNonArrayProperty<double>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"Latitude",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<uint8_t> expected(10, 255);
|
|
checkNonArrayProperty<uint8_t>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"code",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT8,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
// clang-format off
|
|
std::vector<glm::dvec3> expected{
|
|
{-1.31968, 0.698874, 6.155801922082901},
|
|
{-1.3196832683949145, 0.6988615321420496, 13.410263679921627},
|
|
{-1.3196637662080655, 0.6988736012180136, 6.1022464875131845},
|
|
{-1.3196656317210846, 0.6988863062831799, 6.742499912157655},
|
|
{-1.319679266890895, 0.6988864387845588, 6.869888566434383},
|
|
{-1.319693717777418, 0.6988814788613282, 10.701326800510287},
|
|
{-1.3196607462778132, 0.6988618972526105, 6.163868889212608},
|
|
{-1.3196940116311096, 0.6988590050687061, 12.224825594574213},
|
|
{-1.319683648959897, 0.6988690935212543, 12.546202838420868},
|
|
{-1.3196959060375169, 0.6988854945986224, 7.632075032219291}
|
|
};
|
|
// clang-format on
|
|
checkNonArrayProperty<glm::dvec3>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"cartographic",
|
|
ClassProperty::Type::VEC3,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Converts batched PNTS batch table to EXT_structural_metadata") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath = testFilePath / "PointCloud" / "pointCloudBatched.pnts";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath);
|
|
|
|
REQUIRE(result.model);
|
|
const Model& gltf = *result.model;
|
|
|
|
const ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
const Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 3);
|
|
|
|
{
|
|
auto nameIt = defaultClass.properties.find("name");
|
|
REQUIRE(nameIt != defaultClass.properties.end());
|
|
auto dimensionsIt = defaultClass.properties.find("dimensions");
|
|
REQUIRE(dimensionsIt != defaultClass.properties.end());
|
|
auto idIt = defaultClass.properties.find("id");
|
|
REQUIRE(idIt != defaultClass.properties.end());
|
|
|
|
CHECK(nameIt->second.type == ClassProperty::Type::STRING);
|
|
CHECK(dimensionsIt->second.type == ClassProperty::Type::VEC3);
|
|
CHECK(
|
|
dimensionsIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT32);
|
|
CHECK(idIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(idIt->second.componentType == ClassProperty::ComponentType::UINT32);
|
|
}
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 3);
|
|
|
|
{
|
|
auto nameIt = propertyTable.properties.find("name");
|
|
REQUIRE(nameIt != propertyTable.properties.end());
|
|
auto dimensionsIt = propertyTable.properties.find("dimensions");
|
|
REQUIRE(dimensionsIt != propertyTable.properties.end());
|
|
auto idIt = propertyTable.properties.find("id");
|
|
REQUIRE(idIt != propertyTable.properties.end());
|
|
|
|
REQUIRE(nameIt->second.values >= 0);
|
|
REQUIRE(
|
|
nameIt->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(dimensionsIt->second.values >= 0);
|
|
REQUIRE(
|
|
dimensionsIt->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(idIt->second.values >= 0);
|
|
REQUIRE(
|
|
idIt->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
}
|
|
|
|
std::set<int32_t> bufferViewSet =
|
|
getUniqueBufferViewIds(gltf.accessors, propertyTable);
|
|
CHECK(bufferViewSet.size() == gltf.bufferViews.size());
|
|
|
|
// Check the mesh primitive
|
|
REQUIRE(gltf.meshes.size() == 1);
|
|
const Mesh& mesh = gltf.meshes[0];
|
|
REQUIRE(mesh.primitives.size() == 1);
|
|
|
|
const MeshPrimitive& primitive = mesh.primitives[0];
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") != primitive.attributes.end());
|
|
|
|
const ExtensionExtMeshFeatures* pPrimitiveExtension =
|
|
primitive.getExtension<ExtensionExtMeshFeatures>();
|
|
REQUIRE(pPrimitiveExtension);
|
|
CHECK(gltf.isExtensionUsed(ExtensionExtMeshFeatures::ExtensionName));
|
|
REQUIRE(pPrimitiveExtension->featureIds.size() == 1);
|
|
const FeatureId& featureId = pPrimitiveExtension->featureIds[0];
|
|
CHECK(featureId.featureCount == 8);
|
|
CHECK(featureId.attribute == 0);
|
|
CHECK(featureId.propertyTable == 0);
|
|
|
|
// Check metadata values
|
|
{
|
|
std::vector<std::string> expected = {
|
|
"section0",
|
|
"section1",
|
|
"section2",
|
|
"section3",
|
|
"section4",
|
|
"section5",
|
|
"section6",
|
|
"section7"};
|
|
checkNonArrayProperty<std::string, std::string_view>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"name",
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<glm::vec3> expected = {
|
|
{0.1182744f, 0.7206326f, 0.6399210f},
|
|
{0.5820198f, 0.1433532f, 0.5373732f},
|
|
{0.9446688f, 0.7586156f, 0.5218483f},
|
|
{0.1059076f, 0.4146619f, 0.4736004f},
|
|
{0.2645556f, 0.1863323f, 0.7742336f},
|
|
{0.7369181f, 0.4561503f, 0.2165503f},
|
|
{0.5684339f, 0.1352181f, 0.0187897f},
|
|
{0.3241409f, 0.6176354f, 0.1496748f}};
|
|
checkNonArrayProperty<glm::vec3>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"dimensions",
|
|
ClassProperty::Type::VEC3,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<uint32_t> expected = {0, 1, 2, 3, 4, 5, 6, 7};
|
|
checkNonArrayProperty<uint32_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"id",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Converts per-point PNTS batch table to EXT_structural_metadata") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath =
|
|
testFilePath / "PointCloud" / "pointCloudWithPerPointProperties.pnts";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath);
|
|
|
|
REQUIRE(result.model);
|
|
const Model& gltf = *result.model;
|
|
|
|
const ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
const Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 3);
|
|
|
|
{
|
|
auto temperatureIt = defaultClass.properties.find("temperature");
|
|
REQUIRE(temperatureIt != defaultClass.properties.end());
|
|
auto secondaryColorIt = defaultClass.properties.find("secondaryColor");
|
|
REQUIRE(secondaryColorIt != defaultClass.properties.end());
|
|
auto idIt = defaultClass.properties.find("id");
|
|
REQUIRE(idIt != defaultClass.properties.end());
|
|
|
|
CHECK(temperatureIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(
|
|
temperatureIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT32);
|
|
CHECK(secondaryColorIt->second.type == ClassProperty::Type::VEC3);
|
|
REQUIRE(secondaryColorIt->second.componentType);
|
|
CHECK(
|
|
secondaryColorIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT32);
|
|
CHECK(idIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(idIt->second.componentType == ClassProperty::ComponentType::UINT16);
|
|
}
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 3);
|
|
|
|
{
|
|
auto temperatureIt = propertyTable.properties.find("temperature");
|
|
REQUIRE(temperatureIt != propertyTable.properties.end());
|
|
auto secondaryColorIt = propertyTable.properties.find("secondaryColor");
|
|
REQUIRE(secondaryColorIt != propertyTable.properties.end());
|
|
auto idIt = propertyTable.properties.find("id");
|
|
REQUIRE(idIt != propertyTable.properties.end());
|
|
|
|
REQUIRE(temperatureIt->second.values >= 0);
|
|
REQUIRE(
|
|
temperatureIt->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(secondaryColorIt->second.values >= 0);
|
|
REQUIRE(
|
|
secondaryColorIt->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(idIt->second.values >= 0);
|
|
REQUIRE(
|
|
idIt->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
}
|
|
|
|
std::set<int32_t> bufferViewSet =
|
|
getUniqueBufferViewIds(gltf.accessors, propertyTable);
|
|
CHECK(bufferViewSet.size() == gltf.bufferViews.size());
|
|
|
|
// Check the mesh primitive
|
|
REQUIRE(gltf.meshes.size() == 1);
|
|
const Mesh& mesh = gltf.meshes[0];
|
|
REQUIRE(mesh.primitives.size() == 1);
|
|
|
|
const MeshPrimitive& primitive = mesh.primitives[0];
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") == primitive.attributes.end());
|
|
|
|
const ExtensionExtMeshFeatures* pPrimitiveExtension =
|
|
primitive.getExtension<ExtensionExtMeshFeatures>();
|
|
REQUIRE(pPrimitiveExtension);
|
|
CHECK(gltf.isExtensionUsed(ExtensionExtMeshFeatures::ExtensionName));
|
|
REQUIRE(pPrimitiveExtension->featureIds.size() == 1);
|
|
const FeatureId& featureId = pPrimitiveExtension->featureIds[0];
|
|
CHECK(featureId.featureCount == 8);
|
|
CHECK(!featureId.attribute);
|
|
CHECK(featureId.propertyTable == 0);
|
|
|
|
// Check metadata values
|
|
{
|
|
std::vector<float> expected = {
|
|
0.2883332f,
|
|
0.4338732f,
|
|
0.1750928f,
|
|
0.1430827f,
|
|
0.1156976f,
|
|
0.3274261f,
|
|
0.1337213f,
|
|
0.0207673f};
|
|
checkNonArrayProperty<float>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"temperature",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<glm::vec3> expected = {
|
|
{0.0202183f, 0, 0},
|
|
{0.3682415f, 0, 0},
|
|
{0.8326198f, 0, 0},
|
|
{0.9571551f, 0, 0},
|
|
{0.7781567f, 0, 0},
|
|
{0.1403507f, 0, 0},
|
|
{0.8700121f, 0, 0},
|
|
{0.8700872f, 0, 0}};
|
|
checkNonArrayProperty<glm::vec3>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"secondaryColor",
|
|
ClassProperty::Type::VEC3,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<uint16_t> expected = {0, 1, 2, 3, 4, 5, 6, 7};
|
|
checkNonArrayProperty<uint16_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"id",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT16,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Draco-compressed b3dm uses _FEATURE_ID_0 attribute name in glTF") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath =
|
|
testFilePath / "BatchTables" / "batchedWithBatchTable-draco.b3dm";
|
|
|
|
CesiumGltfReader::GltfReaderOptions options;
|
|
options.decodeDraco = false;
|
|
|
|
GltfConverterResult result =
|
|
ConvertTileToGltf::fromB3dm(testFilePath, options);
|
|
CHECK(result.errors.errors.empty());
|
|
CHECK(result.errors.warnings.empty());
|
|
REQUIRE(result.model);
|
|
|
|
const Model& gltf = *result.model;
|
|
|
|
CHECK(!gltf.meshes.empty());
|
|
for (const Mesh& mesh : gltf.meshes) {
|
|
CHECK(!mesh.primitives.empty());
|
|
for (const MeshPrimitive& primitive : mesh.primitives) {
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") !=
|
|
primitive.attributes.end());
|
|
|
|
const ExtensionKhrDracoMeshCompression* pDraco =
|
|
primitive.getExtension<ExtensionKhrDracoMeshCompression>();
|
|
REQUIRE(pDraco);
|
|
CHECK(
|
|
pDraco->attributes.find("_FEATURE_ID_0") != pDraco->attributes.end());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Converts Draco per-point PNTS batch table to "
|
|
"EXT_structural_metadata") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath = testFilePath / "PointCloud" / "pointCloudDraco.pnts";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath);
|
|
|
|
REQUIRE(result.model);
|
|
const Model& gltf = *result.model;
|
|
|
|
const ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
const Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 3);
|
|
|
|
{
|
|
auto temperatureIt = defaultClass.properties.find("temperature");
|
|
REQUIRE(temperatureIt != defaultClass.properties.end());
|
|
auto secondaryColorIt = defaultClass.properties.find("secondaryColor");
|
|
REQUIRE(secondaryColorIt != defaultClass.properties.end());
|
|
auto idIt = defaultClass.properties.find("id");
|
|
REQUIRE(idIt != defaultClass.properties.end());
|
|
|
|
CHECK(temperatureIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(
|
|
temperatureIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT32);
|
|
CHECK(secondaryColorIt->second.type == ClassProperty::Type::VEC3);
|
|
REQUIRE(secondaryColorIt->second.componentType);
|
|
CHECK(
|
|
secondaryColorIt->second.componentType ==
|
|
ClassProperty::ComponentType::FLOAT32);
|
|
CHECK(idIt->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(idIt->second.componentType == ClassProperty::ComponentType::UINT16);
|
|
}
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 3);
|
|
|
|
{
|
|
auto temperatureIt = propertyTable.properties.find("temperature");
|
|
REQUIRE(temperatureIt != propertyTable.properties.end());
|
|
auto secondaryColorIt = propertyTable.properties.find("secondaryColor");
|
|
REQUIRE(secondaryColorIt != propertyTable.properties.end());
|
|
auto idIt = propertyTable.properties.find("id");
|
|
REQUIRE(idIt != propertyTable.properties.end());
|
|
|
|
REQUIRE(temperatureIt->second.values >= 0);
|
|
REQUIRE(
|
|
temperatureIt->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(secondaryColorIt->second.values >= 0);
|
|
REQUIRE(
|
|
secondaryColorIt->second.values <
|
|
static_cast<int32_t>(gltf.bufferViews.size()));
|
|
REQUIRE(idIt->second.values >= 0);
|
|
REQUIRE(
|
|
idIt->second.values < static_cast<int32_t>(gltf.bufferViews.size()));
|
|
}
|
|
|
|
std::set<int32_t> bufferViewSet =
|
|
getUniqueBufferViewIds(gltf.accessors, propertyTable);
|
|
CHECK(bufferViewSet.size() == gltf.bufferViews.size());
|
|
|
|
// Check the mesh primitive
|
|
REQUIRE(gltf.meshes.size() == 1);
|
|
const Mesh& mesh = gltf.meshes[0];
|
|
REQUIRE(mesh.primitives.size() == 1);
|
|
|
|
const MeshPrimitive& primitive = mesh.primitives[0];
|
|
CHECK(
|
|
primitive.attributes.find("_FEATURE_ID_0") == primitive.attributes.end());
|
|
|
|
const ExtensionExtMeshFeatures* pPrimitiveExtension =
|
|
primitive.getExtension<ExtensionExtMeshFeatures>();
|
|
REQUIRE(pPrimitiveExtension);
|
|
CHECK(gltf.isExtensionUsed(ExtensionExtMeshFeatures::ExtensionName));
|
|
REQUIRE(pPrimitiveExtension->featureIds.size() == 1);
|
|
const FeatureId& featureId = pPrimitiveExtension->featureIds[0];
|
|
CHECK(featureId.featureCount == 8);
|
|
CHECK(!featureId.attribute);
|
|
CHECK(featureId.propertyTable == 0);
|
|
|
|
// Check metadata values
|
|
{
|
|
std::vector<float> expected = {
|
|
0.2883025f,
|
|
0.4338731f,
|
|
0.1751145f,
|
|
0.1430345f,
|
|
0.1156959f,
|
|
0.3274441f,
|
|
0.1337535f,
|
|
0.0207673f};
|
|
checkNonArrayProperty<float>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"temperature",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<glm::vec3> expected = {
|
|
{0.1182744f, 0, 0},
|
|
{0.7206645f, 0, 0},
|
|
{0.6399421f, 0, 0},
|
|
{0.5820239f, 0, 0},
|
|
{0.1432983f, 0, 0},
|
|
{0.5374249f, 0, 0},
|
|
{0.9446688f, 0, 0},
|
|
{0.7586040f, 0, 0}};
|
|
checkNonArrayProperty<glm::vec3>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"secondaryColor",
|
|
ClassProperty::Type::VEC3,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<uint16_t> expected = {0, 1, 2, 3, 4, 5, 6, 7};
|
|
checkNonArrayProperty<uint16_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
"id",
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT16,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Upgrade nested JSON metadata to string") {
|
|
std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR;
|
|
testFilePath =
|
|
testFilePath / "BatchTables" / "batchedWithStringAndNestedJson.b3dm";
|
|
|
|
GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath);
|
|
|
|
REQUIRE(!result.errors);
|
|
REQUIRE(result.model);
|
|
|
|
const Model& model = *result.model;
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
result.model->getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema>& schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 6);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
REQUIRE(propertyTable.count == 10);
|
|
|
|
{
|
|
std::vector<std::string> expected;
|
|
for (int64_t i = 0; i < propertyTable.count; ++i) {
|
|
std::string v = std::string("{\"name\":\"building") + std::to_string(i) +
|
|
"\",\"year\":" + std::to_string(i) + "}";
|
|
expected.push_back(v);
|
|
}
|
|
checkNonArrayProperty<std::string, std::string_view>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"info",
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
{
|
|
std::vector<std::vector<std::string>> expected;
|
|
for (int64_t i = 0; i < propertyTable.count; ++i) {
|
|
std::vector<std::string> expectedVal;
|
|
expectedVal.emplace_back("room" + std::to_string(i) + "_a");
|
|
expectedVal.emplace_back("room" + std::to_string(i) + "_b");
|
|
expectedVal.emplace_back("room" + std::to_string(i) + "_c");
|
|
expected.emplace_back(std::move(expectedVal));
|
|
}
|
|
checkArrayProperty<std::string, std::string_view>(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"rooms",
|
|
3,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Upgrade JSON booleans to binary") {
|
|
Model model;
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetInt64(10);
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
std::vector<bool> expected =
|
|
{true, false, true, true, false, true, false, true, false, true};
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value boolProperties(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
boolProperties.PushBack(
|
|
rapidjson::Value(static_cast<bool>(expected[i])),
|
|
batchTableJson.GetAllocator());
|
|
}
|
|
batchTableJson.AddMember(
|
|
"boolProp",
|
|
boolProperties,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema>& schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 1);
|
|
|
|
const ClassProperty& propertyClass = properties.at("boolProp");
|
|
REQUIRE(propertyClass.type == "BOOLEAN");
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
checkNonArrayProperty(
|
|
model,
|
|
propertyTable,
|
|
defaultClass,
|
|
"boolProp",
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
expected,
|
|
expected.size());
|
|
}
|
|
|
|
TEST_CASE("Upgrade fixed-length JSON arrays") {
|
|
SUBCASE("int8_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int8_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{12, 50, -12, -1},
|
|
{123, 10, 122, 3},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, 5}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint8_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint8_t>> expected {
|
|
{0, 1, 4, 1, 223},
|
|
{12, 50, 242, 212, 11},
|
|
{223, 10, 122, 3, 44},
|
|
{13, 45, 122, 94, 244},
|
|
{119, 112, 156, 5, 35}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT8,
|
|
5,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int16_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int16_t>> expected {
|
|
{0, 1, 4, 4445},
|
|
{12, 50, -12, -1},
|
|
{123, 10, 3333, 3},
|
|
{13, 450, 122, 94},
|
|
{11, 22, 3, 50}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT16,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint16_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint16_t>> expected {
|
|
{0, 1, 4, 65000},
|
|
{12, 50, 12, 1},
|
|
{123, 10, 33330, 3},
|
|
{13, 450, 1220, 94},
|
|
{11, 22, 3, 50000}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT16,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int32_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int32_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, -500000, 1222, 544662},
|
|
{123, -10, 122, 334},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, 2147483647}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT32,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint32_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint32_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, 12200000, 1222, 544662},
|
|
{123, 10, 122, 334},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, (uint32_t)4294967295}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT32,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int64_t") {
|
|
// The max positive number only requires uint32_t, but due to
|
|
// the negative number, it is upgraded to int64_t.
|
|
// clang-format off
|
|
std::vector<std::vector<int64_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, -922, 1222, 54},
|
|
{123, 10, 122, 334},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, 3147483647}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT64,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint64_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint64_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, 13223302036854775807u, 1222, 544662},
|
|
{123, 10, 122, 334},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, 13223302036854775807u}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT64,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("float") {
|
|
// clang-format off
|
|
std::vector<std::vector<float>> expected {
|
|
{0.122f, 1.1233f, 4.113f, 1.11f},
|
|
{1.244f, 122.3f, 1.222f, 544.66f},
|
|
{12.003f, 1.21f, 2.123f, 33.12f},
|
|
{1.333f, 4.232f, 1.422f, 9.4f},
|
|
{1.1221f, 2.2f, 3.0f, 122.31f}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("double") {
|
|
// clang-format off
|
|
std::vector<std::vector<double>> expected {
|
|
{0.122, 1.1233, 4.113, 1.11},
|
|
{1.244, 122.3, 1.222, 544.66},
|
|
{12.003, 1.21, 2.123, 33.12},
|
|
{1.333, 4.232, 1.422, 9.4},
|
|
{1.1221, 2.2, 3.0, 122.31}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("string") {
|
|
// clang-format off
|
|
std::vector<std::vector<std::string>> expected{
|
|
{"Test0", "Test1", "Test2", "Test4"},
|
|
{"Test5", "Test6", "Test7", "Test8"},
|
|
{"Test9", "Test10", "Test11", "Test12"},
|
|
{"Test13", "Test14", "Test15", "Test16"},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
4,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("Boolean") {
|
|
// clang-format off
|
|
std::vector<std::vector<bool>> expected{
|
|
{true, true, false, true, false, true},
|
|
{true, false, true, false, true, true},
|
|
{false, true, true, false, false, true},
|
|
{false, true, true, true, true, true},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
6,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Upgrade variable-length JSON arrays") {
|
|
SUBCASE("int8_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int8_t>> expected {
|
|
{0, 1, 4},
|
|
{12, 50, -12},
|
|
{123, 10, 122, 3, 23},
|
|
{13, 45},
|
|
{11, 22, 3, 5, 33, 12, -122}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint8_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint8_t>> expected {
|
|
{0, 223},
|
|
{12, 50, 242, 212, 11},
|
|
{223},
|
|
{13, 45},
|
|
{119, 112, 156, 5, 35, 244, 122}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT8,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int16_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int16_t>> expected {
|
|
{0, 1, 4, 4445, 12333},
|
|
{12, 50, -12, -1},
|
|
{123, 10},
|
|
{13, 450, 122, 94, 334},
|
|
{11, 22, 3, 50, 455, 122, 3333, 5555, 12233}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT16,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint16_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint16_t>> expected {
|
|
{0, 1},
|
|
{12, 50, 12, 1, 333, 5666},
|
|
{123, 10, 33330, 3, 1},
|
|
{13, 1220},
|
|
{11, 22, 3, 50000, 333}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT16,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int32_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int32_t>> expected {
|
|
{0, 1},
|
|
{1244, -500000, 1222, 544662},
|
|
{123, -10},
|
|
{13},
|
|
{11, 22, 3, 2147483647, 12233}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT32,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint32_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint32_t>> expected {
|
|
{0, 1},
|
|
{1244, 12200000, 1222, 544662},
|
|
{123, 10},
|
|
{13, 45, 122, 94, 333, 212, 534, 1122},
|
|
{11, 22, 3, (uint32_t)4294967295}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT32,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("int64_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<int64_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, -9223372036854775807, 1222, 544662, 12233},
|
|
{123},
|
|
{13, 45},
|
|
{11, 22, 3, 9223372036854775807, 12333}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT64,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("uint64_t") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint64_t>> expected {
|
|
{1},
|
|
{1244, 13223302036854775807u, 1222, 544662},
|
|
{123, 10, 2},
|
|
{13, 94},
|
|
{11, 22, 3, 13223302036854775807u, 32323}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT64,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("float") {
|
|
// clang-format off
|
|
std::vector<std::vector<float>> expected {
|
|
{0.122f, 1.1233f},
|
|
{1.244f, 122.3f, 1.222f, 544.66f, 323.122f},
|
|
{12.003f, 1.21f, 2.123f, 33.12f, 122.2f},
|
|
{1.333f},
|
|
{1.1221f, 2.2f}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT32,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("double") {
|
|
// clang-format off
|
|
std::vector<std::vector<double>> expected {
|
|
{0.122, 1.1233},
|
|
{1.244, 122.3, 1.222, 544.66, 323.122},
|
|
{12.003, 1.21, 2.123, 33.12, 122.2},
|
|
{1.333},
|
|
{1.1221, 2.2}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::FLOAT64,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("string") {
|
|
// clang-format off
|
|
std::vector<std::vector<std::string>> expected{
|
|
{"This is Test", "Another Test"},
|
|
{"Good morning", "How you doing?", "The book in the freezer", "Batman beats superman", ""},
|
|
{"Test9", "Test10", "", "Test12", ""},
|
|
{"Test13", ""},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
0,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("Boolean") {
|
|
// clang-format off
|
|
std::vector<std::vector<bool>> expected{
|
|
{true, true, false, true, false, false, true},
|
|
{true, false},
|
|
{false, true, true, false},
|
|
{false, true, true},
|
|
{true, true, true, true, false, false}
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
0,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Upgrade JSON values") {
|
|
SUBCASE("Uint32") {
|
|
// Even though the values are typed uint32, they are small enough to be
|
|
// stored as int8s. Signed types are preferred over unsigned.
|
|
std::vector<uint32_t> expected{32, 45, 21, 65, 78};
|
|
createTestForNonArrayJson<uint32_t, int8_t>(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("Boolean") {
|
|
std::vector<bool> expected{true, false, true, false, true, true, false};
|
|
createTestForNonArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
expected.size());
|
|
}
|
|
|
|
SUBCASE("String") {
|
|
std::vector<std::string> expected{"Test 0", "Test 1", "Test 2", "Test 3"};
|
|
createTestForNonArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Uses sentinel values for JSON null values") {
|
|
SUBCASE("Uint32 with sentinel value 0") {
|
|
// Even though the values are typed uint32, they are small enough to be
|
|
// stored as int8s. Signed types are preferred over unsigned.
|
|
std::vector<uint32_t> expected{32, 45, 0, 21, 0, 65, 78};
|
|
createTestForNonArrayJson<uint32_t, int8_t>(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected.size(),
|
|
static_cast<int8_t>(0));
|
|
}
|
|
|
|
SUBCASE("Int32 with sentinel value 0") {
|
|
// Even though the values are typed int32, they are small enough to be
|
|
// stored as int8s. Signed types are preferred over unsigned.
|
|
std::vector<int32_t> expected{32, 45, -3, 0, 21, 0, -65, 78};
|
|
createTestForNonArrayJson<int32_t, int8_t>(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected.size(),
|
|
static_cast<int8_t>(0));
|
|
}
|
|
|
|
SUBCASE("Int32 with sentinel value -1") {
|
|
// Even though the values are typed int32, they are small enough to be
|
|
// stored as int8s. Signed types are preferred over unsigned.
|
|
std::vector<int32_t> expected{32, 45, -3, 0, 21, 0, -1, -65, 78};
|
|
createTestForNonArrayJson<int32_t, int8_t>(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected.size(),
|
|
static_cast<int8_t>(-1));
|
|
}
|
|
|
|
SUBCASE("String with 'null'") {
|
|
std::vector<std::string> expected{
|
|
"Test 0",
|
|
"Test 1",
|
|
"Test 2",
|
|
"null"
|
|
"Test 3"};
|
|
createTestForNonArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected.size(),
|
|
std::string_view("null"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Defaults to string if no sentinel values are available") {
|
|
SUBCASE("Uint64") {
|
|
Model model;
|
|
std::vector<std::optional<uint64_t>> expected{
|
|
32,
|
|
45,
|
|
0,
|
|
255,
|
|
std::nullopt,
|
|
0,
|
|
65,
|
|
78,
|
|
std::numeric_limits<uint64_t>::max()};
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetUint64(static_cast<uint64_t>(expected.size()));
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value scalarProperty(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
if (!expected[i]) {
|
|
rapidjson::Value nullValue;
|
|
nullValue.SetNull();
|
|
scalarProperty.PushBack(nullValue, batchTableJson.GetAllocator());
|
|
continue;
|
|
}
|
|
|
|
scalarProperty.PushBack(*expected[i], batchTableJson.GetAllocator());
|
|
}
|
|
|
|
batchTableJson.AddMember(
|
|
"scalarProperty",
|
|
scalarProperty,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema> schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 1);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
const ClassProperty& property =
|
|
defaultClass.properties.at("scalarProperty");
|
|
REQUIRE(property.type == ClassProperty::Type::STRING);
|
|
REQUIRE(!property.componentType);
|
|
REQUIRE(!property.array);
|
|
REQUIRE(!property.count);
|
|
|
|
PropertyTableView view(model, propertyTable);
|
|
REQUIRE(view.status() == PropertyTableViewStatus::Valid);
|
|
REQUIRE(view.size() == propertyTable.count);
|
|
|
|
PropertyTablePropertyView<std::string_view> propertyView =
|
|
view.getPropertyView<std::string_view>("scalarProperty");
|
|
REQUIRE(propertyView.status() == PropertyTablePropertyViewStatus::Valid);
|
|
REQUIRE(propertyView.size() == propertyTable.count);
|
|
REQUIRE(propertyView.size() == static_cast<int64_t>(expected.size()));
|
|
for (int64_t i = 0; i < propertyView.size(); ++i) {
|
|
auto expectedValue = expected[static_cast<size_t>(i)];
|
|
if (expectedValue) {
|
|
std::string asString = std::to_string(*expectedValue);
|
|
REQUIRE(propertyView.getRaw(i) == asString);
|
|
} else {
|
|
REQUIRE(propertyView.getRaw(i) == "null");
|
|
}
|
|
|
|
REQUIRE(propertyView.get(i) == propertyView.getRaw(i));
|
|
}
|
|
}
|
|
|
|
SUBCASE("Int32") {
|
|
Model model;
|
|
std::vector<std::optional<int32_t>>
|
|
expected{32, 45, 0, -1, std::nullopt, 0, 65, 78};
|
|
|
|
rapidjson::Document featureTableJson;
|
|
featureTableJson.SetObject();
|
|
rapidjson::Value batchLength(rapidjson::kNumberType);
|
|
batchLength.SetUint64(static_cast<uint64_t>(expected.size()));
|
|
featureTableJson.AddMember(
|
|
"BATCH_LENGTH",
|
|
batchLength,
|
|
featureTableJson.GetAllocator());
|
|
|
|
rapidjson::Document batchTableJson;
|
|
batchTableJson.SetObject();
|
|
rapidjson::Value scalarProperty(rapidjson::kArrayType);
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
if (!expected[i]) {
|
|
rapidjson::Value nullValue;
|
|
nullValue.SetNull();
|
|
scalarProperty.PushBack(nullValue, batchTableJson.GetAllocator());
|
|
continue;
|
|
}
|
|
|
|
scalarProperty.PushBack(*expected[i], batchTableJson.GetAllocator());
|
|
}
|
|
|
|
batchTableJson.AddMember(
|
|
"scalarProperty",
|
|
scalarProperty,
|
|
batchTableJson.GetAllocator());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableJson,
|
|
batchTableJson,
|
|
std::span<const std::byte>(),
|
|
model);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pMetadata =
|
|
model.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pMetadata);
|
|
|
|
const CesiumUtility::IntrusivePointer<Schema> schema = pMetadata->schema;
|
|
REQUIRE(schema);
|
|
|
|
const std::unordered_map<std::string, Class>& classes = schema->classes;
|
|
REQUIRE(classes.size() == 1);
|
|
|
|
const Class& defaultClass = classes.at("default");
|
|
const std::unordered_map<std::string, ClassProperty>& properties =
|
|
defaultClass.properties;
|
|
REQUIRE(properties.size() == 1);
|
|
|
|
REQUIRE(pMetadata->propertyTables.size() == 1);
|
|
|
|
const PropertyTable& propertyTable = pMetadata->propertyTables[0];
|
|
const ClassProperty& property =
|
|
defaultClass.properties.at("scalarProperty");
|
|
REQUIRE(property.type == ClassProperty::Type::STRING);
|
|
REQUIRE(!property.componentType);
|
|
REQUIRE(!property.array);
|
|
REQUIRE(!property.count);
|
|
|
|
PropertyTableView view(model, propertyTable);
|
|
REQUIRE(view.status() == PropertyTableViewStatus::Valid);
|
|
REQUIRE(view.size() == propertyTable.count);
|
|
|
|
PropertyTablePropertyView<std::string_view> propertyView =
|
|
view.getPropertyView<std::string_view>("scalarProperty");
|
|
REQUIRE(propertyView.status() == PropertyTablePropertyViewStatus::Valid);
|
|
REQUIRE(propertyView.size() == propertyTable.count);
|
|
REQUIRE(propertyView.size() == static_cast<int64_t>(expected.size()));
|
|
for (int64_t i = 0; i < propertyView.size(); ++i) {
|
|
auto expectedValue = expected[static_cast<size_t>(i)];
|
|
if (expectedValue) {
|
|
std::string asString = std::to_string(*expectedValue);
|
|
REQUIRE(propertyView.getRaw(i) == asString);
|
|
} else {
|
|
REQUIRE(propertyView.getRaw(i) == "null");
|
|
}
|
|
|
|
REQUIRE(propertyView.get(i) == propertyView.getRaw(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Cannot write past batch table length") {
|
|
SUBCASE("Uint32") {
|
|
std::vector<uint32_t> expected{32, 45, 21, 65, 78, 20, 33, 12};
|
|
createTestForNonArrayJson<uint32_t, int8_t>(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
4);
|
|
}
|
|
|
|
SUBCASE("Boolean") {
|
|
std::vector<bool> expected{true, false, true, false, true, true, false};
|
|
createTestForNonArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
4);
|
|
}
|
|
|
|
SUBCASE("String") {
|
|
std::vector<std::string>
|
|
expected{"Test 0", "Test 1", "Test 2", "Test 3", "Test 4"};
|
|
createTestForNonArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
3);
|
|
}
|
|
|
|
SUBCASE("Fixed-length scalar array") {
|
|
// clang-format off
|
|
std::vector<std::vector<uint64_t>> expected {
|
|
{0, 1, 4, 1},
|
|
{1244, 13223302036854775807u, 1222, 544662},
|
|
{123, 10, 122, 334},
|
|
{13, 45, 122, 94},
|
|
{11, 22, 3, 13223302036854775807u}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::UINT64,
|
|
4,
|
|
2);
|
|
}
|
|
|
|
SUBCASE("Fixed-length boolean array") {
|
|
// clang-format off
|
|
std::vector<std::vector<bool>> expected{
|
|
{true, true, false},
|
|
{true, false, true},
|
|
{false, true, true},
|
|
{false, true, true},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
3,
|
|
2);
|
|
}
|
|
|
|
SUBCASE("Fixed-length string array") {
|
|
// clang-format off
|
|
std::vector<std::vector<std::string>> expected{
|
|
{"Test0", "Test1", "Test2", "Test4"},
|
|
{"Test5", "Test6", "Test7", "Test8"},
|
|
{"Test9", "Test10", "Test11", "Test12"},
|
|
{"Test13", "Test14", "Test15", "Test16"},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
4,
|
|
2);
|
|
}
|
|
|
|
SUBCASE("Variable-length number array") {
|
|
// clang-format off
|
|
std::vector<std::vector<int32_t>> expected {
|
|
{0, 1},
|
|
{1244, -500000, 1222, 544662},
|
|
{123, -10},
|
|
{13},
|
|
{11, 22, 3, 2147483647, 12233}};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT32,
|
|
0,
|
|
3);
|
|
}
|
|
|
|
SUBCASE("Variable-length boolean array") {
|
|
// clang-format off
|
|
std::vector<std::vector<bool>> expected{
|
|
{true, true, false, true, false, false, true},
|
|
{true, false},
|
|
{false, true, true, false},
|
|
{false, true, true},
|
|
{true, true, false, false}
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson(
|
|
expected,
|
|
ClassProperty::Type::BOOLEAN,
|
|
std::nullopt,
|
|
0,
|
|
2);
|
|
}
|
|
|
|
SUBCASE("Variable-length string array") {
|
|
// clang-format off
|
|
std::vector<std::vector<std::string>> expected{
|
|
{"This is Test", "Another Test"},
|
|
{"Good morning", "How you doing?", "The book in the freezer", "Batman beats superman", ""},
|
|
{"Test9", "Test10", "", "Test12", ""},
|
|
{"Test13", ""},
|
|
};
|
|
// clang-format on
|
|
|
|
createTestForArrayJson<std::string, std::string_view>(
|
|
expected,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
0,
|
|
2);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Converts \"Feature Classes\" 3DTILES_batch_table_hierarchy example "
|
|
"to EXT_structural_metadata") {
|
|
Model gltf;
|
|
|
|
std::string featureTableJson = R"(
|
|
{
|
|
"BATCH_LENGTH": 8
|
|
}
|
|
)";
|
|
|
|
// "Feature classes" example from the spec:
|
|
// https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy#feature-classes
|
|
std::string batchTableJson = R"(
|
|
{
|
|
"extensions" : {
|
|
"3DTILES_batch_table_hierarchy" : {
|
|
"classes" : [
|
|
{
|
|
"name" : "Lamp",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"lampStrength" : [10, 5, 7],
|
|
"lampColor" : ["yellow", "white", "white"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Car",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"carType" : ["truck", "bus", "sedan"],
|
|
"carColor" : ["green", "blue", "red"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Tree",
|
|
"length" : 2,
|
|
"instances" : {
|
|
"treeHeight" : [10, 15],
|
|
"treeAge" : [5, 8]
|
|
}
|
|
}
|
|
],
|
|
"instancesLength" : 8,
|
|
"classIds" : [0, 0, 0, 1, 1, 1, 2, 2]
|
|
}
|
|
}
|
|
}
|
|
)";
|
|
|
|
rapidjson::Document featureTableParsed;
|
|
featureTableParsed.Parse(featureTableJson.data(), featureTableJson.size());
|
|
|
|
rapidjson::Document batchTableParsed;
|
|
batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableParsed,
|
|
batchTableParsed,
|
|
std::span<const std::byte>(),
|
|
gltf);
|
|
|
|
ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 6);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 6);
|
|
|
|
struct ExpectedString {
|
|
std::string name;
|
|
std::vector<std::string> values;
|
|
std::optional<std::string> noDataValue;
|
|
};
|
|
|
|
struct ExpectedScalar {
|
|
std::string name;
|
|
std::vector<int8_t> values;
|
|
std::optional<int8_t> noDataValue;
|
|
};
|
|
|
|
std::vector<ExpectedScalar> expectedScalar{
|
|
{"lampStrength", {10, 5, 7, 0, 0, 0, 0, 0}, static_cast<int8_t>(0)},
|
|
{"treeHeight", {0, 0, 0, 0, 0, 0, 10, 15}, static_cast<int8_t>(0)},
|
|
{"treeAge", {0, 0, 0, 0, 0, 0, 5, 8}, static_cast<int8_t>(0)}};
|
|
|
|
std::vector<ExpectedString> expectedString{
|
|
{"lampColor",
|
|
{"yellow", "white", "white", "null", "null", "null", "null", "null"},
|
|
"null"},
|
|
{"carType",
|
|
{"null", "null", "null", "truck", "bus", "sedan", "null", "null"},
|
|
"null"},
|
|
{"carColor",
|
|
{"null", "null", "null", "green", "blue", "red", "null", "null"},
|
|
"null"}};
|
|
|
|
for (const auto& expected : expectedScalar) {
|
|
auto it = defaultClass.properties.find(expected.name);
|
|
REQUIRE(it != defaultClass.properties.end());
|
|
CHECK(it->second.type == ClassProperty::Type::SCALAR);
|
|
CHECK(it->second.componentType == ClassProperty::ComponentType::INT8);
|
|
|
|
checkNonArrayProperty<int8_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
expected.name,
|
|
ClassProperty::Type::SCALAR,
|
|
ClassProperty::ComponentType::INT8,
|
|
expected.values,
|
|
expected.values.size(),
|
|
expected.noDataValue);
|
|
}
|
|
|
|
for (const auto& expected : expectedString) {
|
|
auto it = defaultClass.properties.find(expected.name);
|
|
REQUIRE(it != defaultClass.properties.end());
|
|
CHECK(it->second.type == ClassProperty::Type::STRING);
|
|
|
|
checkNonArrayProperty<std::string, std::string_view>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
expected.name,
|
|
ClassProperty::Type::STRING,
|
|
std::nullopt,
|
|
expected.values,
|
|
expected.values.size(),
|
|
expected.noDataValue);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Omits value-less properties when converting "
|
|
"3DTILES_batch_table_hierarchy to EXT_structural_metadata") {
|
|
Model gltf;
|
|
|
|
std::string featureTableJson = R"(
|
|
{
|
|
"BATCH_LENGTH": 8
|
|
}
|
|
)";
|
|
|
|
// "Feature classes" example from the spec:
|
|
// https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy#feature-classes
|
|
std::string batchTableJson = R"(
|
|
{
|
|
"extensions" : {
|
|
"3DTILES_batch_table_hierarchy" : {
|
|
"classes" : [
|
|
{
|
|
"name" : "Lamp",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"lampStrength" : [10, 5, 7],
|
|
"lampColor" : ["yellow", "white", "white"],
|
|
"missingValues": []
|
|
}
|
|
},
|
|
{
|
|
"name" : "Car",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"carType" : ["truck", "bus", "sedan"],
|
|
"carColor" : ["green", "blue", "red"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Tree",
|
|
"length" : 2,
|
|
"instances" : {
|
|
"treeHeight" : [10, 15],
|
|
"treeAge" : [5, 8]
|
|
}
|
|
}
|
|
],
|
|
"instancesLength" : 8,
|
|
"classIds" : [0, 0, 0, 1, 1, 1, 2, 2]
|
|
}
|
|
}
|
|
}
|
|
)";
|
|
|
|
rapidjson::Document featureTableParsed;
|
|
featureTableParsed.Parse(featureTableJson.data(), featureTableJson.size());
|
|
|
|
rapidjson::Document batchTableParsed;
|
|
batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size());
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableParsed,
|
|
batchTableParsed,
|
|
std::span<const std::byte>(),
|
|
gltf);
|
|
|
|
ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 7);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
|
|
// Verify that all property table properties refer to a valid bufferView.
|
|
for (const std::pair<const std::string, PropertyTableProperty>& pair :
|
|
propertyTable.properties) {
|
|
CHECK(pair.second.values >= 0);
|
|
CHECK(size_t(pair.second.values) < gltf.bufferViews.size());
|
|
}
|
|
|
|
CHECK(
|
|
propertyTable.properties.find("missingValues") ==
|
|
propertyTable.properties.end());
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Converts \"Feature Hierarchy\" 3DTILES_batch_table_hierarchy example to "
|
|
"EXT_structural_metadata") {
|
|
Model gltf;
|
|
|
|
std::string featureTableJson = R"(
|
|
{
|
|
"BATCH_LENGTH": 6
|
|
}
|
|
)";
|
|
|
|
// "Feature hierarchy" example from the spec:
|
|
// https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy#feature-hierarchy
|
|
std::string batchTableJson = R"(
|
|
{
|
|
"extensions" : {
|
|
"3DTILES_batch_table_hierarchy" : {
|
|
"classes" : [
|
|
{
|
|
"name" : "Wall",
|
|
"length" : 6,
|
|
"instances" : {
|
|
"wall_color" : ["blue", "pink", "green", "lime", "black",
|
|
"brown"], "wall_windows" : [2, 4, 4, 2, 0, 3]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Building",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"building_name" : ["building_0", "building_1",
|
|
"building_2"], "building_id" : [0, 1, 2], "building_address"
|
|
: ["10 Main St", "12 Main St", "14 Main St"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Block",
|
|
"length" : 1,
|
|
"instances" : {
|
|
"block_lat_long" : [[0.12, 0.543]],
|
|
"block_district" : ["central"]
|
|
}
|
|
}
|
|
],
|
|
"instancesLength" : 10,
|
|
"classIds" : [0, 0, 0, 0, 0, 0, 1, 1, 1, 2],
|
|
"parentIds" : [6, 6, 7, 7, 8, 8, 9, 9, 9, 9]
|
|
}
|
|
}
|
|
}
|
|
)";
|
|
|
|
rapidjson::Document featureTableParsed;
|
|
featureTableParsed.Parse(featureTableJson.data(), featureTableJson.size());
|
|
|
|
rapidjson::Document batchTableParsed;
|
|
batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size());
|
|
|
|
BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableParsed,
|
|
batchTableParsed,
|
|
std::span<const std::byte>(),
|
|
gltf);
|
|
|
|
ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 7);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 7);
|
|
|
|
struct ExpectedString {
|
|
std::string name;
|
|
std::vector<std::string> values;
|
|
std::string type() const { return ClassProperty::Type::STRING; }
|
|
std::optional<std::string> componentType() const { return std::nullopt; }
|
|
};
|
|
|
|
std::vector<ExpectedString> expectedStringProperties{
|
|
{"wall_color", {"blue", "pink", "green", "lime", "black", "brown"}},
|
|
{"building_name",
|
|
{"building_0",
|
|
"building_0",
|
|
"building_1",
|
|
"building_1",
|
|
"building_2",
|
|
"building_2"}},
|
|
{"building_address",
|
|
{"10 Main St",
|
|
"10 Main St",
|
|
"12 Main St",
|
|
"12 Main St",
|
|
"14 Main St",
|
|
"14 Main St"}},
|
|
{"block_district",
|
|
{"central", "central", "central", "central", "central", "central"}}};
|
|
|
|
for (const auto& expected : expectedStringProperties) {
|
|
auto it = defaultClass.properties.find(expected.name);
|
|
REQUIRE(it != defaultClass.properties.end());
|
|
CHECK(it->second.type == expected.type());
|
|
CHECK(it->second.componentType == expected.componentType());
|
|
|
|
checkNonArrayProperty<std::string, std::string_view>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
expected.name,
|
|
expected.type(),
|
|
expected.componentType(),
|
|
expected.values,
|
|
expected.values.size());
|
|
}
|
|
|
|
struct ExpectedInt8Properties {
|
|
std::string name;
|
|
std::vector<int8_t> values;
|
|
std::string type() const { return ClassProperty::Type::SCALAR; }
|
|
std::optional<std::string> componentType() const {
|
|
return ClassProperty::ComponentType::INT8;
|
|
}
|
|
};
|
|
|
|
std::vector<ExpectedInt8Properties> expectedInt8Properties{
|
|
{"wall_windows", {2, 4, 4, 2, 0, 3}},
|
|
{"building_id", {0, 0, 1, 1, 2, 2}},
|
|
};
|
|
|
|
for (const auto& expected : expectedInt8Properties) {
|
|
auto it = defaultClass.properties.find(expected.name);
|
|
REQUIRE(it != defaultClass.properties.end());
|
|
CHECK(it->second.type == expected.type());
|
|
CHECK(it->second.componentType == expected.componentType());
|
|
|
|
checkNonArrayProperty<int8_t>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
expected.name,
|
|
expected.type(),
|
|
expected.componentType(),
|
|
expected.values,
|
|
expected.values.size());
|
|
}
|
|
|
|
struct ExpectedDoubleArrayProperties {
|
|
std::string name;
|
|
int64_t count;
|
|
std::vector<std::vector<double>> values;
|
|
std::string type() const { return ClassProperty::Type::SCALAR; }
|
|
std::optional<std::string> componentType() const {
|
|
return ClassProperty::ComponentType::FLOAT64;
|
|
}
|
|
};
|
|
|
|
std::vector<ExpectedDoubleArrayProperties> expectedDoubleArrayProperties{
|
|
{"block_lat_long",
|
|
2,
|
|
{{0.12, 0.543},
|
|
{0.12, 0.543},
|
|
{0.12, 0.543},
|
|
{0.12, 0.543},
|
|
{0.12, 0.543},
|
|
{0.12, 0.543}}}};
|
|
|
|
for (const auto& expected : expectedDoubleArrayProperties) {
|
|
auto it = defaultClass.properties.find(expected.name);
|
|
REQUIRE(it != defaultClass.properties.end());
|
|
CHECK(it->second.type == expected.type());
|
|
CHECK(it->second.componentType == expected.componentType());
|
|
CHECK(it->second.array);
|
|
CHECK(it->second.count == expected.count);
|
|
|
|
checkArrayProperty<double>(
|
|
gltf,
|
|
propertyTable,
|
|
defaultClass,
|
|
expected.name,
|
|
expected.count,
|
|
expected.type(),
|
|
expected.componentType(),
|
|
expected.values,
|
|
expected.values.size());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("3DTILES_batch_table_hierarchy with parentCounts is okay if all "
|
|
"values are 1") {
|
|
Model gltf;
|
|
|
|
std::string featureTableJson = R"(
|
|
{
|
|
"BATCH_LENGTH": 3
|
|
}
|
|
)";
|
|
|
|
// "Feature hierarchy" example from the spec:
|
|
// https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy#feature-hierarchy
|
|
std::string batchTableJson = R"(
|
|
{
|
|
"extensions" : {
|
|
"3DTILES_batch_table_hierarchy" : {
|
|
"classes" : [
|
|
{
|
|
"name" : "Parent1",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"some_property" : ["a", "b", "c"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Parent2",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"another_property" : ["d", "e", "f"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Main",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"third" : [1, 2, 3]
|
|
}
|
|
}
|
|
],
|
|
"instancesLength" : 5,
|
|
"classIds" : [2, 2, 2, 0, 1],
|
|
"parentCounts": [1, 1, 1, 1, 1],
|
|
"parentIds" : [3, 3, 3, 4, 4]
|
|
}
|
|
}
|
|
}
|
|
)";
|
|
|
|
rapidjson::Document featureTableParsed;
|
|
featureTableParsed.Parse(featureTableJson.data(), featureTableJson.size());
|
|
|
|
rapidjson::Document batchTableParsed;
|
|
batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size());
|
|
|
|
auto pLog = std::make_shared<spdlog::sinks::ringbuffer_sink_mt>(3);
|
|
spdlog::default_logger()->sinks().emplace_back(pLog);
|
|
|
|
BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableParsed,
|
|
batchTableParsed,
|
|
std::span<const std::byte>(),
|
|
gltf);
|
|
|
|
// There should not be any log messages about parentCounts, since they're
|
|
// all 1.
|
|
std::vector<std::string> logMessages = pLog->last_formatted();
|
|
REQUIRE(logMessages.size() == 0);
|
|
|
|
// There should actually be metadata properties as normal.
|
|
const ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
const Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 3);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 3);
|
|
}
|
|
|
|
TEST_CASE("3DTILES_batch_table_hierarchy with parentCounts values != 1 is "
|
|
"unsupported") {
|
|
Model gltf;
|
|
|
|
std::string featureTableJson = R"(
|
|
{
|
|
"BATCH_LENGTH": 3
|
|
}
|
|
)";
|
|
|
|
// "Feature hierarchy" example from the spec:
|
|
// https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy#feature-hierarchy
|
|
std::string batchTableJson = R"(
|
|
{
|
|
"extensions" : {
|
|
"3DTILES_batch_table_hierarchy" : {
|
|
"classes" : [
|
|
{
|
|
"name" : "Parent1",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"some_property" : ["a", "b", "c"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Parent2",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"another_property" : ["d", "e", "f"]
|
|
}
|
|
},
|
|
{
|
|
"name" : "Main",
|
|
"length" : 3,
|
|
"instances" : {
|
|
"third" : [1, 2, 3]
|
|
}
|
|
}
|
|
],
|
|
"instancesLength" : 5,
|
|
"classIds" : [2, 2, 2, 0, 1],
|
|
"parentCounts": [2, 2, 2, 1, 1],
|
|
"parentIds" : [3, 4, 3, 4, 3, 4, 3, 4]
|
|
}
|
|
}
|
|
}
|
|
)";
|
|
|
|
rapidjson::Document featureTableParsed;
|
|
featureTableParsed.Parse(featureTableJson.data(), featureTableJson.size());
|
|
|
|
rapidjson::Document batchTableParsed;
|
|
batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size());
|
|
|
|
auto pLog = std::make_shared<spdlog::sinks::ringbuffer_sink_mt>(3);
|
|
spdlog::default_logger()->sinks().emplace_back(pLog);
|
|
|
|
auto errors = BatchTableToGltfStructuralMetadata::convertFromB3dm(
|
|
featureTableParsed,
|
|
batchTableParsed,
|
|
std::span<const std::byte>(),
|
|
gltf);
|
|
|
|
// There should be a log message about parentCounts, and no properties.
|
|
const std::vector<std::string>& logMessages = errors.warnings;
|
|
REQUIRE(logMessages.size() == 1);
|
|
CHECK(logMessages[0].find("parentCounts") != std::string::npos);
|
|
|
|
const ExtensionModelExtStructuralMetadata* pExtension =
|
|
gltf.getExtension<ExtensionModelExtStructuralMetadata>();
|
|
REQUIRE(pExtension);
|
|
CHECK(
|
|
gltf.isExtensionUsed(ExtensionModelExtStructuralMetadata::ExtensionName));
|
|
|
|
// Check the schema
|
|
REQUIRE(pExtension->schema);
|
|
REQUIRE(pExtension->schema->classes.size() == 1);
|
|
|
|
auto firstClassIt = pExtension->schema->classes.begin();
|
|
CHECK(firstClassIt->first == "default");
|
|
|
|
const Class& defaultClass = firstClassIt->second;
|
|
REQUIRE(defaultClass.properties.size() == 0);
|
|
|
|
// Check the property table
|
|
REQUIRE(pExtension->propertyTables.size() == 1);
|
|
const PropertyTable& propertyTable = pExtension->propertyTables[0];
|
|
|
|
CHECK(propertyTable.classProperty == "default");
|
|
REQUIRE(propertyTable.properties.size() == 0);
|
|
}
|