/******************************************************************************** * * * This file is part of IfcOpenShell. * * * * IfcOpenShell is free software: you can redistribute it and/or modify * * it under the terms of the Lesser GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * * IfcOpenShell is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * Lesser GNU General Public License for more details. * * * * You should have received a copy of the Lesser GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ #include "InstancedGeometry.h" #include "SidecarCache.h" #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace { // Each test creates its own scratch directory under the OS tmp root so they // can run in parallel without colliding on file paths. fs::path makeScratchDir(const char* tag) { fs::path base = fs::temp_directory_path() / "ifcviewer_test_sidecar"; fs::create_directories(base); static std::atomic counter{0}; auto unique = std::to_string(counter.fetch_add(1)) + "_" + tag; fs::path dir = base / unique; fs::create_directories(dir); return dir; } SidecarData buildFixture() { SidecarData sd; // 4 vertices worth of arbitrary bytes (12 B/vertex). sd.vertices.resize(4 * INSTANCED_VERTEX_STRIDE_BYTES); for (size_t i = 0; i < sd.vertices.size(); ++i) sd.vertices[i] = uint8_t(i * 7); // Two meshes share the VBO — second mesh starts at vertex 2. sd.indices = {0, 1, 2, 1, 2, 3}; MeshInfo m1{}; m1.vbo_byte_offset = 0; m1.vertex_count = 2; m1.ebo_byte_offset = 0; m1.index_count = 3; m1.local_aabb_min[0] = -1; m1.local_aabb_min[1] = -2; m1.local_aabb_min[2] = -3; m1.local_aabb_max[0] = 4; m1.local_aabb_max[1] = 5; m1.local_aabb_max[2] = 6; m1.first_instance = 0; m1.instance_count = 3; m1.lod1_ebo_byte_offset = 0; m1.lod1_index_count = 0; MeshInfo m2{}; m2.vbo_byte_offset = 2 * INSTANCED_VERTEX_STRIDE_BYTES; m2.vertex_count = 2; m2.ebo_byte_offset = 3 * sizeof(uint32_t); m2.index_count = 3; m2.local_aabb_min[0] = 10; m2.local_aabb_min[1] = 11; m2.local_aabb_min[2] = 12; m2.local_aabb_max[0] = 13; m2.local_aabb_max[1] = 14; m2.local_aabb_max[2] = 15; m2.first_instance = 3; m2.instance_count = 2; m2.lod1_ebo_byte_offset = 0; m2.lod1_index_count = 0; sd.meshes = {m1, m2}; sd.instances.resize(5); for (size_t i = 0; i < sd.instances.size(); ++i) { InstanceCpu& inst = sd.instances[i]; inst.mesh_id = (i < 3) ? 0u : 1u; inst.object_id = uint32_t(100 + i); inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u)); inst.model_id = 1; for (int k = 0; k < 16; ++k) { inst.placement_transformation[k] = double(i) * 0.25 + double(k); inst.transform[k] = float(i) * 0.5f + float(k); } inst.world_aabb_min[0] = float(i); inst.world_aabb_min[1] = float(i + 1); inst.world_aabb_min[2] = float(i + 2); inst.world_aabb_max[0] = float(i) + 10.0f; inst.world_aabb_max[1] = float(i + 1) + 10.0f; inst.world_aabb_max[2] = float(i + 2) + 10.0f; } // Non-default georef block. sd.has_coordinate_operation = 1; for (int k = 0; k < 16; ++k) sd.coordinate_operation_meters[k] = 0.5 + 0.1 * k; sd.project_length_to_meters = 0.001; // mm project sd.map_unit_to_meters = 1.0; // metres map sd.string_table = std::string("\0Wall\0Slab\0", 11); // includes embedded NULs sd.elements.resize(3); for (size_t i = 0; i < sd.elements.size(); ++i) { PackedElementInfo& e = sd.elements[i]; e.object_id = uint32_t(100 + i); e.model_id = 1; e.ifc_id = int32_t(1000 + i); e.parent_id = (i == 0) ? -1 : int32_t(100); e.guid_offset = 0; e.guid_length = 0; e.name_offset = 1; e.name_length = 4; // "Wall" e.type_offset = 6; e.type_length = 4; // "Slab" } return sd; } bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) { if (a.vertices != b.vertices) return false; if (a.indices != b.indices) return false; if (a.meshes.size() != b.meshes.size()) return false; if (a.instances.size() != b.instances.size()) return false; if (a.elements.size() != b.elements.size()) return false; if (a.string_table != b.string_table) return false; for (size_t i = 0; i < a.meshes.size(); ++i) { if (std::memcmp(&a.meshes[i], &b.meshes[i], sizeof(MeshInfo)) != 0) return false; } for (size_t i = 0; i < a.instances.size(); ++i) { if (std::memcmp(&a.instances[i], &b.instances[i], sizeof(InstanceCpu)) != 0) return false; } for (size_t i = 0; i < a.elements.size(); ++i) { if (std::memcmp(&a.elements[i], &b.elements[i], sizeof(PackedElementInfo)) != 0) return false; } // v11 georef block. if (a.has_coordinate_operation != b.has_coordinate_operation) return false; if (a.project_length_to_meters != b.project_length_to_meters) return false; if (a.map_unit_to_meters != b.map_unit_to_meters) return false; for (int i = 0; i < 16; ++i) { if (a.coordinate_operation_meters[i] != b.coordinate_operation_meters[i]) return false; } return true; } } // namespace TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") { REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(InstanceGpu) == 80); REQUIRE(SIDECAR_VERSION == 13); REQUIRE(SIDECAR_MAGIC == 0x49465657u); } TEST_CASE("writeSidecar then readSidecar round-trips the full fixture", "[sidecar]") { fs::path dir = makeScratchDir("roundtrip"); fs::path ifc = dir / "model.ifc"; fs::path expected = dir / "model.ifcview"; SidecarData original = buildFixture(); REQUIRE(writeSidecar(ifc.string(), original)); REQUIRE(fs::exists(expected)); auto loaded = readSidecar(ifc.string()); REQUIRE(loaded.has_value()); REQUIRE(sidecarDataEqual(original, *loaded)); } TEST_CASE("readSidecar returns nullopt when the sidecar is missing", "[sidecar]") { fs::path dir = makeScratchDir("missing"); fs::path ifc = dir / "absent.ifc"; auto loaded = readSidecar(ifc.string()); REQUIRE_FALSE(loaded.has_value()); } TEST_CASE("readSidecar rejects a truncated header", "[sidecar]") { fs::path dir = makeScratchDir("truncated"); fs::path ifc = dir / "bad.ifc"; fs::path bad = dir / "bad.ifcview"; { FILE* f = std::fopen(bad.string().c_str(), "wb"); REQUIRE(f); const char junk[] = "X"; std::fwrite(junk, 1, sizeof(junk), f); std::fclose(f); } auto loaded = readSidecar(ifc.string()); REQUIRE_FALSE(loaded.has_value()); } TEST_CASE("readSidecar rejects a wrong magic / version", "[sidecar]") { fs::path dir = makeScratchDir("wrongver"); fs::path ifc = dir / "old.ifc"; fs::path old = dir / "old.ifcview"; struct Hdr { uint32_t magic, version, endian; } h{ SIDECAR_MAGIC, SIDECAR_VERSION - 1, SIDECAR_ENDIAN }; { FILE* f = std::fopen(old.string().c_str(), "wb"); REQUIRE(f); std::fwrite(&h, sizeof(h), 1, f); // Write zeroed payload so the failure must come from the header check. uint32_t zero = 0; for (int i = 0; i < 6; ++i) std::fwrite(&zero, 4, 1, f); std::fclose(f); } auto loaded = readSidecar(ifc.string()); REQUIRE_FALSE(loaded.has_value()); } TEST_CASE("Empty SidecarData round-trips cleanly", "[sidecar]") { fs::path dir = makeScratchDir("empty"); fs::path ifc = dir / "empty.ifc"; SidecarData empty; REQUIRE(writeSidecar(ifc.string(), empty)); auto loaded = readSidecar(ifc.string()); REQUIRE(loaded.has_value()); REQUIRE(loaded->vertices.empty()); REQUIRE(loaded->indices.empty()); REQUIRE(loaded->meshes.empty()); REQUIRE(loaded->instances.empty()); REQUIRE(loaded->elements.empty()); REQUIRE(loaded->string_table.empty()); } TEST_CASE("Sidecar path stem maps .ifc / .ifcdb / extensionless to .ifcview", "[sidecar]") { // The mapping is internal but observable: writing under one source name // must be readable under any other name that maps to the same stem. fs::path dir = makeScratchDir("stems"); SidecarData sd = buildFixture(); fs::path ifc_path = dir / "shared.ifc"; fs::path ifcdb_path = dir / "shared.ifcdb"; fs::path ifcdb_slash = dir / "shared.ifcdb/"; fs::path noext_path = dir / "shared"; REQUIRE(writeSidecar(ifc_path.string(), sd)); REQUIRE(fs::exists(dir / "shared.ifcview")); auto a = readSidecar(ifcdb_path.string()); auto b = readSidecar(ifcdb_slash.string()); auto c = readSidecar(noext_path.string()); REQUIRE(a.has_value()); REQUIRE(b.has_value()); REQUIRE(c.has_value()); REQUIRE(sidecarDataEqual(sd, *a)); REQUIRE(sidecarDataEqual(sd, *b)); REQUIRE(sidecarDataEqual(sd, *c)); }