From 6e86072d5a46454d940c1e54ae3a1ee93e6246dd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 9 Jul 2026 12:48:38 +1000 Subject: [PATCH] viewport: select a section plane, highlight it, delete the selected one Previously Del always removed the most recently added section plane. Now a plane can be picked and deleted individually: - ViewportCore tracks a selected plane index, kept valid as planes are added (the new one becomes selected), removed, or cleared. - Clicking a gizmo with the section tool active selects that plane. - The section gizmo geometry is baked white and coloured via its per-plane tint, so the selected plane draws in a bright amber highlight while the rest stay red (unchanged look). - Del/Backspace removes the selected plane, falling back to the most recent one when nothing is selected. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer/SectionGizmoRenderer.cpp | 33 ++++++++++++++++---------- src/ifcviewer/SectionGizmoRenderer.h | 4 +++- src/ifcviewer/ViewportCore.cpp | 19 ++++++++++++++- src/ifcviewer/ViewportCore.h | 6 +++++ src/ifcviewer/ViewportWindow.cpp | 14 +++++++---- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/ifcviewer/SectionGizmoRenderer.cpp b/src/ifcviewer/SectionGizmoRenderer.cpp index bd9ca4969f..c0ef92ab6f 100644 --- a/src/ifcviewer/SectionGizmoRenderer.cpp +++ b/src/ifcviewer/SectionGizmoRenderer.cpp @@ -172,18 +172,20 @@ bool SectionGizmoRenderer::init(WGPUDevice device, WGPUQueue queue, if (!device_ || !queue_) return false; // ---- Gizmo geometry: 9 line segments (quad outline + normal arrow) ---- + // Baked white so the per-plane `tint` uniform supplies the colour (red + // normally, a highlight colour for the selected plane — see encode()). struct Seg { std::array s, e, c; }; - static constexpr std::array kRed = { 1.000f, 0.200f, 0.322f }; + static constexpr std::array kWhite = { 1.0f, 1.0f, 1.0f }; static const Seg segs[] = { - { {-1, -1, 0}, { 1, -1, 0}, kRed }, // quad outline - { { 1, -1, 0}, { 1, 1, 0}, kRed }, - { { 1, 1, 0}, {-1, 1, 0}, kRed }, - { {-1, 1, 0}, {-1, -1, 0}, kRed }, - { { 0, 0, 0}, { 0, 0, 1}, kRed }, // arrow shaft along +n - { { 0, 0, 1}, {-0.18f, 0, 0.78f}, kRed }, // arrow head - { { 0, 0, 1}, { 0.18f, 0, 0.78f}, kRed }, - { { 0, 0, 1}, { 0, -0.18f, 0.78f}, kRed }, - { { 0, 0, 1}, { 0, 0.18f, 0.78f}, kRed }, + { {-1, -1, 0}, { 1, -1, 0}, kWhite }, // quad outline + { { 1, -1, 0}, { 1, 1, 0}, kWhite }, + { { 1, 1, 0}, {-1, 1, 0}, kWhite }, + { {-1, 1, 0}, {-1, -1, 0}, kWhite }, + { { 0, 0, 0}, { 0, 0, 1}, kWhite }, // arrow shaft along +n + { { 0, 0, 1}, {-0.18f, 0, 0.78f}, kWhite }, // arrow head + { { 0, 0, 1}, { 0.18f, 0, 0.78f}, kWhite }, + { { 0, 0, 1}, { 0, -0.18f, 0.78f}, kWhite }, + { { 0, 0, 1}, { 0, 0.18f, 0.78f}, kWhite }, }; std::vector verts; verts.reserve(std::size(segs) * 6 * 11); @@ -302,7 +304,8 @@ bool SectionGizmoRenderer::init(WGPUDevice device, WGPUQueue queue, void SectionGizmoRenderer::encode(WGPURenderPassEncoder pass, const Eigen::Matrix4f& view_proj, const std::vector& planes, - int viewport_w_px, int viewport_h_px, int device_pixel_ratio) { + int viewport_w_px, int viewport_h_px, int device_pixel_ratio, + int selected_index) { if (!pipeline_ || planes.empty()) return; wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE); @@ -321,9 +324,15 @@ void SectionGizmoRenderer::encode(WGPURenderPassEncoder pass, const Eigen::Matri // arrow would shoot past the eye (clip.w<0) and vanish. const float half = 1.0f; + // Red normally; a bright amber highlight for the selected plane. + const bool selected = (i == selected_index); + const float tr = selected ? 1.00f : 1.000f; + const float tg = selected ? 0.75f : 0.200f; + const float tb = selected ? 0.10f : 0.322f; + uint8_t slot[256]; packSectionUniform(slot, view_proj, plane.origin, half, tangent, line_w, - bitangent, nn, 1.0f, 1.0f, 1.0f, 1.0f, vw, vh); + bitangent, nn, tr, tg, tb, 1.0f, vw, vh); const uint32_t slot_offset = uint32_t(i) * kSectionUniformSlot; wgpuQueueWriteBuffer(queue_, uniform_buffer_, slot_offset, slot, sizeof(slot)); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &slot_offset); diff --git a/src/ifcviewer/SectionGizmoRenderer.h b/src/ifcviewer/SectionGizmoRenderer.h index b902953ed3..49ef000eea 100644 --- a/src/ifcviewer/SectionGizmoRenderer.h +++ b/src/ifcviewer/SectionGizmoRenderer.h @@ -51,9 +51,11 @@ public: bool ready() const { return pipeline_ != nullptr; } // Draw one gizmo per plane into an already-open render pass (the main pass). + // `selected_index` (or -1) is drawn with a highlight tint to show selection. void encode(WGPURenderPassEncoder pass, const Eigen::Matrix4f& view_proj, const std::vector& planes, - int viewport_w_px, int viewport_h_px, int device_pixel_ratio); + int viewport_w_px, int viewport_h_px, int device_pixel_ratio, + int selected_index = -1); // Screen-space hit test: index of the plane whose gizmo (arrow segment, // origin→origin+normal) the (x, y) logical-pixel point lies within diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 8a753d26e5..99c3000a0d 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -6439,7 +6439,7 @@ void ViewportCore::render() { // Section-plane gizmo — shared renderer, drawn for desktop + web from here. // (The desktop's OverlayRenderer no longer draws it, to avoid doubling.) section_gizmo_.encode(pass, vp_this_frame, section_planes_, - viewport_w_px, viewport_h_px, dpr_int); + viewport_w_px, viewport_h_px, dpr_int, section_selected_index_); // Remaining in-pass overlays (highlight triangles, pivot, overlay // lines/points). QtViewportHost forwards to overlays_.X(); web host no-ops. @@ -6728,6 +6728,8 @@ bool ViewportCore::addSectionPlaneAtSurface(const Eigen::Vector3f& point, p.d = -n.dot(point); p.visual_radius = (visual_radius > 0.0f) ? visual_radius : 1.0f; section_planes_.push_back(p); + // The freshly added plane becomes the selected one. + section_selected_index_ = int(section_planes_.size()) - 1; Log::info() << "[wgpu section] added plane #" << section_planes_.size() - 1 << " origin=(" << point.x() << "," << point.y() << "," << point.z() << ")" @@ -6736,9 +6738,23 @@ bool ViewportCore::addSectionPlaneAtSurface(const Eigen::Vector3f& point, return true; } +void ViewportCore::setSelectedSectionPlane(int index) { + const int clamped = (index >= 0 && index < int(section_planes_.size())) ? index : -1; + if (clamped == section_selected_index_) return; + section_selected_index_ = clamped; + host_->requestFrame(); +} + void ViewportCore::removeSectionPlane(int index) { if (index < 0 || index >= int(section_planes_.size())) return; section_planes_.erase(section_planes_.begin() + index); + // Keep the selection pointing at the same plane: clear it if it was the one + // removed, shift it down if it sat after the removed index. + if (section_selected_index_ == index) { + section_selected_index_ = -1; + } else if (section_selected_index_ > index) { + --section_selected_index_; + } Log::info() << "[wgpu section] removed plane " << index; host_->requestFrame(); } @@ -6746,6 +6762,7 @@ void ViewportCore::removeSectionPlane(int index) { void ViewportCore::clearSectionPlanes() { if (section_planes_.empty()) return; section_planes_.clear(); + section_selected_index_ = -1; Log::info() << "[wgpu section] cleared all planes"; host_->requestFrame(); } diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 29b21a3593..e03d60d3fe 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -534,6 +534,11 @@ public: // Number of active section planes (0..kMaxSectionPlanes). int sectionPlaneCount() const { return int(section_planes_.size()); } + // The selected section plane (drawn highlighted; the target of a delete), or + // -1 for none. Index is kept valid as planes are added/removed/cleared. + void setSelectedSectionPlane(int index); + int selectedSectionPlane() const { return section_selected_index_; } + // ---- Section gizmo interaction (shared desktop + web) ------------------- // // All coords are LOGICAL (CSS) pixels; the core derives the logical viewport @@ -1057,6 +1062,7 @@ private: // the press point (logical px) so update can slide it along the normal. bool section_drag_active_ = false; int section_drag_index_ = -1; + int section_selected_index_ = -1; Eigen::Vector3f section_drag_start_origin_ = Eigen::Vector3f::Zero(); int section_drag_start_mx_ = 0; int section_drag_start_my_ = 0; diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 555d097d48..b4e95bf78c 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1554,6 +1554,7 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) { const Eigen::Vector2i lp = toV2i(event->position().toPoint()); const int hit = core_.hitTestSectionGizmo(lp.x(), lp.y()); if (hit >= 0 && core_.beginSectionDrag(hit, lp.x(), lp.y())) { + core_.setSelectedSectionPlane(hit); // clicking a gizmo selects it nav_drag_kind_ = NavDrag::Inactive; Log::info().noquote().nospace() << "[wgpu section] drag start: plane=" << hit; @@ -1918,9 +1919,10 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { // Section tool. K toggles the tool; Shift+K clears all planes. When // the tool is active, click adds a plane at the surface (handled in - // mouseReleaseEvent), Esc deactivates, Del/Backspace removes the - // most recently added plane. Mirrors GL ViewportWindow + Bonsai's - // bind_shortcut(K / Shift+K) bindings. + // mouseReleaseEvent) or selects the gizmo under the cursor, Esc + // deactivates, Del/Backspace removes the selected plane (or the most + // recent one when nothing is selected). Mirrors GL ViewportWindow + + // Bonsai's bind_shortcut(K / Shift+K) bindings. if (key == Qt::Key_K && !event->isAutoRepeat()) { if (mods == Qt::ShiftModifier) { clearSectionPlanes(); @@ -1936,7 +1938,11 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { } if ((key == Qt::Key_Delete || key == Qt::Key_Backspace) && !section_planes_.empty()) { - removeSectionPlane(int(section_planes_.size()) - 1); + // Delete the selected plane; fall back to the most recent one when + // nothing is selected. + const int selected = core_.selectedSectionPlane(); + removeSectionPlane(selected >= 0 ? selected + : int(section_planes_.size()) - 1); return; } }