diff --git a/src/bonsaiviewer/modules/models/Commands.cpp b/src/bonsaiviewer/modules/models/Commands.cpp index 517c8942dc..6496c1b200 100644 --- a/src/bonsaiviewer/modules/models/Commands.cpp +++ b/src/bonsaiviewer/modules/models/Commands.cpp @@ -61,6 +61,24 @@ #include +namespace bonsaiviewer::modules::models { + +namespace { +bool should_guess_federated_false_origin_ = false; +} // namespace + +void armFederatedFalseOriginGuess() { + should_guess_federated_false_origin_ = true; +} + +bool consumeFederatedFalseOriginGuess() { + const bool armed = should_guess_federated_false_origin_; + should_guess_federated_false_origin_ = false; + return armed; +} + +} // namespace bonsaiviewer::modules::models + namespace bonsaiviewer::modules::models::commands { namespace { @@ -247,6 +265,15 @@ void addModel(SessionState& s, QWidget& host) { return; } + // Arm the false-origin guess if we're adding into a session with no + // models yet — the first model that finishes loading will set the + // origin via ViewportView. Checked here (before federation->addModel) + // because federation->addModel doesn't yet populate SessionState's + // model mapping; modelIds() reflects pre-add state at this point. + if (s.modelIds().isEmpty()) { + armFederatedFalseOriginGuess(); + } + QStringList accepted_paths; QStringList accepted_fed_ids; for (const auto& path : paths) { @@ -290,6 +317,12 @@ void addModelFromCloud(SessionState& s, QWidget& host) { proc->call("pull_models_interactive", QJsonValue(), [sguard, connector_id](const QJsonValue& result) { if (!sguard) return; + // Arm before the first addCloudModel — modelIds() reflects the + // session state at the moment the connector returns, which is + // when the user's "add into empty session" intent applies. + if (sguard->modelIds().isEmpty()) { + armFederatedFalseOriginGuess(); + } const QJsonArray arr = result.toArray(); QStringList paths; QStringList fed_ids; diff --git a/src/bonsaiviewer/modules/models/Commands.h b/src/bonsaiviewer/modules/models/Commands.h index aeb7f78b4e..42988d9f89 100644 --- a/src/bonsaiviewer/modules/models/Commands.h +++ b/src/bonsaiviewer/modules/models/Commands.h @@ -31,6 +31,23 @@ class QWidget; class WgpuViewportWindow; namespace bonsaiviewer { class SessionState; } +namespace bonsaiviewer::modules::models { + +// Arm/consume pair for "the next model load should auto-guess the +// federation's false origin." Add-model commands arm this when they're +// about to add into an empty session; ViewportView consumes it on the +// next modelGeometryReady. The flag has consume-on-read semantics so +// the API is functions, not a raw bool (a peek-without-clear would +// silently break the one-shot guarantee). +// +// Module-scoped (not on SessionState) because it's add-command intent — +// SessionState shouldn't grow a field for every module's per-command +// state. Lives next to the commands that arm it. +void armFederatedFalseOriginGuess(); +bool consumeFederatedFalseOriginGuess(); + +} // namespace bonsaiviewer::modules::models + namespace bonsaiviewer::modules::models::commands { // User-facing commands. Each one is responsible for emitting any notify() diff --git a/src/bonsaiviewer/modules/viewport/View.cpp b/src/bonsaiviewer/modules/viewport/View.cpp index f52474f52f..8d5cbc4d00 100644 --- a/src/bonsaiviewer/modules/viewport/View.cpp +++ b/src/bonsaiviewer/modules/viewport/View.cpp @@ -22,6 +22,7 @@ #include "../../ViewerSettings.h" #include "../../SessionState.h" +#include "../models/Commands.h" #include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/SceneLoader.h" #include "../../../ifcviewer-wgpu/WgpuViewportWindow.h" @@ -56,13 +57,18 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state, connect(session_state_, &SessionState::modelsChanged, this, &ViewportView::refresh); connect(session_state_, &SessionState::federationChanged, this, &ViewportView::refresh); connect(session_state_, &SessionState::visibilityChanged, this, &ViewportView::refresh); - // modelGeometryReady is the right hook for "first model just finished - // loading" — count is already in modelIds() by the time the geometry - // signal lands. We try to auto-guess the false origin here (and only - // here — refresh() stays terminal) so a slot can't accidentally re-emit - // into itself through federationChanged. + // modelGeometryReady is the right hook for "a model finished loading" + // — by this point the loader has populated firstPlacement/modelGeoref, + // so the guess has the data it needs. Whether to actually guess is + // gated by the arm-flag set by the add-model commands when they're + // adding into an empty session (so batch-adds work too: arm once, + // first geometry-ready consumes the arm). refresh() stays terminal — + // any federation mutation from the guess propagates through + // SessionState's federatedFalseOriginChanged relay. connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t mid) { - tryGuessFirstModelFalseOrigin(mid); + if (modules::models::consumeFederatedFalseOriginGuess()) { + guessFederatedFalseOriginFromFirstModel(mid); + } refresh(); }); @@ -180,25 +186,28 @@ void ViewportView::applyModelVisibility(uint32_t mid) { } } -// Auto-guess the federation's false origin when a model finishes loading, -// scoped to the precise case where it's actually wanted: the just-loaded -// model is the *only* model in the session and the false origin hasn't -// been set yet. Re-firable: if the user removes the model and adds a new -// one, and the previous load's guess didn't change the origin from -// defaults (e.g. first-placement happened to land at the world origin — -// see Holter Tower), the next load will retry. +// Apply the federation false-origin guess from this model's first +// placement. Called only when the add-model command armed the guess +// (i.e. it was adding into an empty session); see the modelGeometryReady +// connection above and modules::models::armFederatedFalseOriginGuess(). // -// This deliberately lives off the refresh() fan-in. refresh() is connected -// to half a dozen signals; calling a federation mutator from inside it -// stack-overflowed BonsaiViewer once the guess returned defaults, because +// The internal guards (filePath, default origin, placement/georef +// presence) are defense-in-depth, not the primary gate — that's the arm. +// Order of checks matters: filePath skip protects project files +// (federation owns origin); current==defaults skip protects a user who +// previously set the origin manually and only later removed the model; +// nullptr placement/georef skip handles loads where the loader hasn't +// surfaced data yet (rare given the modelGeometryReady contract). +// +// Lives off the refresh() fan-in deliberately. refresh() is connected to +// half a dozen signals; calling a federation mutator from inside it +// stack-overflowed BonsaiViewer once the guess returned defaults because // the mutation re-emitted through SessionState → re-entered refresh(). // Guessing only on the modelGeometryReady edge means a Federation -// mutation here propagates through SessionState's normal relay +// mutation here propagates through SessionState's federation relay // (federatedFalseOriginChanged → notifyFederationChanged) without // re-entering this function. -void ViewportView::tryGuessFirstModelFalseOrigin(uint32_t mid) { - if (session_state_->modelIds().size() != 1) return; - +void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t mid) { Federation* federation = session_state_->federation(); if (!federation->filePath().isEmpty()) return; @@ -206,13 +215,26 @@ void ViewportView::tryGuessFirstModelFalseOrigin(uint32_t mid) { const FederatedFalseOrigin defaults; if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return; - SceneLoader* loader = session_state_->loader(); - const Eigen::Matrix4d* placement = loader->firstPlacement(mid); - const ModelGeoref* georef = loader->modelGeoref(mid); - if (placement == nullptr || georef == nullptr) return; + Eigen::Vector3d first_geometry_point_m; + if (!viewport_->firstGeometryPointWorldM(mid, first_geometry_point_m)) return; - federation->setFederatedFalseOrigin(guessFederatedFalseOrigin( - *placement, *georef, federation->config())); + SceneLoader* loader = session_state_->loader(); + const ModelGeoref* georef = loader->modelGeoref(mid); + if (georef == nullptr) return; + + federation->setFederatedFalseOrigin(::guessFederatedFalseOrigin( + first_geometry_point_m, *georef, federation->config())); + + // applyCachedModel's auto-viewAll already framed the camera against + // the pre-shift (surveyor) coordinates. The setFederatedFalseOrigin + // call above propagated through SessionState's federation relay → + // refresh() → viewport->setFederatedFalseOrigin → recomposeAndUpload + // (all synchronous Qt direct connections), which rewrote every + // instance's world AABB to its post-shift position. Re-target on + // (0,0,0) — the federated false origin in render space — capped at + // 100 m so a model with crazy-coord geometry can't pull the camera + // back into nothing. + viewport_->frameOnFederatedOrigin(mid, 100.0f); } void ViewportView::updateVolumeReadout() { diff --git a/src/bonsaiviewer/modules/viewport/View.h b/src/bonsaiviewer/modules/viewport/View.h index 67bf71d983..f8fca48670 100644 --- a/src/bonsaiviewer/modules/viewport/View.h +++ b/src/bonsaiviewer/modules/viewport/View.h @@ -53,7 +53,7 @@ private: void applyCoordinateOperation(uint32_t mid); void applyModelTransformation(uint32_t mid); void applyModelVisibility(uint32_t mid); - void tryGuessFirstModelFalseOrigin(uint32_t mid); + void guessFederatedFalseOriginFromFirstModel(uint32_t mid); void updateVolumeReadout(); bonsaiviewer::SessionState* session_state_ = nullptr; diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 6c02c0fd0e..d504be6ba4 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -1468,6 +1468,92 @@ bool WgpuViewportWindow::findInstance(uint32_t object_id, InstanceLookup& out) c return false; } +bool WgpuViewportWindow::firstGeometryPointWorldM(uint32_t model_id, + Eigen::Vector3d& out) const { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return false; + const WgpuModelGpuData& m = it->second; + if (m.instances.empty()) return false; + + const InstanceCpu& inst0 = m.instances[0]; + if (inst0.mesh_id >= m.meshes.size()) return false; + const MeshInfo& mesh0 = m.meshes[inst0.mesh_id]; + + // Mesh-local AABB centre — a point that's actually on the geometry. + // Using AABB centre (vs. literal vertex 0) gives a centroid-like + // anchor rather than a corner, which is more representative of where + // the mesh "is" for the false-origin guess. + const Eigen::Vector3d local_center_m( + 0.5 * (double(mesh0.local_aabb_min[0]) + double(mesh0.local_aabb_max[0])), + 0.5 * (double(mesh0.local_aabb_min[1]) + double(mesh0.local_aabb_max[1])), + 0.5 * (double(mesh0.local_aabb_min[2]) + double(mesh0.local_aabb_max[2]))); + + // placement_transformation is double[16] column-major in metres, + // pre-CoordinateOperation / FederatedFalseOrigin / ModelTransformation + // (same convention as InstanceLookup above). + using Mat4dCol = Eigen::Matrix; + const Eigen::Matrix4d P = + Eigen::Map(inst0.placement_transformation); + out = (P * local_center_m.homogeneous()).head<3>(); + return true; +} + +void WgpuViewportWindow::frameOnFederatedOrigin(uint32_t model_id, + float max_distance_m) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + const WgpuModelGpuData& m = it->second; + if (m.instances.empty()) return; + + float mn[3] = { std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity() }; + float mx[3] = { -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity() }; + for (const auto& inst : m.instances) { + for (int a = 0; a < 3; ++a) { + mn[a] = std::min(mn[a], inst.world_aabb_min[a]); + mx[a] = std::max(mx[a], inst.world_aabb_max[a]); + } + } + + // The federated false origin sits at (0,0,0) in post-shift space + // by construction (federated_false_origin_meters_ inverts it into + // the instance compose); target it directly so the anchor point + // we used in the guess is dead-centre in the view. + camera_target_[0] = 0.0f; + camera_target_[1] = 0.0f; + camera_target_[2] = 0.0f; + + // Distance: same viewAll() fit math (bounding sphere radius pulled + // just inside the tighter FOV with 1.10 padding), then clamped so + // a model with one crazy-coord outlier vertex doesn't pull the + // camera back so far that the real geometry becomes a pixel. + const float dx = mx[0] - mn[0]; + const float dy = mx[1] - mn[1]; + const float dz = mx[2] - mn[2]; + const float radius = 0.5f * std::sqrt(dx * dx + dy * dy + dz * dz); + if (radius > 1e-4f) { + const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_); + const float tan_half = std::tan(fovy_rad * 0.5f); + if (tan_half > 1e-6f) { + const int h = std::max(configured_h_, 1); + const float aspect = float(std::max(configured_w_, 1)) / float(h); + const float min_aspect = aspect < 1.0f ? aspect : 1.0f; + const float fit_dist = (radius / (tan_half * min_aspect)) * 1.10f; + camera_distance_ = std::clamp(fit_dist, 0.1f, max_distance_m); + } + } + + qInfo().noquote().nospace() + << "[wgpu] frameOnFederatedOrigin model=" << model_id + << " distance=" << camera_distance_ + << " (cap=" << max_distance_m << "m, model radius=" << radius << ")"; + + if (isExposed()) requestUpdate(); +} + void WgpuViewportWindow::flushPendingSidecarQueue() { while (!pending_sidecars_.empty()) { const QString p = pending_sidecars_.front(); @@ -1958,16 +2044,29 @@ void WgpuViewportWindow::configureSurface(int width_px, int height_px) { // ready, may tear on motion. Useful for raw // throughput benchmarking. // Preference order. Override with WGPU_PRESENT_MODE=...; otherwise we - // try Mailbox → FifoRelaxed → Immediate → Fifo and pick the first + // try Mailbox → Immediate → FifoRelaxed → Fifo and pick the first // mode actually advertised by the surface. Asking for a mode that // the backend doesn't list aborts the process (wgpu-native panics // from Rust at wgpuSurfaceConfigure). On Metal in particular only // Fifo + Immediate are exposed today, so a static Mailbox default // crashes there. + // + // Why Immediate sits above FifoRelaxed: Mailbox is the right answer + // for an interactive viewer (vsync-aligned, no tearing, 1-frame + // queue) but a meaningful subset of Linux Vulkan stacks (some + // compositors, some driver/WSI combinations) silently don't expose + // it — see the "[wgpu] surface advertises present modes:" startup + // log. On those stacks, Fifo's 2-3 frame queue doubles input-to- + // photon latency the moment WASD activates, which on a 60 Hz + // display reads as judder during fly-mode mouse-look. Immediate + // can tear but keeps latency at one render-body, which preserves + // the responsive-feel that's the main reason to use a wgpu viewer. + // FifoRelaxed is the middle option — better latency than Fifo at + // the edge, can tear when over budget — kept as the next fallback. WGPUPresentMode preferred[4] = { WGPUPresentMode_Mailbox, - WGPUPresentMode_FifoRelaxed, WGPUPresentMode_Immediate, + WGPUPresentMode_FifoRelaxed, WGPUPresentMode_Fifo, }; const char* pm_name = "mailbox"; @@ -1999,6 +2098,30 @@ void WgpuViewportWindow::configureSurface(int width_px, int height_px) { } return false; }; + + // Log the full advertised set on first configure. Diagnostic for + // "WGPU_PRESENT_MODE=mailbox falls back to fifo" — if Mailbox is + // missing here, the driver/compositor doesn't expose it (drives the + // input-latency question; see WGPU_PRESENT_MODE notes above). If + // Mailbox is listed but we still pick Fifo, the preference order + // has a bug. + if (!surface_configured_) { + QString advertised; + for (size_t i = 0; i < caps.presentModeCount; ++i) { + const char* name = "?"; + switch (caps.presentModes[i]) { + case WGPUPresentMode_Fifo: name = "fifo"; break; + case WGPUPresentMode_FifoRelaxed: name = "fifo_relaxed"; break; + case WGPUPresentMode_Mailbox: name = "mailbox"; break; + case WGPUPresentMode_Immediate: name = "immediate"; break; + default: break; + } + if (i > 0) advertised += ", "; + advertised += QString::fromLatin1(name); + } + qInfo().noquote().nospace() + << "[wgpu] surface advertises present modes: " << advertised; + } WGPUPresentMode pm = WGPUPresentMode_Fifo; // spec-guaranteed fallback for (WGPUPresentMode candidate : preferred) { if (supports(candidate)) { pm = candidate; break; } diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index 6016dece38..49e171efaa 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -390,6 +390,36 @@ public: }; bool findInstance(uint32_t object_id, InstanceLookup& out) const; + // A point that actually lies on the model's first instance — the + // first instance's mesh AABB centre transformed by that instance's + // placement, in metres, pre-CoordinateOperation. Lookup only — the + // viewport already keeps the CPU-side MeshInfo + InstanceCpu around + // for picking / measurement; the federation false-origin guess + // (ViewportView::guessFederatedFalseOriginFromFirstModel) consumes + // this lazily on modelGeometryReady. Returns false when the model + // is unknown or has no instances. + bool firstGeometryPointWorldM(uint32_t model_id, + Eigen::Vector3d& out) const; + + // Re-frame the camera onto the federated false origin in post-shift + // space. After ViewportView's first-model false-origin guess sets a + // federation origin and the resulting recomposeAndUploadModel runs, + // the federation false origin (in world coords) maps to (0,0,0) in + // render coords — so we target (0,0,0) and the first model's + // anchor point sits dead-centre. + // + // Distance comes from the model's post-shift AABB diagonal with the + // same padding math as viewAll(), but clamped to `max_distance_m` + // so a model with one crazy-coord outlier vertex (16 km AABB + // diagonal because of one bad triangle) can't pull the camera so + // far back that the bulk of the geometry becomes a single pixel. + // Yaw/pitch unchanged — preserves the user's current look direction. + // + // Unlike viewAll() this *never* iterates all loaded models — it + // frames around the specific model the guess fired for, ignoring + // models with bad coordinates elsewhere in the session. + void frameOnFederatedOrigin(uint32_t model_id, float max_distance_m); + // Selection accessor. Exposed for callers (bonsai's volume readout) // that need to read selectionIds() / activeObjectId(). Mutation goes // through the existing setSelection / pick paths. diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index ec42696999..b290dac346 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -129,10 +129,10 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { } FederatedFalseOrigin -guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters, +guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, const ModelGeoref& georef, const FederationConfig& fed_cfg) { - Eigen::Vector3d t_m = first_placement_meters.block<3, 1>(0, 3); + Eigen::Vector3d t_m = first_geometry_point_m; const bool use_coord_op = georef.has_coordinate_operation; if (use_coord_op) { diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 726325efa4..bc0268814d 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -132,20 +132,24 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file); // Build a FederatedFalseOrigin guess so that a model lands near the // federation origin instead of out at its surveyor coordinates. Designed // to work without an open IFC file so it's usable from sidecar-only loads -// (the inputs are all derivable from the InstanceCpu cache + ModelGeoref). +// (the inputs are all derivable from the resident MeshInfo + InstanceCpu +// data + ModelGeoref). // -// Position: `first_placement_meters` is the model's "anchor" placement — -// typically the first instance's `placement_transformation`, which the -// iterator already produces in metres (its `convert-back-units` default -// is false). The translation is lifted through -// `georef.coordinate_operation_meters` when one is present, then -// expressed in the federation unit. +// Position: `first_geometry_point_m` is a point that actually lies on the +// model's first instance's geometry, in metres, pre-CoordinateOperation — +// typically the world-space centre of the first instance's mesh AABB +// (instance0.placement_transformation * mesh.local_aabb_center). We use +// a real geometry point rather than the instance's placement translation +// because IFC placements often live far from the actual geometry (long +// ObjectPlacement chains, intermediate local coordinate systems). The +// point is lifted through `georef.coordinate_operation_meters` when one +// is present, then expressed in the federation unit. // // Rotation: read directly from `georef.coordinate_operation_meters` // when `has_coordinate_operation` (this is the helmert grid-north // angle); otherwise zero. Anticlockwise positive. FederatedFalseOrigin -guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters, +guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, const ModelGeoref& georef, const FederationConfig& fed_cfg); diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index db3fe52696..94c3652f20 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -89,12 +89,6 @@ const ModelGeoref* SceneLoader::modelGeoref(uint32_t mid) { return &m.georef; } -const Eigen::Matrix4d* SceneLoader::firstPlacement(uint32_t mid) const { - auto it = models_.find(mid); - if (it == models_.end() || !it->second.has_first_placement) return nullptr; - return &it->second.first_placement; -} - std::vector SceneLoader::addFiles(const QStringList& paths) { std::vector assigned; assigned.reserve(paths.size()); @@ -283,13 +277,6 @@ void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) { model.has_georef = true; } - if (!d.instances.empty() && !model.has_first_placement) { - using Mat4dCol = Eigen::Matrix; - model.first_placement = - Eigen::Map(d.instances[0].placement_transformation); - model.has_first_placement = true; - } - std::vector elements = std::move(d.elements); std::string stbl = std::move(d.string_table); @@ -354,16 +341,8 @@ void SceneLoader::onStreamerMeshReady(MeshChunk chunk) { void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { if (loading_model_id_ != 0) { auto it = models_.find(loading_model_id_); - if (it != models_.end()) { - if (!it->second.has_first_placement) { - using Mat4dCol = Eigen::Matrix; - it->second.first_placement = - Eigen::Map(chunk.transform); - it->second.has_first_placement = true; - } - if (it->second.sidecar_builder) { - it->second.sidecar_builder->onInstanceReady(chunk); - } + if (it != models_.end() && it->second.sidecar_builder) { + it->second.sidecar_builder->onInstanceReady(chunk); } } viewport_->uploadInstanceChunk(chunk); diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index f7f1b232f6..591b4ceb36 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -94,14 +94,6 @@ public: // sidecar-hit path before the data-source thread populates the streamer). const ModelGeoref* modelGeoref(uint32_t mid); - // The placement_transformation (in metres, column-major 4x4) of the - // first instance the loader saw for `mid` — captured from the streamer's - // first InstanceChunk during a stream load, or from the cached - // InstanceCpu[0] on a sidecar hit. Returns nullptr until at least one - // instance has been observed. Used by the federation false-origin - // auto-guess to anchor the model without re-parsing the IFC. - const Eigen::Matrix4d* firstPlacement(uint32_t mid) const; - signals: void progressChanged(int percent); void loadStarted(uint32_t mid, QString display_name); @@ -156,12 +148,6 @@ private: ModelGeoref georef; bool has_georef = false; - // The first instance's placement_transformation (in metres) — set - // once per model from either the sidecar's InstanceCpu[0] or the - // streamer's first InstanceChunk. - Eigen::Matrix4d first_placement = Eigen::Matrix4d::Identity(); - bool has_first_placement = false; - // Live-load sidecar accumulator. Constructed at the start of a // stream load when shouldWriteSidecar is on; null otherwise. std::unique_ptr sidecar_builder;