diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 3503072e77..7e261eaf3d 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -594,12 +594,18 @@ void MainWindow::applyFederatedFalseOriginToViewport() { viewport_->setFederatedFalseOrigin(M); } -void MainWindow::onLoadedFromSidecar(uint32_t /*mid*/, qint64 elapsed_ms) { +void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) { progress_bar_->setVisible(false); status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3") .arg(element_map_.size()) .arg(loader_->modelCount()) .arg(formatElapsed(elapsed_ms))); + + // Sidecar v11+ caches the CoordinateOperation, so SceneLoader has + // already populated modelGeoref by now — push CoordinateOperation + + // ModelTransformation immediately rather than waiting for the + // (possibly never-arriving) data-source load. + applyCoordinateOperationToViewport(mid); } void MainWindow::onStreamedElementsReady(uint32_t /*mid*/, std::vector elements) { @@ -615,6 +621,16 @@ void MainWindow::writeSidecarForModel(uint32_t mid) { SidecarData sd; if (!viewport_->snapshotModel(mid, sd)) return; + // Cache the model's CoordinateOperation alongside the geometry so a + // sidecar load doesn't need the IFC source just to apply georef. + if (const ModelGeoref* gr = loader_->modelGeoref(mid)) { + sd.has_coordinate_operation = gr->has_coordinate_operation ? 1 : 0; + Eigen::Map>( + sd.coordinate_operation_meters) = gr->coordinate_operation_meters; + sd.project_length_to_meters = gr->units.project_length_to_meters; + sd.map_unit_to_meters = gr->units.map_unit_to_meters; + } + for (const auto& [oid, info] : element_map_) { if (info.model_id != mid) continue; PackedElementInfo pe; diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index d455615376..e8184ff7fd 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -199,6 +199,21 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { inst.model_id = mid; } + // Restore the cached CoordinateOperation into the model so + // modelGeoref(mid) returns it without needing the IFC source. Prevents + // sidecar-loaded models from silently losing their georef when the + // .ifc/.rdb sibling is absent or AppSettings.loadDataSource is off. + { + ModelGeoref& gr = model.georef; + gr.has_coordinate_operation = data.has_coordinate_operation != 0; + Eigen::Map> M( + data.coordinate_operation_meters); + gr.coordinate_operation_meters = M; + gr.units.project_length_to_meters = data.project_length_to_meters; + gr.units.map_unit_to_meters = data.map_unit_to_meters; + model.has_georef = true; + } + std::vector elements = std::move(data.elements); std::string stbl = std::move(data.string_table); diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index b603dce354..86951f7395 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -17,7 +17,7 @@ * * ********************************************************************************/ -// v9 layout (all multi-byte fields native-endian; endianness marker in header). +// v11 layout (all multi-byte fields native-endian; endianness marker in header). // // SidecarHeader (12 bytes) // @@ -30,7 +30,12 @@ // MeshInfo[num_meshes] // // uint32_t num_instances -// InstanceCpu[num_instances] (already sorted by mesh_id) +// InstanceCpu[num_instances] (already sorted by mesh_id; v10 layout) +// +// uint32_t has_coordinate_operation (v11+) +// double[16] coordinate_operation_meters (v11+; column-major) +// double project_length_to_meters (v11+) +// double map_unit_to_meters (v11+) // // uint32_t num_elements // PackedElementInfo[num_elements] @@ -93,6 +98,16 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { if (!writeVec(f, data.indices)) { fclose(f); return false; } if (!writeVec(f, data.meshes)) { fclose(f); return false; } if (!writeVec(f, data.instances)) { fclose(f); return false; } + + // v11 georef block (148 B). + if (fwrite(&data.has_coordinate_operation, 4, 1, f) != 1) { fclose(f); return false; } + if (fwrite(data.coordinate_operation_meters, + sizeof(double), 16, f) != 16) { fclose(f); return false; } + if (fwrite(&data.project_length_to_meters, + sizeof(double), 1, f) != 1) { fclose(f); return false; } + if (fwrite(&data.map_unit_to_meters, + sizeof(double), 1, f) != 1) { fclose(f); return false; } + if (!writeVec(f, data.elements)) { fclose(f); return false; } uint32_t stbl_len = static_cast(data.string_table.size()); @@ -123,6 +138,16 @@ std::optional readSidecar(const std::string& ifc_path) { if (!readVec(f, data.indices)) return fail(); if (!readVec(f, data.meshes)) return fail(); if (!readVec(f, data.instances)) return fail(); + + // v11 georef block. + if (fread(&data.has_coordinate_operation, 4, 1, f) != 1) return fail(); + if (fread(data.coordinate_operation_meters, + sizeof(double), 16, f) != 16) return fail(); + if (fread(&data.project_length_to_meters, + sizeof(double), 1, f) != 1) return fail(); + if (fread(&data.map_unit_to_meters, + sizeof(double), 1, f) != 1) return fail(); + if (!readVec(f, data.elements)) return fail(); uint32_t stbl_len; diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index 916a42acb1..ac33e200de 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -53,7 +53,13 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" // result. Sidecar serialises both; on load the transform is recomputed // from placement_transformation + the ViewportWindow's current stage // matrices, so v10 sidecars are reusable across .ifcfeds. -static constexpr uint32_t SIDECAR_VERSION = 10; +// v11 = SidecarData gains a per-model CoordinateOperation cache: +// coordinate_operation_meters[16] (column-major), unit scales, and a +// has_coordinate_operation flag. Lets sidecar-loaded models apply +// georef without re-parsing the IFC source. Edits to the IFC's +// IfcMapConversion do NOT invalidate the sidecar — delete the +// .ifcview manually if you change the source's georef parameters. +static constexpr uint32_t SIDECAR_VERSION = 11; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; // Fixed-size element record. Strings are stored as (offset, length) pairs @@ -83,6 +89,19 @@ struct SidecarData { std::vector meshes; // indexed by local_mesh_id std::vector instances; // sorted by mesh_id + // CoordinateOperation cache (v11+). Mirrors ModelGeoref so a sidecar + // load can apply georef without re-parsing the IFC source. + // has_coordinate_operation == 0 means the model has no + // IfcMapConversion; the matrix is then the identity placeholder. + double coordinate_operation_meters[16] = { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 }; + double project_length_to_meters = 1.0; + double map_unit_to_meters = 1.0; + uint32_t has_coordinate_operation = 0; + // Element tree metadata. std::vector elements; std::string string_table; diff --git a/src/ifcviewer/tests/test_sidecar_cache.cpp b/src/ifcviewer/tests/test_sidecar_cache.cpp index 978305db53..f592a37772 100644 --- a/src/ifcviewer/tests/test_sidecar_cache.cpp +++ b/src/ifcviewer/tests/test_sidecar_cache.cpp @@ -88,7 +88,10 @@ SidecarData buildFixture() { 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.transform[k] = float(i) * 0.5f + float(k); + for (int k = 0; k < 16; ++k) { + inst.placement_transformation[k] = float(i) * 0.25f + float(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); @@ -97,6 +100,12 @@ SidecarData buildFixture() { 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) { @@ -129,6 +138,15 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) { 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; } @@ -137,7 +155,7 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) { TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") { REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(InstanceGpu) == 80); - REQUIRE(SIDECAR_VERSION == 9); + REQUIRE(SIDECAR_VERSION == 11); REQUIRE(SIDECAR_MAGIC == 0x49465657u); }