mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
ifcviewer-full: length tool (2/3/4+ point distance, angle, polygon area)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -200,7 +200,8 @@ private:
|
||||
QString pending_camera_;
|
||||
int pending_benchmark_ = 0;
|
||||
|
||||
AreaMeasurement area_measurement_;
|
||||
AreaMeasurement area_measurement_;
|
||||
LengthMeasurement length_measurement_;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -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<float, 3>& a, const std::array<float, 3>& 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<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const std::array<float, 3>& 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<std::array<float, 3>>& 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<double>::infinity());
|
||||
Vec3d bbox_max = Vec3d::Constant(-std::numeric_limits<double>::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<Mat3d> 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<std::array<double, 2>> 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<float> 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<float> seg_xyz;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
|
||||
if (points_.size() >= 2) {
|
||||
const size_t n = points_.size();
|
||||
seg_xyz.reserve(n * 6);
|
||||
labels.reserve(n);
|
||||
auto pushSegment = [&](const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& 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);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#define IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <QString>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -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<std::array<float, 3>> points_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
@@ -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<float>& 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<float>& endpoints,
|
||||
std::vector<float>& 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<Label>& labels) {
|
||||
labels_ = labels;
|
||||
}
|
||||
|
||||
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;
|
||||
triangles_.color[0] = r; triangles_.color[1] = g;
|
||||
triangles_.color[2] = b; triangles_.color[3] = a;
|
||||
triangles_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, triangles_.vbo, triangles_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
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::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_) return;
|
||||
points_.inner_color[0] = r; points_.inner_color[1] = g;
|
||||
points_.inner_color[2] = b; points_.inner_color[3] = a;
|
||||
points_.stroke_color[0] = sr; points_.stroke_color[1] = sg;
|
||||
points_.stroke_color[2] = sb; points_.stroke_color[3] = sa;
|
||||
points_.pixel_size = pixel_size;
|
||||
points_.stroke_extra = stroke_extra;
|
||||
points_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, points_.vbo, points_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLines(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float line_width,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_) return;
|
||||
lines_.inner_color[0] = r; lines_.inner_color[1] = g;
|
||||
lines_.inner_color[2] = b; lines_.inner_color[3] = a;
|
||||
lines_.stroke_color[0] = sr; lines_.stroke_color[1] = sg;
|
||||
lines_.stroke_color[2] = sb; lines_.stroke_color[3] = sa;
|
||||
lines_.line_width = line_width;
|
||||
lines_.stroke_extra = stroke_extra;
|
||||
|
||||
std::vector<float> expanded;
|
||||
expandLineSegments(world_xyz, expanded);
|
||||
lines_.vertex_count = GLsizei(expanded.size() / 8);
|
||||
uploadFloats(gl_, lines_.vbo, lines_.vbo_capacity, expanded);
|
||||
}
|
||||
|
||||
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 GL state we touch.
|
||||
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
|
||||
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_pt_size = gl_->glIsEnabled(GL_PROGRAM_POINT_SIZE);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
gl_->glDepthMask(GL_FALSE);
|
||||
gl_->glDepthFunc(GL_LEQUAL);
|
||||
gl_->glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
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
|
||||
if (triangles_.vertex_count > 0 && triangles_.color[3] > 0.0f) {
|
||||
// Highlight triangles stay depth-aware (GL_LEQUAL) so they tint
|
||||
// the surface in place rather than poking through walls.
|
||||
gl_->glUseProgram(program_tri_);
|
||||
gl_->glUniformMatrix4fv(u_tri_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform4fv(u_tri_color_, 1, triangles_.color);
|
||||
gl_->glBindVertexArray(triangles_.vao);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, triangles_.vertex_count);
|
||||
}
|
||||
// Measurement annotations (lines + points) draw on top of every other
|
||||
// pass — the standard CAD convention. GL_ALWAYS wins every depth
|
||||
// compare; GL_LEQUAL is restored at the end of the function.
|
||||
gl_->glDepthFunc(GL_ALWAYS);
|
||||
if (lines_.vertex_count > 0 && lines_.inner_color[3] > 0.0f) {
|
||||
gl_->glUseProgram(program_ln_);
|
||||
gl_->glUniformMatrix4fv(u_ln_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform2f(u_ln_screen_size_, float(pixel_w), float(pixel_h));
|
||||
gl_->glUniform1f(u_ln_half_width_, lines_.line_width * 0.5f);
|
||||
gl_->glUniform1f(u_ln_stroke_extra_, lines_.stroke_extra);
|
||||
gl_->glUniform4fv(u_ln_inner_color_, 1, lines_.inner_color);
|
||||
gl_->glUniform4fv(u_ln_stroke_color_, 1, lines_.stroke_color);
|
||||
gl_->glBindVertexArray(lines_.vao);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, lines_.vertex_count);
|
||||
}
|
||||
if (points_.vertex_count > 0 && points_.inner_color[3] > 0.0f) {
|
||||
// Inner-radius ratio in [0, 1]: how much of the sprite is the
|
||||
// inner colour vs the stroke band. pixel_size is the inner-disc
|
||||
// diameter; the sprite (and gl_PointSize) is enlarged by
|
||||
// 2*stroke_extra so the halo has somewhere to draw.
|
||||
const float total = points_.pixel_size + 2.0f * points_.stroke_extra;
|
||||
const float inner_ratio = (total > 0.0f)
|
||||
? (points_.pixel_size / total) : 1.0f;
|
||||
gl_->glUseProgram(program_pt_);
|
||||
gl_->glUniformMatrix4fv(u_pt_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform1f(u_pt_point_size_, total);
|
||||
gl_->glUniform1f(u_pt_inner_radius_, inner_ratio);
|
||||
gl_->glUniform4fv(u_pt_inner_color_, 1, points_.inner_color);
|
||||
gl_->glUniform4fv(u_pt_stroke_color_, 1, points_.stroke_color);
|
||||
gl_->glBindVertexArray(points_.vao);
|
||||
gl_->glDrawArrays(GL_POINTS, 0, points_.vertex_count);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
|
||||
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);
|
||||
if (!prev_pt_size) gl_->glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
gl_->glDepthMask(prev_depth_msk);
|
||||
gl_->glDepthFunc(prev_depth_func);
|
||||
|
||||
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);
|
||||
// Two-stage HUD/label pass: collect rect bounds (in logical pixels) +
|
||||
// text strings, draw all rect backgrounds via GL (screen-space NDC
|
||||
// quads, depth test off), then run a QPainter pass that *only* draws
|
||||
// text on top. Side-stepping QPainter::fillRect entirely avoids the
|
||||
// QOpenGLPaintDevice quirk where solid fills silently drop while
|
||||
// text continues to render.
|
||||
const bool any_painter = !hud_text_.isEmpty() || !labels_.empty();
|
||||
if (!any_painter || pixel_w <= 0 || pixel_h <= 0) return;
|
||||
|
||||
const float logical_w = float(pixel_w) / float(dpr ? dpr : 1.0);
|
||||
const float logical_h = float(pixel_h) / float(dpr ? dpr : 1.0);
|
||||
|
||||
QFont label_font("monospace", 9);
|
||||
label_font.setStyleHint(QFont::TypeWriter);
|
||||
QFont hud_font("monospace", 11);
|
||||
hud_font.setStyleHint(QFont::TypeWriter);
|
||||
const QFontMetrics lfm(label_font);
|
||||
const QFontMetrics hfm(hud_font);
|
||||
|
||||
const int label_pad_x = 4, label_pad_y = 2;
|
||||
const int hud_pad_x = 10, hud_pad_y = 6;
|
||||
const int hud_margin = 12;
|
||||
|
||||
struct PaintItem { QRect bg; QString text; const QFont* font; int align; };
|
||||
std::vector<PaintItem> items;
|
||||
items.reserve(labels_.size() + 1);
|
||||
|
||||
// World-anchored label rects.
|
||||
for (const auto& lbl : labels_) {
|
||||
const float* p = lbl.world_pos;
|
||||
// Column-major: M[col*4 + row].
|
||||
const float wx = view_proj[0]*p[0] + view_proj[4]*p[1] + view_proj[8]*p[2] + view_proj[12];
|
||||
const float wy = view_proj[1]*p[0] + view_proj[5]*p[1] + view_proj[9]*p[2] + view_proj[13];
|
||||
const float ww = view_proj[3]*p[0] + view_proj[7]*p[1] + view_proj[11]*p[2] + view_proj[15];
|
||||
if (ww <= 0.0f) continue; // behind camera
|
||||
const float ndc_x = wx / ww;
|
||||
const float ndc_y = wy / ww;
|
||||
if (ndc_x < -1.0f || ndc_x > 1.0f
|
||||
|| ndc_y < -1.0f || ndc_y > 1.0f) continue;
|
||||
const float sx = (ndc_x * 0.5f + 0.5f) * logical_w;
|
||||
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * logical_h;
|
||||
const int tw = lfm.horizontalAdvance(lbl.text);
|
||||
const int th = lfm.height();
|
||||
QRect bg(int(sx) - tw / 2 - label_pad_x,
|
||||
int(sy) - th / 2 - label_pad_y,
|
||||
tw + 2 * label_pad_x,
|
||||
th + 2 * label_pad_y);
|
||||
items.push_back({bg, lbl.text, &label_font, Qt::AlignCenter});
|
||||
}
|
||||
// HUD rect (always top-left if any text).
|
||||
if (!hud_text_.isEmpty()) {
|
||||
const QStringList lines = hud_text_.split('\n');
|
||||
int tw = 0;
|
||||
for (const auto& ln : lines) tw = qMax(tw, hfm.horizontalAdvance(ln));
|
||||
const int th = hfm.height() * lines.size();
|
||||
QRect bg(hud_margin, hud_margin,
|
||||
tw + 2 * hud_pad_x,
|
||||
th + 2 * hud_pad_y);
|
||||
items.push_back({bg, hud_text_, &hud_font, int(Qt::AlignLeft | Qt::AlignTop)});
|
||||
}
|
||||
|
||||
// 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);
|
||||
// GL pass: draw all background rects as NDC-space triangles.
|
||||
if (!items.empty()) {
|
||||
std::vector<float> ndc;
|
||||
ndc.reserve(items.size() * 12); // 6 verts * 2 floats per rect
|
||||
auto px_to_ndc_x = [logical_w](float px) {
|
||||
return (px / logical_w) * 2.0f - 1.0f;
|
||||
};
|
||||
auto px_to_ndc_y = [logical_h](float px) {
|
||||
return 1.0f - (px / logical_h) * 2.0f;
|
||||
};
|
||||
for (const auto& it : items) {
|
||||
const float x0 = px_to_ndc_x(float(it.bg.left()));
|
||||
const float x1 = px_to_ndc_x(float(it.bg.right() + 1));
|
||||
const float y0 = px_to_ndc_y(float(it.bg.top()));
|
||||
const float y1 = px_to_ndc_y(float(it.bg.bottom() + 1));
|
||||
ndc.insert(ndc.end(), {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
});
|
||||
}
|
||||
const size_t bytes = ndc.size() * sizeof(float);
|
||||
if (bytes > vbo_rect_capacity_) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl_->glNamedBufferData(vbo_rect_, GLsizeiptr(new_cap),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
vbo_rect_capacity_ = new_cap;
|
||||
}
|
||||
gl_->glNamedBufferSubData(vbo_rect_, 0, GLsizeiptr(bytes), ndc.data());
|
||||
|
||||
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);
|
||||
// GL_TRIANGLES respects GL_CULL_FACE; the NDC→window y-flip turns
|
||||
// our CCW NDC quads into window-CW which get back-culled if cull
|
||||
// is on (which it is by default in this app). Disable cull for
|
||||
// the rect pass — lines+points above were unaffected since
|
||||
// GL_LINES / GL_POINTS skip face culling entirely.
|
||||
GLboolean prev_depth_test = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cull_face = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_BLEND);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glUniform4f(u_rect_color_, 0.08f, 0.08f, 0.08f, 1.0f);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, GLsizei(items.size() * 6));
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_depth_test) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cull_face) gl_->glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
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_);
|
||||
// QPainter pass: text only, on top of the GL-drawn backgrounds.
|
||||
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
|
||||
device.setDevicePixelRatio(dpr);
|
||||
QPainter painter(&device);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
painter.setPen(Qt::white);
|
||||
for (const auto& it : items) {
|
||||
painter.setFont(*it.font);
|
||||
const int px = it.font == &hud_font ? hud_pad_x : label_pad_x;
|
||||
const int py = it.font == &hud_font ? hud_pad_y : label_pad_y;
|
||||
painter.drawText(it.bg.adjusted(px, py, -px, -py), it.align, it.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,42 @@ public:
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// Replace the overlay-line list (3 floats per vertex, 2 verts per
|
||||
// segment, world space).
|
||||
//
|
||||
// When stroke_a > 0, every segment is rendered twice — first a wider
|
||||
// (line_width + 2*stroke_extra) stroke pass, then the inner line_width
|
||||
// pass. Most desktop GL drivers clamp glLineWidth at ~1, so the
|
||||
// stroke pass on lines may visually collapse onto the inner; reliable
|
||||
// two-tone outlining will need a screen-space-quad thick-line shader.
|
||||
void setOverlayLines(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float line_width,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// `pixel_size` is the inner-dot diameter in physical pixels.
|
||||
//
|
||||
// When stroke_a > 0, every point is rendered twice: a wider
|
||||
// (pixel_size + 2*stroke_extra) outer dot in stroke_color, then the
|
||||
// pixel_size inner dot in the main color — giving a crisp halo that
|
||||
// reads on any background.
|
||||
void setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// World-anchored text labels: each one is projected to screen space
|
||||
// and drawn via QPainter at that pixel. Used today for per-segment
|
||||
// length readouts in the length tool.
|
||||
struct Label {
|
||||
float world_pos[3];
|
||||
QString text;
|
||||
};
|
||||
void setOverlayLabels(const std::vector<Label>& labels);
|
||||
|
||||
// Top-left HUD text drawn via QPainter on the GL surface as part of
|
||||
// render(). Empty hides the HUD.
|
||||
void setHudText(const QString& text);
|
||||
@@ -60,16 +96,86 @@ public:
|
||||
int pixel_w, int pixel_h, qreal device_pixel_ratio);
|
||||
|
||||
private:
|
||||
// Triangle bundle: position-only VBO, single-color shader.
|
||||
struct TriBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float color[4] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Point bundle: position-only VBO, sprite shader uses gl_PointCoord
|
||||
// to draw an antialiased disc with an outlined halo.
|
||||
struct PointBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float inner_color[4] = {0, 0, 0, 0};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float pixel_size = 8.0f;
|
||||
float stroke_extra = 0.0f;
|
||||
};
|
||||
|
||||
// Line bundle: each input segment is CPU-expanded into 6 vertices
|
||||
// (a quad as 2 triangles), each carrying both endpoints and a
|
||||
// (side, along) corner index. The vertex shader projects to screen,
|
||||
// computes the screen-space perpendicular, and offsets accordingly;
|
||||
// the fragment shader uses the interpolated signed perpendicular
|
||||
// distance to discard outside the half-width and to pick inner vs
|
||||
// stroke color. Result: real outlined lines independent of the
|
||||
// driver's glLineWidth clamp.
|
||||
struct LineBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float inner_color[4] = {0, 0, 0, 0};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float line_width = 1.0f;
|
||||
float stroke_extra = 0.0f;
|
||||
};
|
||||
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
GLuint program_ = 0;
|
||||
GLuint vao_ = 0;
|
||||
GLuint vbo_ = 0;
|
||||
size_t vbo_capacity_ = 0; // bytes
|
||||
GLsizei vertex_count_ = 0;
|
||||
float color_[4] = {0, 0, 0, 0};
|
||||
GLint u_view_proj_ = -1;
|
||||
GLint u_color_ = -1;
|
||||
QString hud_text_;
|
||||
|
||||
// Triangle program (highlight tris).
|
||||
GLuint program_tri_ = 0;
|
||||
GLint u_tri_view_proj_ = -1;
|
||||
GLint u_tri_color_ = -1;
|
||||
|
||||
// Point program (sprite).
|
||||
GLuint program_pt_ = 0;
|
||||
GLint u_pt_view_proj_ = -1;
|
||||
GLint u_pt_point_size_ = -1;
|
||||
GLint u_pt_inner_color_ = -1;
|
||||
GLint u_pt_stroke_color_ = -1;
|
||||
GLint u_pt_inner_radius_ = -1;
|
||||
|
||||
// Line program (screen-space expanded quads).
|
||||
GLuint program_ln_ = 0;
|
||||
GLint u_ln_view_proj_ = -1;
|
||||
GLint u_ln_screen_size_ = -1;
|
||||
GLint u_ln_half_width_ = -1;
|
||||
GLint u_ln_stroke_extra_ = -1;
|
||||
GLint u_ln_inner_color_ = -1;
|
||||
GLint u_ln_stroke_color_ = -1;
|
||||
|
||||
// Screen-space rect program (label + HUD backgrounds). Vertex
|
||||
// attribute is vec2 NDC; fragment outputs a uniform color. Drawn
|
||||
// with depth test off so rects stack on top of the entire scene.
|
||||
GLuint program_rect_ = 0;
|
||||
GLint u_rect_color_ = -1;
|
||||
GLuint vao_rect_ = 0;
|
||||
GLuint vbo_rect_ = 0;
|
||||
size_t vbo_rect_capacity_ = 0;
|
||||
|
||||
TriBundle triangles_;
|
||||
PointBundle points_;
|
||||
LineBundle lines_;
|
||||
|
||||
std::vector<Label> labels_;
|
||||
QString hud_text_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_OVERLAYRENDERER_H
|
||||
|
||||
@@ -1690,12 +1690,19 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Esc also exits the area tool.
|
||||
if (area_tool_active_
|
||||
&& key == Qt::Key_Escape
|
||||
&& !event->isAutoRepeat()) {
|
||||
toggleAreaTool();
|
||||
return;
|
||||
// Esc exits any active measurement tool. Backspace/Delete in length
|
||||
// mode removes the last point — emitted as a signal because the
|
||||
// library doesn't track per-tool semantic state.
|
||||
if (tool_mode_ != ToolMode::None && !event->isAutoRepeat()) {
|
||||
if (key == Qt::Key_Escape) {
|
||||
setToolMode(ToolMode::None);
|
||||
return;
|
||||
}
|
||||
if (tool_mode_ == ToolMode::Length
|
||||
&& (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
|
||||
emit toolBackspacePressed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
QWindow::keyPressEvent(event);
|
||||
}
|
||||
@@ -3492,7 +3499,7 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
||||
if (active_button_ == Qt::LeftButton
|
||||
&& !section_tool_active_
|
||||
&& (e->pos() - last_mouse_pos_).manhattanLength() < 5) {
|
||||
if (area_tool_active_) {
|
||||
if (tool_mode_ != ToolMode::None) {
|
||||
emit surfacePickedInTool(e->pos().x(), e->pos().y(),
|
||||
int(e->modifiers()));
|
||||
} else {
|
||||
@@ -3868,9 +3875,18 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ViewportWindow::setToolMode(ToolMode mode) {
|
||||
if (tool_mode_ == mode) return;
|
||||
tool_mode_ = mode;
|
||||
emit toolModeChanged(mode);
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleAreaTool() {
|
||||
area_tool_active_ = !area_tool_active_;
|
||||
emit areaToolToggled(area_tool_active_);
|
||||
setToolMode(tool_mode_ == ToolMode::Area ? ToolMode::None : ToolMode::Area);
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleLengthTool() {
|
||||
setToolMode(tool_mode_ == ToolMode::Length ? ToolMode::None : ToolMode::Length);
|
||||
}
|
||||
|
||||
void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
@@ -3881,6 +3897,35 @@ void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayLines(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float line_width,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_initialized_) return;
|
||||
context_->makeCurrent(this);
|
||||
overlay_renderer_.setOverlayLines(world_xyz, r, g, b, a, line_width,
|
||||
sr, sg, sb, sa, stroke_extra);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_initialized_) return;
|
||||
context_->makeCurrent(this);
|
||||
overlay_renderer_.setOverlayPoints(world_xyz, r, g, b, a, pixel_size,
|
||||
sr, sg, sb, sa, stroke_extra);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels) {
|
||||
overlay_renderer_.setOverlayLabels(labels);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setHudText(const QString& text) {
|
||||
overlay_renderer_.setHudText(text);
|
||||
requestUpdate();
|
||||
|
||||
@@ -230,12 +230,18 @@ public:
|
||||
};
|
||||
bool pickMeshLocalAt(int x, int y, MeshLocalPick& out);
|
||||
|
||||
// Area tool: while active, LMB clicks emit surfacePickedInTool with
|
||||
// the click coordinates instead of swapping object selection — the
|
||||
// app interprets them (typically by calling pickMeshLocalAt and
|
||||
// accumulating triangle area). Esc exits.
|
||||
// Measurement tool modes. While any tool is active, LMB clicks emit
|
||||
// surfacePickedInTool with the click coordinates (instead of swapping
|
||||
// object selection); the app interprets them per-tool. Esc exits the
|
||||
// active tool. Backspace/Delete in length mode emits
|
||||
// toolBackspacePressed for "remove last point" semantics.
|
||||
enum class ToolMode { None, Area, Length };
|
||||
Q_ENUM(ToolMode)
|
||||
|
||||
void toggleAreaTool();
|
||||
bool areaToolActive() const { return area_tool_active_; }
|
||||
void toggleLengthTool();
|
||||
void setToolMode(ToolMode mode);
|
||||
ToolMode toolMode() const { return tool_mode_; }
|
||||
|
||||
// Replace the overlay highlight-triangle list rendered after the main
|
||||
// pass. `world_xyz` is 3 floats per vertex, 3 verts per triangle, in
|
||||
@@ -244,10 +250,35 @@ public:
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// Top-left HUD text drawn via QPainter on top of the GL surface at
|
||||
// the end of each frame. Empty hides the HUD. Used today by the
|
||||
// area-measurement tool for the running total; later tools can pile
|
||||
// additional readouts in by extending this with a multi-line API.
|
||||
// Replace the overlay-line list (3 floats per vertex, 2 verts per
|
||||
// segment, world space). When stroke_a > 0 each segment is rendered
|
||||
// with a wider (line_width + 2*stroke_extra) halo behind the inner
|
||||
// line_width — proper outlined lines via screen-space-quad shader.
|
||||
void setOverlayLines(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float line_width,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// pixel_size is the inner-disc diameter in physical pixels; when
|
||||
// stroke_a > 0 the sprite is enlarged by 2*stroke_extra to draw a
|
||||
// halo around the inner disc — proper outlined points via the
|
||||
// sprite shader (no two-pass glPointSize trickery).
|
||||
void setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// Replace the world-anchored label list — projected to screen space
|
||||
// and drawn via QPainter inside the same overlay pass as the HUD.
|
||||
// Used today for per-segment length readouts.
|
||||
void setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels);
|
||||
|
||||
// Multi-line HUD text drawn via QPainter on top of the GL surface at
|
||||
// the end of each frame. Empty hides the HUD. Newlines split the
|
||||
// string into separate rows in the same translucent box.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Federation pipeline: composed instance transform =
|
||||
@@ -344,14 +375,17 @@ signals:
|
||||
void objectPicked(uint32_t object_id);
|
||||
void initialized();
|
||||
void frameStatsUpdated(const ViewportWindow::FrameStats& stats);
|
||||
// Emitted instead of objectPicked when the area tool is active. The
|
||||
// app is expected to call pickMeshLocalAt(x, y, ...) and accumulate.
|
||||
// modifiers carries the Qt::KeyboardModifiers held at click time so
|
||||
// the app can branch on Alt etc.
|
||||
// Emitted instead of objectPicked when any measurement tool is active.
|
||||
// The app branches on toolMode() to decide what to do, calls
|
||||
// pickMeshLocalAt(x, y, ...) for hit details, and accumulates.
|
||||
// modifiers carries Qt::KeyboardModifiers at click time (Alt etc.).
|
||||
void surfacePickedInTool(int x, int y, int modifiers);
|
||||
// Emitted whenever toggleAreaTool flips the mode. The app uses this
|
||||
// to reset accumulator state on entry/exit.
|
||||
void areaToolToggled(bool active);
|
||||
// Emitted whenever the active tool changes (incl. on→off transitions).
|
||||
// The app uses this to reset accumulator state on entry/exit.
|
||||
void toolModeChanged(ViewportWindow::ToolMode mode);
|
||||
// Backspace/Delete pressed while a tool is active. Used by the length
|
||||
// tool to remove the last point; other tools may ignore it.
|
||||
void toolBackspacePressed();
|
||||
|
||||
protected:
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
@@ -651,8 +685,8 @@ private:
|
||||
// Selection
|
||||
uint32_t selected_object_id_ = 0;
|
||||
|
||||
// Area-measurement tool: see toggleAreaTool / surfacePickedInTool.
|
||||
bool area_tool_active_ = false;
|
||||
// Active measurement tool: see ToolMode / surfacePickedInTool.
|
||||
ToolMode tool_mode_ = ToolMode::None;
|
||||
|
||||
// Renders any client-supplied overlay primitives (highlight triangles
|
||||
// today; lines/points/labels later) in their own pass after the main
|
||||
|
||||
Reference in New Issue
Block a user