ifcviewer: viewport overlay subsystem (highlight tris + HUD text)

New OverlayRenderer module owns every client-supplied overlay primitive
drawn after the main pass: tinted, depth-aware highlight triangles via
its own GL shader, and top-left HUD text via QPainter on a
QOpenGLPaintDevice.  Public surface on ViewportWindow is just two
forwarders (setHighlightTriangles, setHudText).

ViewportWindow's MeshLocalPick now exposes the instance's composed
transform so consumers can map mesh-local geometry back to world
space without re-querying.  AreaMeasurement uses both: its selection
key is now (object_id, tri) so per-instance highlighting works for
two distinct walls sharing a mesh, and on every pick it rebuilds the
world-space tri list and the HUD readout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-07 16:43:38 +10:00
parent d565dc3ff3
commit ad62dd6d91
7 changed files with 414 additions and 21 deletions
+199
View File
@@ -0,0 +1,199 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "OverlayRenderer.h"
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QtGlobal>
#include <QtOpenGL/QOpenGLPaintDevice>
namespace {
GLuint compile(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* src) {
GLuint s = gl->glCreateShader(type);
gl->glShaderSource(s, 1, &src, nullptr);
gl->glCompileShader(s);
GLint ok = 0;
gl->glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
gl->glGetShaderInfoLog(s, sizeof(log), nullptr, log);
qWarning("OverlayRenderer shader compile error: %s", log);
}
return s;
}
GLuint link(QOpenGLFunctions_4_5_Core* gl, GLuint vs, GLuint fs) {
GLuint p = gl->glCreateProgram();
gl->glAttachShader(p, vs);
gl->glAttachShader(p, fs);
gl->glLinkProgram(p);
GLint ok = 0;
gl->glGetProgramiv(p, GL_LINK_STATUS, &ok);
if (!ok) {
char log[2048];
gl->glGetProgramInfoLog(p, sizeof(log), nullptr, log);
qWarning("OverlayRenderer program link error: %s", log);
}
gl->glDeleteShader(vs);
gl->glDeleteShader(fs);
return p;
}
const char* VERT_SRC = R"(
#version 450 core
layout(location = 0) in vec3 in_pos;
uniform mat4 u_view_proj;
void main() {
gl_Position = u_view_proj * vec4(in_pos, 1.0);
}
)";
const char* FRAG_SRC = R"(
#version 450 core
uniform vec4 u_color;
out vec4 frag_color;
void main() {
frag_color = u_color;
}
)";
} // namespace
void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
if (gl_) return;
gl_ = gl;
GLuint vs = compile(gl_, GL_VERTEX_SHADER, VERT_SRC);
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, FRAG_SRC);
program_ = link(gl_, vs, fs);
u_view_proj_ = gl_->glGetUniformLocation(program_, "u_view_proj");
u_color_ = gl_->glGetUniformLocation(program_, "u_color");
gl_->glCreateVertexArrays(1, &vao_);
gl_->glCreateBuffers(1, &vbo_);
gl_->glEnableVertexArrayAttrib(vao_, 0);
gl_->glVertexArrayAttribFormat(vao_, 0, 3, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(vao_, 0, 0);
gl_->glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, 3 * sizeof(float));
}
void OverlayRenderer::release() {
if (!gl_) return;
if (vbo_) gl_->glDeleteBuffers(1, &vbo_);
if (vao_) gl_->glDeleteVertexArrays(1, &vao_);
if (program_) gl_->glDeleteProgram(program_);
program_ = vao_ = vbo_ = 0;
vbo_capacity_ = 0;
vertex_count_ = 0;
gl_ = nullptr;
}
void OverlayRenderer::setHudText(const QString& text) {
hud_text_ = text;
}
void OverlayRenderer::setHighlightTriangles(const std::vector<float>& world_xyz,
float r, float g, float b, float a) {
if (!gl_) return;
color_[0] = r; color_[1] = g; color_[2] = b; color_[3] = a;
vertex_count_ = GLsizei(world_xyz.size() / 3);
if (vertex_count_ == 0) return;
const size_t bytes = world_xyz.size() * sizeof(float);
if (bytes > vbo_capacity_) {
// Grow with a little headroom so frequent appends don't realloc.
const size_t new_cap = bytes + bytes / 2;
gl_->glNamedBufferData(vbo_, GLsizeiptr(new_cap),
nullptr, GL_DYNAMIC_DRAW);
vbo_capacity_ = new_cap;
}
gl_->glNamedBufferSubData(vbo_, 0, GLsizeiptr(bytes), world_xyz.data());
}
void OverlayRenderer::render(const float view_proj[16],
int pixel_w, int pixel_h, qreal dpr) {
if (!gl_) return;
// GL pass: tinted highlight triangles.
if (program_ && vertex_count_ > 0 && color_[3] > 0.0f) {
gl_->glUseProgram(program_);
gl_->glUniformMatrix4fv(u_view_proj_, 1, GL_FALSE, view_proj);
gl_->glUniform4fv(u_color_, 1, color_);
// Save the GL state we touch and restore at the end so the rest of
// the render pass keeps seeing what it expects.
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
GLboolean prev_depth_msk = GL_TRUE;
gl_->glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_msk);
GLint prev_depth_func = GL_LESS;
gl_->glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
GLint prev_blend_src = GL_ONE, prev_blend_dst = GL_ZERO;
gl_->glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
gl_->glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
gl_->glEnable(GL_BLEND);
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl_->glDisable(GL_CULL_FACE); // both sides tinted
gl_->glDepthMask(GL_FALSE); // tint, don't occlude
gl_->glDepthFunc(GL_LEQUAL); // win the coplanar fight
gl_->glBindVertexArray(vao_);
gl_->glDrawArrays(GL_TRIANGLES, 0, vertex_count_);
gl_->glBindVertexArray(0);
if (!prev_blend) gl_->glDisable(GL_BLEND);
gl_->glBlendFunc(prev_blend_src, prev_blend_dst);
if (prev_cull) gl_->glEnable(GL_CULL_FACE);
gl_->glDepthMask(prev_depth_msk);
gl_->glDepthFunc(prev_depth_func);
}
// QPainter pass: HUD text. This rebinds programs/VAOs internally, so
// it has to come after every other GL primitive in the overlay.
if (!hud_text_.isEmpty() && pixel_w > 0 && pixel_h > 0) {
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
device.setDevicePixelRatio(dpr);
QPainter painter(&device);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
QFont font("monospace", 11);
font.setStyleHint(QFont::TypeWriter);
painter.setFont(font);
const QFontMetrics fm(font);
const int pad_x = 10, pad_y = 6, margin = 12;
const int text_w = fm.horizontalAdvance(hud_text_);
const int text_h = fm.height();
const QRect bg(margin, margin,
text_w + 2 * pad_x,
text_h + 2 * pad_y);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(0, 0, 0, 160));
painter.drawRoundedRect(bg, 4, 4);
painter.setPen(Qt::white);
painter.drawText(bg.adjusted(pad_x, pad_y, -pad_x, -pad_y),
Qt::AlignLeft | Qt::AlignVCenter,
hud_text_);
}
}