/******************************************************************************** * * * 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 "Selection.h" #include SelectionState::SelectionState(QObject* parent) : QObject(parent) { // Reserve slot 0 — object_id 0 is the "no object" sentinel and the // shader still indexes the buffer with v_object_id, so the slot must // exist (and be zero) to avoid OOB reads. cpu_flags_.assign(1, 0u); } SelectionState::~SelectionState() = default; void SelectionState::initializeGl(QOpenGLFunctions_4_5_Core* gl) { gl_ = gl; if (ssbo_ == 0) { gl_->glCreateBuffers(1, &ssbo_); } // Force a fresh upload against the new context. dirty_ = true; } void SelectionState::releaseGl() { if (gl_ && ssbo_) { gl_->glDeleteBuffers(1, &ssbo_); } ssbo_ = 0; ssbo_capacity_slots_ = 0; gl_ = nullptr; dirty_ = true; } void SelectionState::reset() { const bool had_state = !selected_ids_.empty() || active_id_ != 0; selected_ids_.clear(); active_id_ = 0; std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u); markDirty(); if (had_state) emit changed(active_id_); } void SelectionState::noteObjectId(uint32_t id) { if (id == 0) return; if (uint32_t(cpu_flags_.size()) <= id) { // Grow CPU side; the SSBO is sized lazily inside uploadFlags so // we don't realloc GL on every streamed instance. cpu_flags_.resize(size_t(id) + 1, 0u); markDirty(); } } void SelectionState::setSelectedObjectId(uint32_t id) { if (id == 0) { clearSelection(); return; } setSelection({id}, id); } void SelectionState::setSelection(const std::unordered_set& ids, uint32_t active) { // Avoid emitting churn when the call is a no-op. if (ids == selected_ids_ && active == active_id_) return; selected_ids_ = ids; selected_ids_.erase(0); active_id_ = (active != 0 && selected_ids_.count(active)) ? active : 0u; markDirty(); emit changed(active_id_); } void SelectionState::addToSelection(const std::unordered_set& ids) { bool any_added = false; for (uint32_t id : ids) { if (id == 0) continue; if (selected_ids_.insert(id).second) any_added = true; } if (!any_added) return; markDirty(); emit changed(active_id_); } void SelectionState::removeFromSelection(const std::unordered_set& ids) { bool any_removed = false; bool active_removed = false; for (uint32_t id : ids) { if (selected_ids_.erase(id) > 0) { any_removed = true; if (id == active_id_) active_removed = true; } } if (!any_removed) return; if (active_removed) active_id_ = 0; markDirty(); emit changed(active_id_); } void SelectionState::toggleInSelection(uint32_t id) { if (id == 0) return; if (selected_ids_.erase(id) > 0) { // Removed. If it was active, drop active. if (active_id_ == id) active_id_ = 0; } else { // Added. Last-toggled becomes active so the properties panel // tracks the most recent click — matches the user's "last single // clicked is active" expectation. selected_ids_.insert(id); active_id_ = id; } markDirty(); emit changed(active_id_); } void SelectionState::clearSelection() { if (selected_ids_.empty() && active_id_ == 0) return; selected_ids_.clear(); active_id_ = 0; markDirty(); emit changed(active_id_); } void SelectionState::markDirty() { dirty_ = true; } void SelectionState::growTo(uint32_t capacity_ids) { if (!gl_ || ssbo_ == 0) return; if (capacity_ids <= ssbo_capacity_slots_) return; // Round up to a power-of-two-ish step so streamed scenes don't realloc // every few instances. Floor at 1024 slots = 4 KB. size_t new_cap = std::max(1024, ssbo_capacity_slots_ * 2); while (new_cap < capacity_ids) new_cap *= 2; gl_->glNamedBufferData(ssbo_, GLsizeiptr(new_cap * sizeof(uint32_t)), nullptr, GL_DYNAMIC_DRAW); ssbo_capacity_slots_ = new_cap; } void SelectionState::uploadFlags() { if (!gl_ || ssbo_ == 0) return; // Rebuild the CPU flag vector from the canonical selected_ids_. The // overhead is O(|cpu_flags_|), which scales with max object_id rather // than with selection size — acceptable: object_ids are dense so this // is just a memset + a handful of writes for the selected set. std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u); for (uint32_t id : selected_ids_) { if (id < cpu_flags_.size()) cpu_flags_[id] = 1u; } growTo(static_cast(cpu_flags_.size())); if (ssbo_capacity_slots_ == 0) return; const GLsizeiptr bytes = GLsizeiptr(cpu_flags_.size() * sizeof(uint32_t)); if (bytes > 0) { gl_->glNamedBufferSubData(ssbo_, 0, bytes, cpu_flags_.data()); } dirty_ = false; } void SelectionState::bindForRender(GLuint binding_index) { if (!gl_ || ssbo_ == 0) return; if (dirty_) uploadFlags(); gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_index, ssbo_); }