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 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-09 12:48:38 +10:00
parent 12002ace86
commit 6e86072d5a
5 changed files with 58 additions and 18 deletions
+21 -12
View File
@@ -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<float, 3> s, e, c; };
static constexpr std::array<float, 3> kRed = { 1.000f, 0.200f, 0.322f };
static constexpr std::array<float, 3> 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<float> 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<SectionPlane>& 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);
+3 -1
View File
@@ -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<SectionPlane>& 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
+18 -1
View File
@@ -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();
}
+6
View File
@@ -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;
+10 -4
View File
@@ -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;
}
}