ifcviewer: section tool — gizmo, drag, K shortcut

Wires up the user-facing section-cut tool on top of the clipping
plumbing landed in the previous commit.

- K toggles the tool.
- LMB while the tool is active:
    * On an existing plane's arrow gizmo (screen-space line-segment
      hit test, 12 px grab radius) → select + start drag.
    * Otherwise on geometry → pickSurfaceAt + addSectionPlaneAt-
      Surface, select the new plane.
    * Otherwise → deselect.
- LMB drag updates the plane's origin by projecting the cursor
  delta onto the screen-space normal axis and converting back to
  metres.  d is rederived from the new origin each frame.
- Delete removes the selected plane; Esc exits the tool.
- Each plane renders a 2x2 m quad outline plus a yellow arrow
  along +n at its origin.  Selected plane draws cyan and
  thicker.

LMB object-pick is suppressed while the tool is active so plane
creation does not also change selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-30 12:32:49 +10:00
parent 309e20009c
commit 51971dd493
2 changed files with 334 additions and 3 deletions
+300 -1
View File
@@ -317,6 +317,39 @@ out vec4 frag_color;
void main() { frag_color = vec4(v_color, u_alpha); }
)";
// Section plane gizmo: a unit-extent quad + arrow expressed in plane-local
// space, transformed to world via the per-plane basis (u, v, n). The arrow
// shaft sits along +z (== +n in plane-local terms), so dragging the arrow
// always slides the plane along its normal regardless of camera orientation.
static const char* PLANE_VERTEX_SHADER = R"(
#version 450 core
layout(location = 0) in vec3 a_local; // (u, v, n) in plane-local space
layout(location = 1) in vec3 a_color;
uniform mat4 u_vp;
uniform vec3 u_origin;
uniform vec3 u_axis_u;
uniform vec3 u_axis_v;
uniform vec3 u_axis_n;
uniform float u_size;
uniform vec4 u_tint;
out vec4 v_color;
void main() {
vec3 world = u_origin
+ a_local.x * u_axis_u * u_size
+ a_local.y * u_axis_v * u_size
+ a_local.z * u_axis_n * u_size;
gl_Position = u_vp * vec4(world, 1.0);
v_color = vec4(a_color, 1.0) * u_tint;
}
)";
static const char* PLANE_FRAGMENT_SHADER = R"(
#version 450 core
in vec4 v_color;
out vec4 frag_color;
void main() { frag_color = v_color; }
)";
static GLuint compileShader(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* source) {
GLuint shader = gl->glCreateShader(type);
gl->glShaderSource(shader, 1, &source, nullptr);
@@ -517,10 +550,13 @@ ViewportWindow::~ViewportWindow() {
if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_);
if (pivot_vao_) gl_->glDeleteVertexArrays(1, &pivot_vao_);
if (pivot_vbo_) gl_->glDeleteBuffers(1, &pivot_vbo_);
if (plane_vao_) gl_->glDeleteVertexArrays(1, &plane_vao_);
if (plane_vbo_) gl_->glDeleteBuffers(1, &plane_vbo_);
if (main_program_) gl_->glDeleteProgram(main_program_);
if (pick_program_) gl_->glDeleteProgram(pick_program_);
if (axis_program_) gl_->glDeleteProgram(axis_program_);
if (pivot_program_) gl_->glDeleteProgram(pivot_program_);
if (plane_program_) gl_->glDeleteProgram(plane_program_);
if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_);
if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_);
if (pick_pos_tex_) gl_->glDeleteTextures(1, &pick_pos_tex_);
@@ -551,6 +587,7 @@ void ViewportWindow::initGL() {
buildShaders();
buildAxisGizmo();
buildPivotIndicator();
buildSectionPlaneGizmo();
gl_->glEnable(GL_DEPTH_TEST);
gl_->glEnable(GL_MULTISAMPLE);
@@ -662,6 +699,11 @@ void ViewportWindow::buildShaders() {
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, PIVOT_FRAGMENT_SHADER);
pivot_program_ = linkProgram(gl_, vs, fs);
}
{
GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, PLANE_VERTEX_SHADER);
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, PLANE_FRAGMENT_SHADER);
plane_program_ = linkProgram(gl_, vs, fs);
}
{
GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, HIZ_DOWNSAMPLE_VS);
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, HIZ_DOWNSAMPLE_FS);
@@ -1465,6 +1507,28 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
viewAll();
return;
}
// K toggles the section tool.
if (key == Qt::Key_K
&& event->modifiers() == Qt::NoModifier
&& !event->isAutoRepeat()) {
toggleSectionTool();
return;
}
// While the section tool is active: Esc exits the tool, Delete removes
// the selected plane.
if (section_tool_active_) {
if (key == Qt::Key_Escape && !event->isAutoRepeat()) {
toggleSectionTool();
return;
}
if ((key == Qt::Key_Delete || key == Qt::Key_Backspace)
&& !event->isAutoRepeat()
&& section_plane_selected_ >= 0) {
removeSectionPlane(section_plane_selected_);
section_plane_selected_ = -1;
return;
}
}
QWindow::keyPressEvent(event);
}
@@ -1904,6 +1968,7 @@ bool ViewportWindow::addSectionPlaneAtSurface(const QVector3D& point,
SectionPlane p;
p.n = n;
p.origin = point;
p.d = -QVector3D::dotProduct(n, point);
section_planes_.push_back(p);
have_cached_cull_ = false;
@@ -1911,6 +1976,15 @@ bool ViewportWindow::addSectionPlaneAtSurface(const QVector3D& point,
return true;
}
void ViewportWindow::toggleSectionTool() {
section_tool_active_ = !section_tool_active_;
if (!section_tool_active_) {
section_drag_active_ = false;
section_drag_index_ = -1;
}
requestUpdate();
}
void ViewportWindow::removeSectionPlane(int index) {
if (index < 0 || index >= int(section_planes_.size())) return;
section_planes_.erase(section_planes_.begin() + index);
@@ -2414,6 +2488,7 @@ void ViewportWindow::render() {
gl_->glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
renderPivotIndicator();
renderSectionPlanes();
renderAxisGizmo();
// Build HiZ from this frame's resolved depth for next frame's cull.
@@ -2754,6 +2829,189 @@ void ViewportWindow::renderPickPass() {
gl_->glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void ViewportWindow::buildSectionPlaneGizmo() {
// Plane-local geometry, all GL_LINES. Quad is drawn in plane-local
// (u, v); arrow shaft + head extend along +n. Layout per vertex:
// x,y,z (plane-local) r,g,b (color, modulated by u_tint)
static const float verts[] = {
// --- quad outline (4 segments = 8 verts, white) ---
-1, -1, 0, 1,1,1, 1, -1, 0, 1,1,1,
1, -1, 0, 1,1,1, 1, 1, 0, 1,1,1,
1, 1, 0, 1,1,1, -1, 1, 0, 1,1,1,
-1, 1, 0, 1,1,1, -1, -1, 0, 1,1,1,
// --- arrow shaft along +n (yellow) ---
0, 0, 0, 1.0f, 0.85f, 0.2f,
0, 0, 1, 1.0f, 0.85f, 0.2f,
// --- arrow head (4 diagonals from tip back to a ring at z=0.7) ---
0, 0, 1, 1.0f, 0.85f, 0.2f,
-0.18f, 0, 0.78f, 1.0f, 0.85f, 0.2f,
0, 0, 1, 1.0f, 0.85f, 0.2f,
0.18f, 0, 0.78f, 1.0f, 0.85f, 0.2f,
0, 0, 1, 1.0f, 0.85f, 0.2f,
0, -0.18f, 0.78f, 1.0f, 0.85f, 0.2f,
0, 0, 1, 1.0f, 0.85f, 0.2f,
0, 0.18f, 0.78f, 1.0f, 0.85f, 0.2f,
};
plane_quad_offset_ = 0;
plane_quad_count_ = 8;
plane_arrow_offset_ = 8;
plane_arrow_count_ = 10; // 2 shaft + 8 head diagonals
gl_->glCreateVertexArrays(1, &plane_vao_);
gl_->glCreateBuffers(1, &plane_vbo_);
gl_->glNamedBufferStorage(plane_vbo_, sizeof(verts), verts, 0);
gl_->glVertexArrayVertexBuffer(plane_vao_, 0, plane_vbo_, 0, 6 * sizeof(float));
gl_->glEnableVertexArrayAttrib(plane_vao_, 0);
gl_->glVertexArrayAttribFormat(plane_vao_, 0, 3, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(plane_vao_, 0, 0);
gl_->glEnableVertexArrayAttrib(plane_vao_, 1);
gl_->glVertexArrayAttribFormat(plane_vao_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float));
gl_->glVertexArrayAttribBinding(plane_vao_, 1, 0);
}
// Pick any unit vector orthogonal to n. Avoids the degenerate case where n
// is parallel to the seed by choosing the seed with the smallest |n_i|.
static QVector3D anyOrthogonal(const QVector3D& n) {
const float ax = std::abs(n.x()), ay = std::abs(n.y()), az = std::abs(n.z());
QVector3D seed = (ax < ay && ax < az) ? QVector3D(1, 0, 0)
: (ay < az) ? QVector3D(0, 1, 0)
: QVector3D(0, 0, 1);
QVector3D u = QVector3D::crossProduct(n, seed);
if (u.lengthSquared() < 1e-12f) u = QVector3D(1, 0, 0);
return u.normalized();
}
void ViewportWindow::renderSectionPlanes() {
if (section_planes_.empty() || !plane_program_ || !plane_vao_) return;
// Plane half-extent in metres — quad is drawn at (±1, ±1) in plane-local
// space, so size = 1.0 produces the 2x2m footprint we want.
constexpr float kHalfSize = 1.0f;
const QMatrix4x4 vp = proj_matrix_ * view_matrix_;
gl_->glUseProgram(plane_program_);
gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(plane_program_, "u_vp"),
1, GL_FALSE, vp.constData());
gl_->glUniform1f(gl_->glGetUniformLocation(plane_program_, "u_size"), kHalfSize);
gl_->glEnable(GL_BLEND);
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl_->glBindVertexArray(plane_vao_);
const GLint loc_origin = gl_->glGetUniformLocation(plane_program_, "u_origin");
const GLint loc_u = gl_->glGetUniformLocation(plane_program_, "u_axis_u");
const GLint loc_v = gl_->glGetUniformLocation(plane_program_, "u_axis_v");
const GLint loc_n = gl_->glGetUniformLocation(plane_program_, "u_axis_n");
const GLint loc_tint = gl_->glGetUniformLocation(plane_program_, "u_tint");
for (int i = 0; i < int(section_planes_.size()); ++i) {
const SectionPlane& p = section_planes_[i];
const QVector3D u = anyOrthogonal(p.n);
const QVector3D v = QVector3D::crossProduct(p.n, u).normalized();
gl_->glUniform3f(loc_origin, p.origin.x(), p.origin.y(), p.origin.z());
gl_->glUniform3f(loc_u, u.x(), u.y(), u.z());
gl_->glUniform3f(loc_v, v.x(), v.y(), v.z());
gl_->glUniform3f(loc_n, p.n.x(), p.n.y(), p.n.z());
const bool selected = (i == section_plane_selected_);
// Selected plane: cyan-tinted, full alpha. Unselected: warm tint,
// dimmer. Multiplied onto the per-vertex color.
if (selected) gl_->glUniform4f(loc_tint, 0.55f, 0.95f, 1.0f, 1.0f);
else gl_->glUniform4f(loc_tint, 1.0f, 0.85f, 0.4f, 0.75f);
gl_->glLineWidth(selected ? 2.5f : 1.5f);
gl_->glDrawArrays(GL_LINES, plane_quad_offset_, plane_quad_count_);
gl_->glDrawArrays(GL_LINES, plane_arrow_offset_, plane_arrow_count_);
}
gl_->glBindVertexArray(0);
gl_->glDisable(GL_BLEND);
}
int ViewportWindow::hitTestSectionGizmo(int x, int y) const {
if (section_planes_.empty()) return -1;
const int w = width();
const int h = height();
if (w <= 0 || h <= 0) return -1;
const QMatrix4x4 vp = proj_matrix_ * view_matrix_;
const float grab_px = 12.0f;
int best = -1;
float best_d2 = grab_px * grab_px;
auto project = [&](const QVector3D& world, QVector2D& out) -> bool {
QVector4D clip = vp * QVector4D(world, 1.0f);
if (clip.w() <= 0.0f) return false; // behind camera
const float invw = 1.0f / clip.w();
// Qt mouse coords have y down from the top — match that here.
out = QVector2D(
(clip.x() * invw * 0.5f + 0.5f) * float(w),
(1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(h));
return true;
};
for (int i = 0; i < int(section_planes_.size()); ++i) {
const SectionPlane& p = section_planes_[i];
QVector2D s_origin, s_tip;
if (!project(p.origin, s_origin)) continue;
if (!project(p.origin + p.n * 1.0f, s_tip)) continue;
// Distance from (x,y) to the line segment (s_origin, s_tip).
const QVector2D q{float(x), float(y)};
const QVector2D ab = s_tip - s_origin;
const float ab_len2 = ab.lengthSquared();
if (ab_len2 < 1e-3f) continue; // degenerate (axis edge-on)
float t = QVector2D::dotProduct(q - s_origin, ab) / ab_len2;
t = qBound(0.0f, t, 1.0f);
const QVector2D proj = s_origin + ab * t;
const float d2 = (q - proj).lengthSquared();
if (d2 < best_d2) { best_d2 = d2; best = i; }
}
return best;
}
void ViewportWindow::updateSectionDrag(int x, int y) {
if (!section_drag_active_) return;
if (section_drag_index_ < 0
|| section_drag_index_ >= int(section_planes_.size())) return;
SectionPlane& p = section_planes_[section_drag_index_];
const int w = width();
const int h = height();
if (w <= 0 || h <= 0) return;
const QMatrix4x4 vp = proj_matrix_ * view_matrix_;
auto project = [&](const QVector3D& world, QVector2D& out) -> bool {
QVector4D clip = vp * QVector4D(world, 1.0f);
if (clip.w() <= 0.0f) return false;
const float invw = 1.0f / clip.w();
out = QVector2D(
(clip.x() * invw * 0.5f + 0.5f) * float(w),
(1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(h));
return true;
};
QVector2D s_origin, s_n;
if (!project(section_drag_start_origin_, s_origin)) return;
if (!project(section_drag_start_origin_ + p.n, s_n)) return;
const QVector2D screen_axis = s_n - s_origin;
const float screen_axis_len2 = screen_axis.lengthSquared();
if (screen_axis_len2 < 1e-3f) return; // axis is edge-on; drag would be ill-conditioned
// Project the cursor delta onto the screen-space axis to get the world-
// space slide along n. delta_pixels / |screen_axis_pixels_per_meter|.
const QVector2D delta_px(float(x - section_drag_start_mouse_.x()),
float(y - section_drag_start_mouse_.y()));
const float delta_along_axis_px = QVector2D::dotProduct(delta_px, screen_axis);
const float meters = delta_along_axis_px / screen_axis_len2;
p.origin = section_drag_start_origin_ + p.n * meters;
p.d = -QVector3D::dotProduct(p.n, p.origin);
have_cached_cull_ = false;
requestUpdate();
}
void ViewportWindow::renderPivotIndicator() {
if (!pivot_indicator_visible_ || !pivot_program_ || !pivot_vao_) return;
@@ -2878,10 +3136,46 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) {
setPivotIndicatorVisible(true);
requestUpdate();
}
if (section_tool_active_ && e->button() == Qt::LeftButton) {
// First try to grab an existing plane's arrow gizmo.
const int hit = hitTestSectionGizmo(e->pos().x(), e->pos().y());
if (hit >= 0) {
section_plane_selected_ = hit;
section_drag_active_ = true;
section_drag_index_ = hit;
section_drag_start_origin_ = section_planes_[hit].origin;
section_drag_start_mouse_ = e->pos();
requestUpdate();
return;
}
// Otherwise: create a new plane at the clicked surface.
uint32_t obj_id = 0;
QVector3D pos, normal;
if (pickSurfaceAt(e->pos().x(), e->pos().y(), obj_id, pos, normal)) {
if (addSectionPlaneAtSurface(pos, normal)) {
section_plane_selected_ = int(section_planes_.size()) - 1;
requestUpdate();
}
} else {
section_plane_selected_ = -1;
requestUpdate();
}
}
}
void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
if (camera_mode_ == CameraMode::Fps) return;
if (active_button_ == Qt::LeftButton && (e->pos() - last_mouse_pos_).manhattanLength() < 5) {
if (section_drag_active_ && active_button_ == Qt::LeftButton) {
section_drag_active_ = false;
section_drag_index_ = -1;
active_button_ = Qt::NoButton;
return;
}
// LMB pick is suppressed in section-tool mode — LMB there creates or
// selects planes (handled in handleMousePress) and the release should
// not also trigger object selection.
if (active_button_ == Qt::LeftButton
&& !section_tool_active_
&& (e->pos() - last_mouse_pos_).manhattanLength() < 5) {
uint32_t id = pickObjectAt(e->pos().x(), e->pos().y());
selected_object_id_ = id;
emit objectPicked(id);
@@ -2928,6 +3222,11 @@ void ViewportWindow::handleMouseMove(QMouseEvent* e) {
return;
}
if (section_drag_active_) {
updateSectionDrag(e->pos().x(), e->pos().y());
last_mouse_pos_ = e->pos();
return;
}
QPoint delta = e->pos() - last_mouse_pos_;
last_mouse_pos_ = e->pos();
if (active_button_ == Qt::MiddleButton) {
+34 -2
View File
@@ -188,14 +188,22 @@ public:
// click immediately cuts away the camera-facing side.
static constexpr int MaxSectionPlanes = 8;
struct SectionPlane {
QVector3D n; // unit world-space normal
float d; // -dot(n, p) for some p on the plane
QVector3D n; // unit world-space normal
QVector3D origin; // point on the plane — the gizmo's anchor
float d; // = -dot(n, origin); kept in sync with origin
};
int sectionPlaneCount() const { return int(section_planes_.size()); }
bool addSectionPlaneAtSurface(const QVector3D& point, const QVector3D& normal);
void removeSectionPlane(int index);
void clearSectionPlanes();
// Section tool: when active, LMB on geometry creates a new plane (using
// pickSurfaceAt for hit-point + normal); LMB on an existing plane's
// arrow gizmo selects + drags it; Delete removes the selected plane;
// Esc or another K-press exits the tool.
void toggleSectionTool();
bool sectionToolActive() const { return section_tool_active_; }
void setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch);
void setBenchmarkFrames(int n);
QString cameraString() const;
@@ -268,6 +276,15 @@ private:
void renderPickPass();
void renderAxisGizmo();
void renderPivotIndicator();
void renderSectionPlanes();
void buildSectionPlaneGizmo();
// Returns the index of the section plane whose arrow gizmo is under
// (x, y), or -1 if none. Screen-space line-segment distance test.
int hitTestSectionGizmo(int x, int y) const;
// Update section_planes_[section_drag_index_] from the current cursor
// position by projecting the move onto the plane's normal axis in
// screen space.
void updateSectionDrag(int x, int y);
void updateCamera();
// Geometry queries used by focusOnSelectedObject() / viewAll(). Both
@@ -500,10 +517,25 @@ private:
// Active section planes. Uploaded as uniform array each frame to the
// main + pick programs; capped at MaxSectionPlanes.
std::vector<SectionPlane> section_planes_;
bool section_tool_active_ = false;
int section_plane_selected_ = -1;
bool section_drag_active_ = false;
int section_drag_index_ = -1;
QVector3D section_drag_start_origin_;
QPoint section_drag_start_mouse_;
// Push the current plane list into a freshly-bound program. No-op if
// the program does not declare u_clip_count / u_clip_planes.
void uploadClipPlaneUniforms(GLuint program);
// GL resources for the per-plane visualization (quad outline + arrow).
GLuint plane_program_ = 0;
GLuint plane_vao_ = 0;
GLuint plane_vbo_ = 0;
int plane_quad_offset_ = 0;
int plane_quad_count_ = 0;
int plane_arrow_offset_ = 0;
int plane_arrow_count_ = 0;
// FPS smoothing
int frame_count_ = 0;
float accumulated_time_ = 0.0f;