mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
ifcviewer: cache CoordinateOperation in sidecar (v10 -> v11)
Previously, applyCoordinateOperationToViewport — which pushes both
CoordinateOperation and ModelTransformation — was only called on
paths that required the IFC source to be loaded
(onLoadedFromStream and onDataSourceReady). Sidecar-only loads
(loadDataSource off, or no .ifc/.rdb sibling) silently lost both
stages.
Cache the per-model georef + unit scales in the sidecar itself so
the IFC source isn't needed to apply them:
SidecarData gains
coordinate_operation_meters[16] // column-major
project_length_to_meters
map_unit_to_meters
has_coordinate_operation
148 B fixed block written/read between instances and elements.
SIDECAR_VERSION 10 -> 11; existing sidecars rebuild on next load.
MainWindow::writeSidecarForModel populates the block from
loader_->modelGeoref(mid) before writeSidecar.
SceneLoader::applySidecarData restores it into the model's
ModelGeoref + sets has_georef = true, so subsequent
loader_->modelGeoref(mid) calls return the cached data without
needing the IFC.
MainWindow::onLoadedFromSidecar now calls
applyCoordinateOperationToViewport(mid) directly — both
CoordinateOperation and ModelTransformation land at sidecar-load
time, no longer waiting on a possibly-never-arriving data source.
Edits to the IFC's IfcMapConversion don't invalidate the cache —
delete the .ifcview manually if the source's georef changes. This
matches the existing cache-invalidation contract.
Tests: round-trip the new fields through the existing sidecar
fixture; assert SIDECAR_VERSION == 11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ElementInfo> 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<Eigen::Matrix<double, 4, 4, Eigen::ColMajor>>(
|
||||
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;
|
||||
|
||||
@@ -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<const Eigen::Matrix<double, 4, 4, Eigen::ColMajor>> 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<PackedElementInfo> elements = std::move(data.elements);
|
||||
std::string stbl = std::move(data.string_table);
|
||||
|
||||
|
||||
@@ -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<uint32_t>(data.string_table.size());
|
||||
@@ -123,6 +138,16 @@ std::optional<SidecarData> 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;
|
||||
|
||||
@@ -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<MeshInfo> meshes; // indexed by local_mesh_id
|
||||
std::vector<InstanceCpu> 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<PackedElementInfo> elements;
|
||||
std::string string_table;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user