/********************************************************************************
* *
* 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 "OverlayRenderer.h"
#include
#include
#include
#include
#include
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;
}
// ---- Triangle program (flat color) ----
const char* TRI_VS = 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* TRI_FS = R"(
#version 450 core
uniform vec4 u_color;
out vec4 frag_color;
void main() {
frag_color = u_color;
}
)";
// ---- Point sprite program (outlined disc via gl_PointCoord) ----
const char* POINT_VS = R"(
#version 450 core
layout(location = 0) in vec3 in_pos;
uniform mat4 u_view_proj;
uniform float u_point_size;
void main() {
gl_Position = u_view_proj * vec4(in_pos, 1.0);
gl_PointSize = u_point_size;
}
)";
// inner_radius_norm is the inner-disc radius as a fraction of the
// half-sprite (so 1.0 = no stroke, smaller = thicker stroke). The
// fragment shader reads gl_PointCoord (range [0,1] across the sprite),
// computes the distance from the centre normalised against the half-
// sprite, picks inner vs stroke with a sharp `step()` (no soft band),
// then anti-aliases the *outer* edge only.
const char* POINT_FS = R"(
#version 450 core
uniform vec4 u_inner_color;
uniform vec4 u_stroke_color;
uniform float u_inner_radius_norm;
out vec4 frag_color;
void main() {
vec2 c = gl_PointCoord - 0.5;
float d = length(c) * 2.0; // 0 at centre, 1 at sprite edge
if (d > 1.0) discard;
float t_inner = step(u_inner_radius_norm, d);
vec4 col = mix(u_inner_color, u_stroke_color, t_inner);
float aa = fwidth(d);
float outer_alpha = smoothstep(1.0, 1.0 - aa, d);
frag_color = vec4(col.rgb, col.a * outer_alpha);
}
)";
// ---- Line program (screen-space-expanded quads with outline) ----
//
// Per-vertex layout: (in_a, in_b, in_side, in_along), 8 floats total.
// The vertex shader projects both endpoints to screen pixels, computes
// the screen-space perpendicular, and offsets *this* corner accordingly.
// Output v_dist_px is the signed perpendicular distance from the line
// axis at this corner; linear interpolation across the quad gives the
// per-fragment distance the FS uses to discard / pick inner vs stroke.
const char* LINE_VS = R"(
#version 450 core
layout(location = 0) in vec3 in_a;
layout(location = 1) in vec3 in_b;
layout(location = 2) in float in_side; // -1 or +1
layout(location = 3) in float in_along; // 0 (at a) or 1 (at b)
uniform mat4 u_view_proj;
uniform vec2 u_screen_size; // physical pixels
uniform float u_half_width; // inner half-width (px)
uniform float u_stroke_extra; // halo per side (px)
out float v_dist_px;
out float v_along_px; // distance from segment start (px)
void main() {
vec4 clip_a = u_view_proj * vec4(in_a, 1.0);
vec4 clip_b = u_view_proj * vec4(in_b, 1.0);
// Project to screen pixels.
vec2 screen_a = (clip_a.xy / clip_a.w) * 0.5 * u_screen_size;
vec2 screen_b = (clip_b.xy / clip_b.w) * 0.5 * u_screen_size;
vec2 delta = screen_b - screen_a;
float len = length(delta);
vec2 dir = (len > 1e-6) ? (delta / len) : vec2(1.0, 0.0);
vec2 perp = vec2(-dir.y, dir.x);
// Offset this corner perpendicular to the line.
vec4 clip_self = mix(clip_a, clip_b, in_along);
vec2 screen_self = (clip_self.xy / clip_self.w) * 0.5 * u_screen_size;
float total_half = u_half_width + u_stroke_extra;
screen_self += perp * in_side * total_half;
// Back to NDC, then to clip space (multiply by w to undo the w-divide
// GL is about to apply). Depth is preserved from the picked endpoint.
vec2 ndc_out = screen_self / (u_screen_size * 0.5);
gl_Position = vec4(ndc_out * clip_self.w, clip_self.z, clip_self.w);
v_dist_px = in_side * total_half;
v_along_px = in_along * len;
}
)";
// ---- Screen-space rect program (label + HUD backgrounds) ----
//
// Skip QPainter::fillRect entirely — on QOpenGLPaintDevice it's
// unreliable across drivers. Backgrounds are drawn as raw GL quads
// using NDC-space coordinates; QPainter only renders the text on top.
const char* RECT_VS = R"(
#version 450 core
layout(location = 0) in vec2 in_ndc;
void main() {
gl_Position = vec4(in_ndc, 0.0, 1.0);
}
)";
const char* RECT_FS = R"(
#version 450 core
uniform vec4 u_color;
out vec4 frag_color;
void main() {
frag_color = u_color;
}
)";
const char* LINE_FS = R"(
#version 450 core
in float v_dist_px;
in float v_along_px;
uniform vec4 u_inner_color;
uniform vec4 u_stroke_color;
uniform float u_half_width;
uniform float u_stroke_extra;
uniform float u_dash_period; // 0 = solid
uniform float u_dash_on_ratio;
out vec4 frag_color;
void main() {
if (u_dash_period > 0.0) {
float t = mod(v_along_px, u_dash_period);
if (t > u_dash_period * u_dash_on_ratio) discard;
}
float ad = abs(v_dist_px);
float total = u_half_width + u_stroke_extra;
if (ad > total) discard;
// Sharp inner-to-stroke transition; AA only the outer halo edge so
// the line reads crisp instead of mushy.
float t_stroke = step(u_half_width, ad);
vec4 col = mix(u_inner_color, u_stroke_color, t_stroke);
float outer_a = smoothstep(total, total - 1.0, ad);
frag_color = vec4(col.rgb, col.a * outer_a);
}
)";
void uploadFloats(QOpenGLFunctions_4_5_Core* gl,
GLuint vbo, size_t& capacity_bytes,
const std::vector& data) {
const size_t bytes = data.size() * sizeof(float);
if (bytes == 0) return;
if (bytes > capacity_bytes) {
const size_t new_cap = bytes + bytes / 2;
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
capacity_bytes = new_cap;
}
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data.data());
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void uploadFloatBytes(QOpenGLFunctions_4_5_Core* gl,
GLuint vbo, size_t& capacity_bytes,
const float* data, size_t float_count) {
const size_t bytes = float_count * sizeof(float);
if (bytes == 0) return;
if (bytes > capacity_bytes) {
const size_t new_cap = bytes + bytes / 2;
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
capacity_bytes = new_cap;
}
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data);
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
}
// CPU expansion of N segments (3 floats * 2 verts per segment, packed) into
// 6 vertices per segment, each carrying (a, b, side, along) = 8 floats.
void expandLineSegments(const std::vector& endpoints,
std::vector& out) {
out.clear();
if (endpoints.size() < 6) return;
const size_t n_segs = endpoints.size() / 6;
out.reserve(n_segs * 6 * 8);
static const float CORNERS[6][2] = {
{-1.0f, 0.0f}, {+1.0f, 0.0f}, {-1.0f, 1.0f},
{-1.0f, 1.0f}, {+1.0f, 0.0f}, {+1.0f, 1.0f},
};
for (size_t s = 0; s < n_segs; ++s) {
const float* a = &endpoints[s * 6 + 0];
const float* b = &endpoints[s * 6 + 3];
for (int c = 0; c < 6; ++c) {
out.push_back(a[0]); out.push_back(a[1]); out.push_back(a[2]);
out.push_back(b[0]); out.push_back(b[1]); out.push_back(b[2]);
out.push_back(CORNERS[c][0]);
out.push_back(CORNERS[c][1]);
}
}
}
} // namespace
void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
if (gl_) return;
gl_ = gl;
// Triangle program.
{
GLuint vs = compile(gl_, GL_VERTEX_SHADER, TRI_VS);
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, TRI_FS);
program_tri_ = link(gl_, vs, fs);
u_tri_view_proj_ = gl_->glGetUniformLocation(program_tri_, "u_view_proj");
u_tri_color_ = gl_->glGetUniformLocation(program_tri_, "u_color");
}
// Point program.
{
GLuint vs = compile(gl_, GL_VERTEX_SHADER, POINT_VS);
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, POINT_FS);
program_pt_ = link(gl_, vs, fs);
u_pt_view_proj_ = gl_->glGetUniformLocation(program_pt_, "u_view_proj");
u_pt_point_size_ = gl_->glGetUniformLocation(program_pt_, "u_point_size");
u_pt_inner_color_ = gl_->glGetUniformLocation(program_pt_, "u_inner_color");
u_pt_stroke_color_ = gl_->glGetUniformLocation(program_pt_, "u_stroke_color");
u_pt_inner_radius_ = gl_->glGetUniformLocation(program_pt_, "u_inner_radius_norm");
}
// Line program.
{
GLuint vs = compile(gl_, GL_VERTEX_SHADER, LINE_VS);
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, LINE_FS);
program_ln_ = link(gl_, vs, fs);
u_ln_view_proj_ = gl_->glGetUniformLocation(program_ln_, "u_view_proj");
u_ln_screen_size_ = gl_->glGetUniformLocation(program_ln_, "u_screen_size");
u_ln_half_width_ = gl_->glGetUniformLocation(program_ln_, "u_half_width");
u_ln_stroke_extra_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_extra");
u_ln_inner_color_ = gl_->glGetUniformLocation(program_ln_, "u_inner_color");
u_ln_stroke_color_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_color");
u_ln_dash_period_ = gl_->glGetUniformLocation(program_ln_, "u_dash_period");
u_ln_dash_on_ratio_ = gl_->glGetUniformLocation(program_ln_, "u_dash_on_ratio");
}
// Screen-space rect program.
{
GLuint vs = compile(gl_, GL_VERTEX_SHADER, RECT_VS);
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, RECT_FS);
program_rect_ = link(gl_, vs, fs);
u_rect_color_ = gl_->glGetUniformLocation(program_rect_, "u_color");
}
// Triangle VAO/VBO: one vec3 attribute.
gl_->glCreateVertexArrays(1, &triangles_.vao);
gl_->glCreateBuffers(1, &triangles_.vbo);
gl_->glEnableVertexArrayAttrib(triangles_.vao, 0);
gl_->glVertexArrayAttribFormat(triangles_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(triangles_.vao, 0, 0);
gl_->glVertexArrayVertexBuffer(triangles_.vao, 0, triangles_.vbo,
0, 3 * sizeof(float));
// Point VAO/VBO: one vec3 attribute.
gl_->glCreateVertexArrays(1, &points_.vao);
gl_->glCreateBuffers(1, &points_.vbo);
gl_->glEnableVertexArrayAttrib(points_.vao, 0);
gl_->glVertexArrayAttribFormat(points_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(points_.vao, 0, 0);
gl_->glVertexArrayVertexBuffer(points_.vao, 0, points_.vbo,
0, 3 * sizeof(float));
// Line VAO/VBO: 8 floats per vertex (a:vec3, b:vec3, side, along).
// Shared across every group; line_draws_ records the (first, count)
// slice for each.
gl_->glCreateVertexArrays(1, &vao_lines_);
gl_->glCreateBuffers(1, &vbo_lines_);
const GLsizei stride = 8 * sizeof(float);
gl_->glEnableVertexArrayAttrib(vao_lines_, 0);
gl_->glVertexArrayAttribFormat(vao_lines_, 0, 3, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(vao_lines_, 0, 0);
gl_->glEnableVertexArrayAttrib(vao_lines_, 1);
gl_->glVertexArrayAttribFormat(vao_lines_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float));
gl_->glVertexArrayAttribBinding(vao_lines_, 1, 0);
gl_->glEnableVertexArrayAttrib(vao_lines_, 2);
gl_->glVertexArrayAttribFormat(vao_lines_, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float));
gl_->glVertexArrayAttribBinding(vao_lines_, 2, 0);
gl_->glEnableVertexArrayAttrib(vao_lines_, 3);
gl_->glVertexArrayAttribFormat(vao_lines_, 3, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float));
gl_->glVertexArrayAttribBinding(vao_lines_, 3, 0);
gl_->glVertexArrayVertexBuffer(vao_lines_, 0, vbo_lines_, 0, stride);
// Screen-rect VAO/VBO: 2 floats per vertex (vec2 NDC).
gl_->glCreateVertexArrays(1, &vao_rect_);
gl_->glCreateBuffers(1, &vbo_rect_);
gl_->glEnableVertexArrayAttrib(vao_rect_, 0);
gl_->glVertexArrayAttribFormat(vao_rect_, 0, 2, GL_FLOAT, GL_FALSE, 0);
gl_->glVertexArrayAttribBinding(vao_rect_, 0, 0);
gl_->glVertexArrayVertexBuffer(vao_rect_, 0, vbo_rect_, 0, 2 * sizeof(float));
}
void OverlayRenderer::release() {
if (!gl_) return;
if (triangles_.vbo) gl_->glDeleteBuffers(1, &triangles_.vbo);
if (triangles_.vao) gl_->glDeleteVertexArrays(1, &triangles_.vao);
if (points_.vbo) gl_->glDeleteBuffers(1, &points_.vbo);
if (points_.vao) gl_->glDeleteVertexArrays(1, &points_.vao);
if (vbo_lines_) gl_->glDeleteBuffers(1, &vbo_lines_);
if (vao_lines_) gl_->glDeleteVertexArrays(1, &vao_lines_);
if (vbo_rect_) gl_->glDeleteBuffers(1, &vbo_rect_);
if (vao_rect_) gl_->glDeleteVertexArrays(1, &vao_rect_);
if (program_tri_) gl_->glDeleteProgram(program_tri_);
if (program_pt_) gl_->glDeleteProgram(program_pt_);
if (program_ln_) gl_->glDeleteProgram(program_ln_);
if (program_rect_) gl_->glDeleteProgram(program_rect_);
triangles_ = {};
points_ = {};
line_draws_.clear();
vao_lines_ = vbo_lines_ = 0;
vbo_lines_capacity_ = 0;
vao_rect_ = vbo_rect_ = 0;
vbo_rect_capacity_ = 0;
program_tri_ = program_pt_ = program_ln_ = program_rect_ = 0;
gl_ = nullptr;
}
void OverlayRenderer::setHudText(const QString& text) {
hud_text_ = text;
}
void OverlayRenderer::setOverlayLabels(const std::vector