From 5bc7e4b98bb1aefc0e0dd8d892e0b82ae91142cf Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 7 May 2026 21:41:47 +1000 Subject: [PATCH] ifcviewer-full: length tool (2/3/4+ point distance, angle, polygon area) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ViewportWindow trades the area_tool_active_ bool for an enum ToolMode {None, Area, Length}; the existing surfacePickedInTool signal carries both, the app dispatches on toolMode(). Esc exits any active tool; Backspace/Delete in length mode emits toolBackspacePressed which the length tool uses to remove the last point. LengthMeasurement collects clicked world-space points and adapts the readout: 2pt → distance + axis-aligned ΔX/ΔY/ΔZ, 3pt → angle at the middle vertex + triangle area, 4+pt → best-fit-plane PCA + shoelace when planar (RMS plane distance / bbox diag < 1e-3) else fan triangulation, with the chosen method labelled in the readout. Per- segment lengths float at each midpoint. OverlayRenderer grows three new pipelines to support this: - point sprite shader: gl_PointCoord-based outlined disc with fwidth-smoothed inner/stroke bands, a single draw call. - line shader: CPU-expand each segment to 6 verts carrying both endpoints + (side, along) corner index; vertex shader computes the screen-space perpendicular and offsets accordingly. Real outlined lines independent of the driver's glLineWidth clamp. - screen-space rect shader: HUD + label backgrounds drawn as raw GL quads in NDC. QPainter::fillRect on QOpenGLPaintDevice was silently dropping fills across drivers; bypassing it entirely via this shader makes backgrounds reliable. Cull-face is also explicitly disabled here — GL_TRIANGLES respects it but the line/point primitives don't, so this was the one path needing the fix. setOverlayLines / setOverlayPoints take an inner color, an outline color, and an extra-pixels-per-side stroke amount. Lines + points draw with GL_ALWAYS so measurement annotations stay visible through geometry; highlight tris stay depth-aware (GL_LEQUAL) so area shading still tints the surface in place. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-full/MainWindow.cpp | 43 ++- src/ifcviewer-full/MainWindow.h | 3 +- src/ifcviewer-full/Measurement.cpp | 236 ++++++++++++ src/ifcviewer-full/Measurement.h | 31 ++ src/ifcviewer/OverlayRenderer.cpp | 579 ++++++++++++++++++++++++----- src/ifcviewer/OverlayRenderer.h | 124 +++++- src/ifcviewer/ViewportWindow.cpp | 63 +++- src/ifcviewer/ViewportWindow.h | 70 +++- 8 files changed, 1019 insertions(+), 130 deletions(-) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index d7b7886b8c..4ac9225985 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -209,20 +209,44 @@ void MainWindow::setupUi() { connect(viewport_, &ViewportWindow::surfacePickedInTool, this, [this](int x, int y, int modifiers) { const bool alt = (modifiers & Qt::AltModifier) != 0; - area_measurement_.onPick(*viewport_, x, y, alt); - viewport_->setHudText(QString("Area: %1 m² (%2 tris)") - .arg(area_measurement_.totalArea(), 0, 'f', 4) - .arg(area_measurement_.triangleCount())); + switch (viewport_->toolMode()) { + case ViewportWindow::ToolMode::Area: + area_measurement_.onPick(*viewport_, x, y, alt); + viewport_->setHudText(QString("Area: %1 m² (%2 tris)") + .arg(area_measurement_.totalArea(), 0, 'f', 4) + .arg(area_measurement_.triangleCount())); + break; + case ViewportWindow::ToolMode::Length: + length_measurement_.onPick(*viewport_, x, y, alt); + break; + case ViewportWindow::ToolMode::None: + break; + } }); - connect(viewport_, &ViewportWindow::areaToolToggled, this, - [this](bool active) { + connect(viewport_, &ViewportWindow::toolModeChanged, this, + [this](ViewportWindow::ToolMode mode) { + // Always clear both — the previous tool's accumulator/overlay + // shouldn't bleed into the next one. area_measurement_.clear(*viewport_); - if (active) { + length_measurement_.clear(*viewport_); + switch (mode) { + case ViewportWindow::ToolMode::Area: viewport_->setHudText("Area: 0.0000 m² (0 tris)"); status_label_->setText("Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits"); - } else { + break; + case ViewportWindow::ToolMode::Length: + viewport_->setHudText("Length tool: click first point"); + status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits"); + break; + case ViewportWindow::ToolMode::None: viewport_->setHudText(QString()); status_label_->setText("Ready"); + break; + } + }); + connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() { + if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) { + length_measurement_.removeLastPoint(*viewport_); } }); @@ -305,6 +329,9 @@ void MainWindow::setupMenus() { view_menu->addAction("&Measure Area", this, [this]() { viewport_->toggleAreaTool(); }, QKeySequence("Ctrl+Shift+A")); + view_menu->addAction("Measure &Length", this, [this]() { + viewport_->toggleLengthTool(); + }, QKeySequence("Ctrl+Shift+L")); view_menu->addSeparator(); view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView); view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView); diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index fa1bc35f91..e518b9c9a8 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -200,7 +200,8 @@ private: QString pending_camera_; int pending_benchmark_ = 0; - AreaMeasurement area_measurement_; + AreaMeasurement area_measurement_; + LengthMeasurement length_measurement_; }; #endif // MAINWINDOW_H diff --git a/src/ifcviewer-full/Measurement.cpp b/src/ifcviewer-full/Measurement.cpp index d487d7bd83..45fba9a82c 100644 --- a/src/ifcviewer-full/Measurement.cpp +++ b/src/ifcviewer-full/Measurement.cpp @@ -22,6 +22,7 @@ #include "ViewportWindow.h" #include +#include #include #include @@ -365,3 +366,238 @@ void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) { delta >= 0.0 ? "+" : "", delta, total_area_m2_, selected_.size()); } + +// ----- LengthMeasurement ----------------------------------------------------- + +namespace { + +double dist3(const std::array& a, const std::array& b) { + const double dx = double(b[0]) - a[0]; + const double dy = double(b[1]) - a[1]; + const double dz = double(b[2]) - a[2]; + return std::sqrt(dx*dx + dy*dy + dz*dz); +} + +double triArea3(const std::array& a, + const std::array& b, + const std::array& c) { + const double bax = double(b[0]) - a[0]; + const double bay = double(b[1]) - a[1]; + const double baz = double(b[2]) - a[2]; + const double cax = double(c[0]) - a[0]; + const double cay = double(c[1]) - a[1]; + const double caz = double(c[2]) - a[2]; + const double nx = bay * caz - baz * cay; + const double ny = baz * cax - bax * caz; + const double nz = bax * cay - bay * cax; + return 0.5 * std::sqrt(nx*nx + ny*ny + nz*nz); +} + +// Polygon area via best-fit plane + shoelace, falling back to fan +// triangulation when the points stray off the plane. Returns the +// resulting area and a label naming which path was taken. +struct PolygonAreaResult { + double area_m2; + const char* method; +}; + +PolygonAreaResult polygonArea(const std::vector>& pts) { + using Vec3d = Eigen::Vector3d; + using Mat3d = Eigen::Matrix3d; + const size_t n = pts.size(); + + // Centroid + bounding box (for the planarity threshold). + Vec3d centroid = Vec3d::Zero(); + Vec3d bbox_min = Vec3d::Constant(std::numeric_limits::infinity()); + Vec3d bbox_max = Vec3d::Constant(-std::numeric_limits::infinity()); + for (const auto& p : pts) { + const Vec3d v(p[0], p[1], p[2]); + centroid += v; + bbox_min = bbox_min.cwiseMin(v); + bbox_max = bbox_max.cwiseMax(v); + } + centroid /= double(n); + const double bbox_diag = (bbox_max - bbox_min).norm(); + + // 3x3 covariance. Smallest eigenvector of this is the plane normal. + Mat3d cov = Mat3d::Zero(); + for (const auto& p : pts) { + const Vec3d d = Vec3d(p[0], p[1], p[2]) - centroid; + cov += d * d.transpose(); + } + + Eigen::SelfAdjointEigenSolver es(cov); + const Vec3d normal = es.eigenvectors().col(0); // smallest eigenvalue + + // RMS plane distance, normalised against the bounding-box diagonal. + double sq_sum = 0.0; + for (const auto& p : pts) { + const double d = (Vec3d(p[0], p[1], p[2]) - centroid).dot(normal); + sq_sum += d * d; + } + const double rms = std::sqrt(sq_sum / double(n)); + const bool planar = bbox_diag > 0.0 && (rms / bbox_diag) < 1e-3; + + if (planar) { + // Build an in-plane orthonormal basis. + Vec3d u = normal.cross(Vec3d::UnitX()); + if (u.squaredNorm() < 1e-6) u = normal.cross(Vec3d::UnitY()); + u.normalize(); + const Vec3d v = normal.cross(u); + + // Project + shoelace. + std::vector> uv(n); + for (size_t i = 0; i < n; ++i) { + const Vec3d d = Vec3d(pts[i][0], pts[i][1], pts[i][2]) - centroid; + uv[i][0] = d.dot(u); + uv[i][1] = d.dot(v); + } + double s = 0.0; + for (size_t i = 0; i < n; ++i) { + const auto& a = uv[i]; + const auto& b = uv[(i + 1) % n]; + s += a[0] * b[1] - b[0] * a[1]; + } + return { 0.5 * std::abs(s), "planar" }; + } + + // Fan from p0. Works for star-shaped polygons; for genuinely twisted + // 3D point sets it's a heuristic — flagged in the method label. + double area = 0.0; + for (size_t i = 1; i + 1 < n; ++i) { + area += triArea3(pts[0], pts[i], pts[i + 1]); + } + return { area, "fan-triangulated (non-planar)" }; +} + +} // namespace + +LengthMeasurement::LengthMeasurement() = default; + +void LengthMeasurement::clear(ViewportWindow& vp) { + points_.clear(); + vp.setOverlayPoints({}, 0,0,0,0, 0, + 0,0,0,0, 0); + vp.setOverlayLines({}, 0,0,0,0, 0, + 0,0,0,0, 0); + vp.setOverlayLabels({}); + vp.setHudText(QString()); +} + +void LengthMeasurement::onPick(ViewportWindow& vp, int x, int y, bool /*alt*/) { + ViewportWindow::MeshLocalPick pick; + if (!vp.pickMeshLocalAt(x, y, pick)) return; + points_.push_back({pick.world_pos[0], pick.world_pos[1], pick.world_pos[2]}); + rebuildOverlay(vp); +} + +void LengthMeasurement::removeLastPoint(ViewportWindow& vp) { + if (points_.empty()) return; + points_.pop_back(); + rebuildOverlay(vp); +} + +void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) { + // Points: orange inner with thin black halo — readable on every + // background. Inner 8px disc + 2px halo each side. + std::vector pts_xyz; + pts_xyz.reserve(points_.size() * 3); + for (const auto& p : points_) { + pts_xyz.push_back(p[0]); + pts_xyz.push_back(p[1]); + pts_xyz.push_back(p[2]); + } + vp.setOverlayPoints(pts_xyz, + /*inner*/ 1.00f, 1.00f, 1.00f, 1.00f, + /*size*/ 8.0f, + /*stroke*/ 0.00f, 0.00f, 0.00f, 0.85f, + /*extra*/ 2.0f); + + // Connecting polyline. For 4+ points also close the polygon since + // that's the area-readout shape. Same orange + halo treatment. + std::vector seg_xyz; + std::vector labels; + + if (points_.size() >= 2) { + const size_t n = points_.size(); + seg_xyz.reserve(n * 6); + labels.reserve(n); + auto pushSegment = [&](const std::array& a, + const std::array& b) { + seg_xyz.insert(seg_xyz.end(), a.begin(), a.end()); + seg_xyz.insert(seg_xyz.end(), b.begin(), b.end()); + OverlayRenderer::Label lbl; + lbl.world_pos[0] = 0.5f * (a[0] + b[0]); + lbl.world_pos[1] = 0.5f * (a[1] + b[1]); + lbl.world_pos[2] = 0.5f * (a[2] + b[2]); + lbl.text = QString::number(dist3(a, b), 'f', 3) + " m"; + labels.push_back(std::move(lbl)); + }; + for (size_t i = 0; i + 1 < n; ++i) pushSegment(points_[i], points_[i + 1]); + if (n >= 4) pushSegment(points_[n - 1], points_[0]); + } + vp.setOverlayLines(seg_xyz, + /*inner*/ 1.00f, 1.00f, 1.00f, 1.00f, + /*width*/ 2.0f, + /*stroke*/ 0.00f, 0.00f, 0.00f, 0.85f, + /*extra*/ 1.5f); + vp.setOverlayLabels(labels); + vp.setHudText(formatReadout()); +} + +QString LengthMeasurement::formatReadout() const { + const size_t n = points_.size(); + if (n == 0) return QStringLiteral("Length tool: click first point"); + if (n == 1) return QStringLiteral("1 point (click another)"); + + if (n == 2) { + const auto& a = points_[0]; + const auto& b = points_[1]; + const double d = dist3(a, b); + const double dx = std::abs(double(b[0]) - a[0]); + const double dy = std::abs(double(b[1]) - a[1]); + const double dz = std::abs(double(b[2]) - a[2]); + return QString("Length: %1 m\nΔX: %2 ΔY: %3 ΔZ: %4 m") + .arg(d, 0, 'f', 4) + .arg(dx, 0, 'f', 4) + .arg(dy, 0, 'f', 4) + .arg(dz, 0, 'f', 4); + } + + if (n == 3) { + const auto& a = points_[0]; + const auto& b = points_[1]; + const auto& c = points_[2]; + // Angle at b (the middle-clicked vertex). + const double bax = double(a[0]) - b[0]; + const double bay = double(a[1]) - b[1]; + const double baz = double(a[2]) - b[2]; + const double bcx = double(c[0]) - b[0]; + const double bcy = double(c[1]) - b[1]; + const double bcz = double(c[2]) - b[2]; + const double la = std::sqrt(bax*bax + bay*bay + baz*baz); + const double lc = std::sqrt(bcx*bcx + bcy*bcy + bcz*bcz); + double angle_deg = 0.0; + if (la > 0.0 && lc > 0.0) { + const double cosang = std::clamp( + (bax*bcx + bay*bcy + baz*bcz) / (la * lc), -1.0, 1.0); + angle_deg = std::acos(cosang) * 180.0 / M_PI; + } + return QString("Angle at pt 2: %1°\nTriangle area: %2 m²\nPerimeter: %3 m") + .arg(angle_deg, 0, 'f', 2) + .arg(triArea3(a, b, c), 0, 'f', 4) + .arg(dist3(a, b) + dist3(b, c) + dist3(c, a), 0, 'f', 4); + } + + // 4+ points: polygon area. + const PolygonAreaResult r = polygonArea(points_); + double perimeter = 0.0; + for (size_t i = 0; i < n; ++i) { + perimeter += dist3(points_[i], points_[(i + 1) % n]); + } + return QString("Polygon (%1 pts, %2)\nArea: %3 m²\nPerimeter: %4 m") + .arg(n) + .arg(r.method) + .arg(r.area_m2, 0, 'f', 4) + .arg(perimeter, 0, 'f', 4); +} diff --git a/src/ifcviewer-full/Measurement.h b/src/ifcviewer-full/Measurement.h index 372356ec23..e6149a4f37 100644 --- a/src/ifcviewer-full/Measurement.h +++ b/src/ifcviewer-full/Measurement.h @@ -21,6 +21,8 @@ #define IFCVIEWER_FULL_MEASUREMENT_H #include +#include +#include #include #include #include @@ -105,4 +107,33 @@ private: double total_area_m2_ = 0.0; }; +// Click-to-place length / angle / area measurement. Each pick appends a +// world-space point. The readout adapts to the point count: +// +// 1 point → "click another point" +// 2 points → straight-line distance plus axis-aligned ΔX/ΔY/ΔZ +// 3 points → angle at the middle vertex plus the triangle's area +// 4+ → polygon area: best-fit-plane shoelace if the points are +// near-coplanar (RMS plane distance < 1e-3 of the bounding +// box), else fan-triangulated from the first point +// +// Clicked points are pushed to the viewport overlay as small dots and +// the connecting polyline; readouts go to the multi-line HUD. +class LengthMeasurement { +public: + LengthMeasurement(); + + void onPick(ViewportWindow& vp, int x, int y, bool alt); + void removeLastPoint(ViewportWindow& vp); + void clear(ViewportWindow& vp); + + size_t pointCount() const { return points_.size(); } + +private: + void rebuildOverlay(ViewportWindow& vp); + QString formatReadout() const; + + std::vector> points_; +}; + #endif // IFCVIEWER_FULL_MEASUREMENT_H diff --git a/src/ifcviewer/OverlayRenderer.cpp b/src/ifcviewer/OverlayRenderer.cpp index 60384061ac..3254528104 100644 --- a/src/ifcviewer/OverlayRenderer.cpp +++ b/src/ifcviewer/OverlayRenderer.cpp @@ -58,7 +58,9 @@ GLuint link(QOpenGLFunctions_4_5_Core* gl, GLuint vs, GLuint fs) { return p; } -const char* VERT_SRC = R"( +// ---- Triangle program (flat color) ---- + +const char* TRI_VS = R"( #version 450 core layout(location = 0) in vec3 in_pos; uniform mat4 u_view_proj; @@ -67,7 +69,7 @@ void main() { } )"; -const char* FRAG_SRC = R"( +const char* TRI_FS = R"( #version 450 core uniform vec4 u_color; out vec4 frag_color; @@ -76,34 +78,284 @@ void main() { } )"; +// ---- 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, and picks inner vs stroke from that. ~1px AA at every band +// boundary using fwidth-style smoothstep with a narrow ramp. +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 aa = fwidth(d) * 1.2; // ~1px feather + float t_inner = smoothstep(u_inner_radius_norm - aa, + u_inner_radius_norm + aa, d); + vec4 col = mix(u_inner_color, u_stroke_color, t_inner); + 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; +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; +} +)"; + +// ---- 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; +uniform vec4 u_inner_color; +uniform vec4 u_stroke_color; +uniform float u_half_width; +uniform float u_stroke_extra; +out vec4 frag_color; +void main() { + float ad = abs(v_dist_px); + float total = u_half_width + u_stroke_extra; + if (ad > total) discard; + + // ~1px AA on the inner/stroke boundary and the outer edge. + float t_stroke = smoothstep(u_half_width - 0.5, u_half_width + 0.5, 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->glNamedBufferData(vbo, GLsizeiptr(new_cap), + nullptr, GL_DYNAMIC_DRAW); + capacity_bytes = new_cap; + } + gl->glNamedBufferSubData(vbo, 0, GLsizeiptr(bytes), data.data()); +} + +// 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; - 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"); + // 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"); + } + // 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"); + } - 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)); + // 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). + gl_->glCreateVertexArrays(1, &lines_.vao); + gl_->glCreateBuffers(1, &lines_.vbo); + const GLsizei stride = 8 * sizeof(float); + gl_->glEnableVertexArrayAttrib(lines_.vao, 0); + gl_->glVertexArrayAttribFormat(lines_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0); + gl_->glVertexArrayAttribBinding(lines_.vao, 0, 0); + gl_->glEnableVertexArrayAttrib(lines_.vao, 1); + gl_->glVertexArrayAttribFormat(lines_.vao, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float)); + gl_->glVertexArrayAttribBinding(lines_.vao, 1, 0); + gl_->glEnableVertexArrayAttrib(lines_.vao, 2); + gl_->glVertexArrayAttribFormat(lines_.vao, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float)); + gl_->glVertexArrayAttribBinding(lines_.vao, 2, 0); + gl_->glEnableVertexArrayAttrib(lines_.vao, 3); + gl_->glVertexArrayAttribFormat(lines_.vao, 3, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float)); + gl_->glVertexArrayAttribBinding(lines_.vao, 3, 0); + gl_->glVertexArrayVertexBuffer(lines_.vao, 0, lines_.vbo, 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 (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; + 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 (lines_.vbo) gl_->glDeleteBuffers(1, &lines_.vbo); + if (lines_.vao) gl_->glDeleteVertexArrays(1, &lines_.vao); + 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_ = {}; + lines_ = {}; + vao_rect_ = vbo_rect_ = 0; + vbo_rect_capacity_ = 0; + program_tri_ = program_pt_ = program_ln_ = program_rect_ = 0; gl_ = nullptr; } @@ -111,89 +363,246 @@ void OverlayRenderer::setHudText(const QString& text) { hud_text_ = text; } +void OverlayRenderer::setOverlayLabels(const std::vector