mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
ifcviewer-full: 1-pt laser, 2-pt XYZ + perpendicular, sharper visuals
Length tool's 1-pt laser is now hybrid:
- On any surface, a coplanar BFS finds the connected face patch
around the click and projects its vertices into the surface
tangent basis to get an exact bounding-box extent. Stops at
the face edge by construction — no overshoot into adjacent
geometry like the previous tangent-raycast did.
- On near-horizontal surfaces (|n.z| > 0.85, i.e. floors and
ceilings) it additionally fires one raycast in +n to the
opposing surface — so a single floor click reports X extent +
Y extent + ceiling height.
- Bars are labelled by their dominant world axis (X/Y/Z) instead
of "vertical/horizontal", which reads cleanly on either kind
of surface.
The 2-pt readout now draws the world-space XYZ stair-step (red ΔX,
green ΔY, blue ΔZ) with each leg labelled, and a dashed
perpendicular line whenever the two picks landed on near-parallel
surfaces — useful for measuring across walls.
To support multiple line styles per frame, OverlayRenderer's
setOverlayLines takes std::vector<LineGroup> instead of a single
inline style; each group has its own color/halo/width and an
optional dash period. The line shader gained v_along_px +
u_dash_period uniforms (screen-space dashes), and both line and
point shaders now use a sharp step() for the inner→stroke
transition with AA only on the outer halo edge — much crisper than
the previous soft band. Default visual style trimmed: 1.5px lines
(0.5px halo), 6px dots (1px halo), opaque black halo.
Also adds ViewportWindow::raycast(origin, dir, RaycastHit&) — CPU
ray traversal of each model's per-instance BVH followed by
Möller-Trumbore against the candidate meshes' triangles (lazily
read back, cached per call). Used by the floor/ceiling laser path
today and reusable for any future raycast-based feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -474,12 +474,70 @@ PolygonAreaResult polygonArea(const std::vector<std::array<float, 3>>& pts) {
|
||||
|
||||
LengthMeasurement::LengthMeasurement() = default;
|
||||
|
||||
namespace {
|
||||
|
||||
// Visual style — reused across all length-tool overlay paths.
|
||||
constexpr float LINE_WIDTH = 1.5f;
|
||||
constexpr float LINE_HALO = 0.5f;
|
||||
constexpr float DOT_SIZE = 6.0f;
|
||||
constexpr float DOT_HALO = 1.0f;
|
||||
constexpr float DASH_PERIOD = 9.0f; // px
|
||||
constexpr float DASH_ON_RATIO = 0.55f; // 5 on, 4 off
|
||||
|
||||
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
|
||||
float r, float g, float b,
|
||||
bool dashed = false) {
|
||||
OverlayRenderer::LineGroup gp;
|
||||
gp.world_xyz = std::move(xyz);
|
||||
gp.color[0] = r; gp.color[1] = g; gp.color[2] = b; gp.color[3] = 1.0f;
|
||||
gp.stroke_color[0] = 0.0f; gp.stroke_color[1] = 0.0f;
|
||||
gp.stroke_color[2] = 0.0f; gp.stroke_color[3] = 1.0f;
|
||||
gp.line_width = LINE_WIDTH;
|
||||
gp.stroke_extra = LINE_HALO;
|
||||
gp.dash_period_px = dashed ? DASH_PERIOD : 0.0f;
|
||||
gp.dash_on_ratio = DASH_ON_RATIO;
|
||||
return gp;
|
||||
}
|
||||
|
||||
void pushDot(std::vector<float>& xyz, const std::array<float, 3>& p) {
|
||||
xyz.push_back(p[0]);
|
||||
xyz.push_back(p[1]);
|
||||
xyz.push_back(p[2]);
|
||||
}
|
||||
|
||||
void pushSeg(std::vector<float>& xyz,
|
||||
const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
xyz.insert(xyz.end(), a.begin(), a.end());
|
||||
xyz.insert(xyz.end(), b.begin(), b.end());
|
||||
}
|
||||
|
||||
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const QString& text) {
|
||||
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 = text;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
|
||||
vp.setOverlayPoints(xyz,
|
||||
/*inner*/ 1.0f, 1.0f, 1.0f, 1.0f,
|
||||
/*size*/ DOT_SIZE,
|
||||
/*stroke*/ 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
/*extra*/ DOT_HALO);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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);
|
||||
normals_.clear();
|
||||
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
|
||||
vp.setOverlayLines({});
|
||||
vp.setOverlayLabels({});
|
||||
vp.setHudText(QString());
|
||||
}
|
||||
@@ -488,63 +546,352 @@ 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]});
|
||||
normals_.push_back({pick.world_normal[0], pick.world_normal[1], pick.world_normal[2]});
|
||||
if (points_.size() == 1) {
|
||||
first_pick_ = pick; // record info the laser BFS needs
|
||||
}
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
|
||||
if (points_.empty()) return;
|
||||
points_.pop_back();
|
||||
if (!normals_.empty()) normals_.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.
|
||||
if (points_.size() == 1 && normals_.size() == 1) {
|
||||
rebuildLaserOverlay(vp);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
for (const auto& p : points_) pushDot(pts_xyz, p);
|
||||
pushDots(vp, pts_xyz);
|
||||
|
||||
// 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::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
const size_t n = points_.size();
|
||||
|
||||
if (points_.size() >= 2) {
|
||||
const size_t n = points_.size();
|
||||
if (n == 2) {
|
||||
// Direct line A→B (white) + total-length label.
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
groups.push_back(makeGroup({a[0], a[1], a[2], b[0], b[1], b[2]},
|
||||
1.0f, 1.0f, 1.0f));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + " m"));
|
||||
|
||||
// Axis-coloured stair-step A → (Bx,Ay,Az) → (Bx,By,Az) → B.
|
||||
// Each leg gets its delta label (omit zero legs to keep the
|
||||
// overlay clean when the points are axis-aligned).
|
||||
const std::array<float, 3> kx = {b[0], a[1], a[2]};
|
||||
const std::array<float, 3> ky = {b[0], b[1], a[2]};
|
||||
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]);
|
||||
if (dx > 1e-6) {
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], kx[0],kx[1],kx[2]},
|
||||
1.00f, 0.30f, 0.30f));
|
||||
labels.push_back(makeLabel(a, kx,
|
||||
"ΔX: " + QString::number(dx, 'f', 3) + " m"));
|
||||
}
|
||||
if (dy > 1e-6) {
|
||||
groups.push_back(makeGroup({kx[0],kx[1],kx[2], ky[0],ky[1],ky[2]},
|
||||
0.30f, 0.90f, 0.30f));
|
||||
labels.push_back(makeLabel(kx, ky,
|
||||
"ΔY: " + QString::number(dy, 'f', 3) + " m"));
|
||||
}
|
||||
if (dz > 1e-6) {
|
||||
groups.push_back(makeGroup({ky[0],ky[1],ky[2], b[0],b[1],b[2]},
|
||||
0.30f, 0.55f, 1.00f));
|
||||
labels.push_back(makeLabel(ky, b,
|
||||
"ΔZ: " + QString::number(dz, 'f', 3) + " m"));
|
||||
}
|
||||
|
||||
// Perpendicular projection: only when both picks landed on
|
||||
// surfaces with near-parallel normals (|n_a · n_b| > 0.95). We
|
||||
// pick the average normal (flipped to agree with n_a if needed)
|
||||
// and project AB onto it. Drawn dashed from A to A + perp·n.
|
||||
if (normals_.size() == 2) {
|
||||
const auto& na = normals_[0];
|
||||
const auto& nb = normals_[1];
|
||||
const double dot_nn = double(na[0])*nb[0]
|
||||
+ double(na[1])*nb[1]
|
||||
+ double(na[2])*nb[2];
|
||||
if (std::abs(dot_nn) > 0.95) {
|
||||
const float sign = dot_nn >= 0.0 ? 1.0f : -1.0f;
|
||||
float n_avg[3] = {
|
||||
0.5f * (na[0] + sign * nb[0]),
|
||||
0.5f * (na[1] + sign * nb[1]),
|
||||
0.5f * (na[2] + sign * nb[2]),
|
||||
};
|
||||
const float len = std::sqrt(n_avg[0]*n_avg[0]
|
||||
+ n_avg[1]*n_avg[1]
|
||||
+ n_avg[2]*n_avg[2]);
|
||||
if (len > 1e-6f) {
|
||||
n_avg[0] /= len; n_avg[1] /= len; n_avg[2] /= len;
|
||||
}
|
||||
const double abx = double(b[0]) - a[0];
|
||||
const double aby = double(b[1]) - a[1];
|
||||
const double abz = double(b[2]) - a[2];
|
||||
const double perp = abx*n_avg[0] + aby*n_avg[1] + abz*n_avg[2];
|
||||
if (std::abs(perp) > 1e-6) {
|
||||
const std::array<float, 3> tip = {
|
||||
float(a[0] + perp * n_avg[0]),
|
||||
float(a[1] + perp * n_avg[1]),
|
||||
float(a[2] + perp * n_avg[2]),
|
||||
};
|
||||
auto perp_grp = makeGroup(
|
||||
{a[0],a[1],a[2], tip[0],tip[1],tip[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true);
|
||||
groups.push_back(perp_grp);
|
||||
labels.push_back(makeLabel(a, tip,
|
||||
"perp: " + QString::number(std::abs(perp), 'f', 3) + " m"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (n >= 3) {
|
||||
// 3-pt and 4+pt: white connecting polyline (closed for 4+) with
|
||||
// per-segment length labels. HUD carries the angle/area readout.
|
||||
std::vector<float> seg_xyz;
|
||||
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));
|
||||
auto addSeg = [&](const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
pushSeg(seg_xyz, a, b);
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + " m"));
|
||||
};
|
||||
for (size_t i = 0; i + 1 < n; ++i) pushSegment(points_[i], points_[i + 1]);
|
||||
if (n >= 4) pushSegment(points_[n - 1], points_[0]);
|
||||
for (size_t i = 0; i + 1 < n; ++i) addSeg(points_[i], points_[i + 1]);
|
||||
if (n >= 4) addSeg(points_[n - 1], points_[0]);
|
||||
groups.push_back(makeGroup(std::move(seg_xyz), 1.0f, 1.0f, 1.0f));
|
||||
}
|
||||
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.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(formatReadout());
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Which world axis is `v` closest to? Used to label the BFS extent
|
||||
// bars (X/Y/Z) without hard-coding wall vs floor convention.
|
||||
const char* dominantAxisLabel(const float v[3]) {
|
||||
const float ax = std::abs(v[0]);
|
||||
const float ay = std::abs(v[1]);
|
||||
const float az = std::abs(v[2]);
|
||||
if (az >= ax && az >= ay) return "Z";
|
||||
if (ax >= ay) return "X";
|
||||
return "Y";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
|
||||
const auto& wp = first_pick_.world_pos; // float[3] world click
|
||||
const auto& n = first_pick_.world_normal; // float[3] world normal
|
||||
|
||||
// ---------- Tangent basis in world ----------
|
||||
// t1 = world-up Gram-Schmidt'd against n; fall back to world-X for
|
||||
// near-horizontal surfaces so the basis never degenerates.
|
||||
constexpr float WORLD_UP[3] = {0.0f, 0.0f, 1.0f};
|
||||
const float dot_un = WORLD_UP[0]*n[0] + WORLD_UP[1]*n[1] + WORLD_UP[2]*n[2];
|
||||
float t1[3] = {
|
||||
WORLD_UP[0] - dot_un * n[0],
|
||||
WORLD_UP[1] - dot_un * n[1],
|
||||
WORLD_UP[2] - dot_un * n[2],
|
||||
};
|
||||
float t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
if (t1_len < 0.1f) {
|
||||
constexpr float WORLD_X[3] = {1.0f, 0.0f, 0.0f};
|
||||
const float dot_xn = WORLD_X[0]*n[0] + WORLD_X[1]*n[1] + WORLD_X[2]*n[2];
|
||||
t1[0] = WORLD_X[0] - dot_xn * n[0];
|
||||
t1[1] = WORLD_X[1] - dot_xn * n[1];
|
||||
t1[2] = WORLD_X[2] - dot_xn * n[2];
|
||||
t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
}
|
||||
if (t1_len > 1e-6f) {
|
||||
t1[0] /= t1_len; t1[1] /= t1_len; t1[2] /= t1_len;
|
||||
}
|
||||
const float t2[3] = {
|
||||
n[1]*t1[2] - n[2]*t1[1],
|
||||
n[2]*t1[0] - n[0]*t1[2],
|
||||
n[0]*t1[1] - n[1]*t1[0],
|
||||
};
|
||||
|
||||
std::vector<OverlayRenderer::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
QStringList hud_lines;
|
||||
hud_lines << QStringLiteral("Laser measure (click another point for distance)");
|
||||
|
||||
// ---------- Coplanar-patch BFS for face extent ----------
|
||||
// Read back the seed mesh, transform every vertex into world space,
|
||||
// build edge adjacency, BFS from the seed triangle keeping only
|
||||
// co-normal neighbours, then project each patch vertex into the
|
||||
// (t1, t2) basis to get the bounding extent of the face. Stops
|
||||
// exactly at the face edge (no overshoot into adjacent geometry).
|
||||
ViewportWindow::MeshTriangles tris;
|
||||
bool have_extent = false;
|
||||
double min_t1 = 0.0, max_t1 = 0.0, min_t2 = 0.0, max_t2 = 0.0;
|
||||
if (vp.readbackMeshTriangles(first_pick_.model_id, first_pick_.mesh_id, tris)) {
|
||||
const size_t n_verts = tris.positions.size() / 3;
|
||||
const size_t n_tris = tris.indices.size() / 3;
|
||||
if (n_tris > 0) {
|
||||
// Vertices → world.
|
||||
std::vector<float> wv(n_verts * 3);
|
||||
const float* M = first_pick_.composed_transform;
|
||||
for (size_t i = 0; i < n_verts; ++i) {
|
||||
const float* p = &tris.positions[i * 3];
|
||||
wv[i*3 + 0] = M[0]*p[0] + M[4]*p[1] + M[8]*p[2] + M[12];
|
||||
wv[i*3 + 1] = M[1]*p[0] + M[5]*p[1] + M[9]*p[2] + M[13];
|
||||
wv[i*3 + 2] = M[2]*p[0] + M[6]*p[1] + M[10]*p[2] + M[14];
|
||||
}
|
||||
// Per-tri world normals + edge adjacency.
|
||||
std::vector<std::array<float, 3>> tri_n(n_tris);
|
||||
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
|
||||
edges.reserve(n_tris * 3);
|
||||
for (size_t t = 0; t < n_tris; ++t) {
|
||||
const uint32_t ia = tris.indices[3*t + 0];
|
||||
const uint32_t ib = tris.indices[3*t + 1];
|
||||
const uint32_t ic = tris.indices[3*t + 2];
|
||||
const float* a = &wv[3*ia];
|
||||
const float* b = &wv[3*ib];
|
||||
const float* c = &wv[3*ic];
|
||||
const float bax = b[0]-a[0], bay = b[1]-a[1], baz = b[2]-a[2];
|
||||
const float cax = c[0]-a[0], cay = c[1]-a[1], caz = c[2]-a[2];
|
||||
float nx = bay*caz - baz*cay;
|
||||
float ny = baz*cax - bax*caz;
|
||||
float nz = bax*cay - bay*cax;
|
||||
const float nl = std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (nl > 0.0f) { nx /= nl; ny /= nl; nz /= nl; }
|
||||
tri_n[t] = {nx, ny, nz};
|
||||
edges[edgeKey(ia, ib)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ib, ic)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ic, ia)].push_back(uint32_t(t));
|
||||
}
|
||||
// Seed = nearest triangle to world click.
|
||||
uint32_t seed = 0;
|
||||
double best = std::numeric_limits<double>::infinity();
|
||||
for (size_t t = 0; t < n_tris; ++t) {
|
||||
const uint32_t ia = tris.indices[3*t + 0];
|
||||
const uint32_t ib = tris.indices[3*t + 1];
|
||||
const uint32_t ic = tris.indices[3*t + 2];
|
||||
const double d = pointTriangleDistSq(
|
||||
wp, &wv[3*ia], &wv[3*ib], &wv[3*ic]);
|
||||
if (d < best) { best = d; seed = uint32_t(t); }
|
||||
}
|
||||
// BFS coplanar.
|
||||
const auto& sn = tri_n[seed];
|
||||
std::unordered_set<uint32_t> in_patch;
|
||||
in_patch.insert(seed);
|
||||
std::queue<uint32_t> frontier;
|
||||
frontier.push(seed);
|
||||
while (!frontier.empty()) {
|
||||
const uint32_t t = frontier.front(); frontier.pop();
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const uint32_t ia = tris.indices[3*t + e];
|
||||
const uint32_t ib = tris.indices[3*t + (e + 1) % 3];
|
||||
auto it = edges.find(edgeKey(ia, ib));
|
||||
if (it == edges.end()) continue;
|
||||
for (uint32_t nt : it->second) {
|
||||
if (nt == t || in_patch.count(nt)) continue;
|
||||
const auto& nn = tri_n[nt];
|
||||
const double dot = double(sn[0])*nn[0]
|
||||
+ double(sn[1])*nn[1]
|
||||
+ double(sn[2])*nn[2];
|
||||
if (dot < kCoplanarDot) continue;
|
||||
in_patch.insert(nt);
|
||||
frontier.push(nt);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Project unique patch vertices → tangent coords.
|
||||
std::unordered_set<uint32_t> patch_verts;
|
||||
for (uint32_t t : in_patch) {
|
||||
patch_verts.insert(tris.indices[3*t + 0]);
|
||||
patch_verts.insert(tris.indices[3*t + 1]);
|
||||
patch_verts.insert(tris.indices[3*t + 2]);
|
||||
}
|
||||
for (uint32_t vi : patch_verts) {
|
||||
const float* v = &wv[3 * vi];
|
||||
const double dx = double(v[0]) - wp[0];
|
||||
const double dy = double(v[1]) - wp[1];
|
||||
const double dz = double(v[2]) - wp[2];
|
||||
const double a1 = dx*t1[0] + dy*t1[1] + dz*t1[2];
|
||||
const double a2 = dx*t2[0] + dy*t2[1] + dz*t2[2];
|
||||
if (!have_extent) {
|
||||
min_t1 = max_t1 = a1;
|
||||
min_t2 = max_t2 = a2;
|
||||
have_extent = true;
|
||||
} else {
|
||||
min_t1 = std::min(min_t1, a1); max_t1 = std::max(max_t1, a1);
|
||||
min_t2 = std::min(min_t2, a2); max_t2 = std::max(max_t2, a2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pushBar = [&](const float t[3], double mn, double mx) {
|
||||
const std::array<float, 3> a = {
|
||||
float(wp[0] + mn * t[0]),
|
||||
float(wp[1] + mn * t[1]),
|
||||
float(wp[2] + mn * t[2]),
|
||||
};
|
||||
const std::array<float, 3> b = {
|
||||
float(wp[0] + mx * t[0]),
|
||||
float(wp[1] + mx * t[1]),
|
||||
float(wp[2] + mx * t[2]),
|
||||
};
|
||||
const double extent = mx - mn;
|
||||
const QString axis = QString::fromLatin1(dominantAxisLabel(t));
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3)));
|
||||
hud_lines << QString("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3);
|
||||
};
|
||||
if (have_extent && (max_t1 - min_t1) > 1e-6) pushBar(t1, min_t1, max_t1);
|
||||
if (have_extent && (max_t2 - min_t2) > 1e-6) pushBar(t2, min_t2, max_t2);
|
||||
|
||||
// ---------- Hybrid: vertical raycast for horizontal surfaces ----------
|
||||
// For floors / ceilings (|n.z| close to 1) the BFS extents give the
|
||||
// floor footprint; the *useful* extra dimension is the room height,
|
||||
// which a single raycast in +n finds. Skip on walls (|n.z| < 0.85)
|
||||
// — there the BFS already covers the user's intent.
|
||||
if (std::abs(n[2]) > 0.85f) {
|
||||
constexpr float NUDGE = 1e-3f;
|
||||
const float ro[3] = {
|
||||
wp[0] + NUDGE * n[0],
|
||||
wp[1] + NUDGE * n[1],
|
||||
wp[2] + NUDGE * n[2],
|
||||
};
|
||||
ViewportWindow::RaycastHit hit;
|
||||
if (vp.raycast(ro, n, hit)) {
|
||||
const double dist = double(hit.distance) + double(NUDGE);
|
||||
const std::array<float, 3> a = {wp[0], wp[1], wp[2]};
|
||||
const std::array<float, 3> b = {hit.world_pos[0],
|
||||
hit.world_pos[1],
|
||||
hit.world_pos[2]};
|
||||
const QString tag = (n[2] > 0.0f)
|
||||
? QStringLiteral("ceiling height")
|
||||
: QStringLiteral("floor distance");
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3)));
|
||||
hud_lines << QString("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3);
|
||||
}
|
||||
}
|
||||
|
||||
pushDots(vp, std::vector<float>(wp, wp + 3));
|
||||
vp.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(hud_lines.join('\n'));
|
||||
}
|
||||
|
||||
QString LengthMeasurement::formatReadout() const {
|
||||
const size_t n = points_.size();
|
||||
if (n == 0) return QStringLiteral("Length tool: click first point");
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#ifndef IFCVIEWER_FULL_MEASUREMENT_H
|
||||
#define IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <array>
|
||||
@@ -27,8 +29,6 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ViewportWindow;
|
||||
|
||||
// Sum of mesh-local volumes (m³) of every instance whose object_id is in
|
||||
// `object_ids`. Groups by (model, mesh) so each unique mesh is read back
|
||||
// from the GPU at most once per call; instances of the same mesh are scaled
|
||||
@@ -110,7 +110,12 @@ private:
|
||||
// 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"
|
||||
// 1 point → "laser-measure" mode: 6 rays (±surface-normal, ±tangent₁,
|
||||
// ±tangent₂ in the surface's own basis) trace into the scene.
|
||||
// On a wall this gives thickness + floor-to-ceiling height +
|
||||
// length-along-wall in one click. Tangent₁ is world up
|
||||
// projected onto the surface plane (Gram-Schmidt against the
|
||||
// normal); tangent₂ = normal × tangent₁.
|
||||
// 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
|
||||
@@ -118,7 +123,8 @@ private:
|
||||
// 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.
|
||||
// the connecting polyline (or the laser rays for 1-point); readouts
|
||||
// go to the multi-line HUD.
|
||||
class LengthMeasurement {
|
||||
public:
|
||||
LengthMeasurement();
|
||||
@@ -131,9 +137,17 @@ public:
|
||||
|
||||
private:
|
||||
void rebuildOverlay(ViewportWindow& vp);
|
||||
void rebuildLaserOverlay(ViewportWindow& vp);
|
||||
QString formatReadout() const;
|
||||
|
||||
std::vector<std::array<float, 3>> points_;
|
||||
std::vector<std::array<float, 3>> normals_; // surface normal at each pick
|
||||
|
||||
// Captured at the very first pick of a fresh sequence and never
|
||||
// updated afterwards. Used by the 1-pt laser BFS to re-locate the
|
||||
// mesh-local position of points_[0] without re-picking. Stays valid
|
||||
// while points_[0] does (pop_back never touches the first element).
|
||||
ViewportWindow::MeshLocalPick first_pick_{};
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
@@ -95,8 +95,8 @@ void main() {
|
||||
// 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.
|
||||
// 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;
|
||||
@@ -107,10 +107,9 @@ 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);
|
||||
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);
|
||||
}
|
||||
@@ -136,6 +135,7 @@ 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);
|
||||
@@ -160,7 +160,8 @@ void main() {
|
||||
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_dist_px = in_side * total_half;
|
||||
v_along_px = in_along * len;
|
||||
}
|
||||
)";
|
||||
|
||||
@@ -190,18 +191,26 @@ void main() {
|
||||
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;
|
||||
|
||||
// ~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);
|
||||
// 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);
|
||||
@@ -282,6 +291,8 @@ void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
|
||||
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.
|
||||
{
|
||||
@@ -310,22 +321,24 @@ void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
|
||||
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);
|
||||
// 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(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);
|
||||
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_);
|
||||
@@ -342,8 +355,8 @@ void OverlayRenderer::release() {
|
||||
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_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_);
|
||||
@@ -352,7 +365,9 @@ void OverlayRenderer::release() {
|
||||
if (program_rect_) gl_->glDeleteProgram(program_rect_);
|
||||
triangles_ = {};
|
||||
points_ = {};
|
||||
lines_ = {};
|
||||
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;
|
||||
@@ -392,23 +407,31 @@ void OverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
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) {
|
||||
void OverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
|
||||
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;
|
||||
line_draws_.clear();
|
||||
|
||||
std::vector<float> expanded;
|
||||
expandLineSegments(world_xyz, expanded);
|
||||
lines_.vertex_count = GLsizei(expanded.size() / 8);
|
||||
uploadFloats(gl_, lines_.vbo, lines_.vbo_capacity, expanded);
|
||||
// Concatenate every group's CPU-expanded vertices into one big buffer
|
||||
// and remember each group's (first, count) slice + style so render()
|
||||
// can iterate without re-expanding.
|
||||
std::vector<float> combined;
|
||||
for (const auto& g : groups) {
|
||||
std::vector<float> exp;
|
||||
expandLineSegments(g.world_xyz, exp);
|
||||
if (exp.empty()) continue;
|
||||
LineDrawCall dc;
|
||||
std::memcpy(dc.color, g.color, sizeof(dc.color));
|
||||
std::memcpy(dc.stroke_color, g.stroke_color, sizeof(dc.stroke_color));
|
||||
dc.line_width = g.line_width;
|
||||
dc.stroke_extra = g.stroke_extra;
|
||||
dc.dash_period_px = g.dash_period_px;
|
||||
dc.dash_on_ratio = g.dash_on_ratio;
|
||||
dc.first = GLint(combined.size() / 8);
|
||||
dc.count = GLsizei(exp.size() / 8);
|
||||
line_draws_.push_back(dc);
|
||||
combined.insert(combined.end(), exp.begin(), exp.end());
|
||||
}
|
||||
uploadFloats(gl_, vbo_lines_, vbo_lines_capacity_, combined);
|
||||
}
|
||||
|
||||
void OverlayRenderer::render(const float view_proj[16],
|
||||
@@ -447,16 +470,21 @@ void OverlayRenderer::render(const float view_proj[16],
|
||||
// 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) {
|
||||
if (!line_draws_.empty()) {
|
||||
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);
|
||||
gl_->glBindVertexArray(vao_lines_);
|
||||
for (const auto& dc : line_draws_) {
|
||||
if (dc.count == 0 || dc.color[3] <= 0.0f) continue;
|
||||
gl_->glUniform1f(u_ln_half_width_, dc.line_width * 0.5f);
|
||||
gl_->glUniform1f(u_ln_stroke_extra_, dc.stroke_extra);
|
||||
gl_->glUniform4fv(u_ln_inner_color_, 1, dc.color);
|
||||
gl_->glUniform4fv(u_ln_stroke_color_, 1, dc.stroke_color);
|
||||
gl_->glUniform1f(u_ln_dash_period_, dc.dash_period_px);
|
||||
gl_->glUniform1f(u_ln_dash_on_ratio_, dc.dash_on_ratio);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, dc.first, dc.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
|
||||
|
||||
@@ -44,19 +44,21 @@ 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);
|
||||
// One stylistic group of line segments rendered through the
|
||||
// outlined / optionally-dashed line shader. Multiple groups in a
|
||||
// single setOverlayLines call let the caller mix solid + dashed +
|
||||
// axis-coloured legs in one frame (e.g. the length tool's white
|
||||
// total line + RGB XYZ stair-step + dashed perpendicular).
|
||||
struct LineGroup {
|
||||
std::vector<float> world_xyz; // 6 floats per segment (a, b)
|
||||
float color[4] = {1, 1, 1, 1}; // inner color
|
||||
float stroke_color[4] = {0, 0, 0, 1}; // outline (0 alpha = no outline)
|
||||
float line_width = 1.5f; // pixels (inner)
|
||||
float stroke_extra = 0.5f; // pixels per side outside inner
|
||||
float dash_period_px = 0.0f; // 0 = solid; else screen-space dash period
|
||||
float dash_on_ratio = 0.6f; // [0..1], used only when period > 0
|
||||
};
|
||||
void setOverlayLines(const std::vector<LineGroup>& groups);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// `pixel_size` is the inner-dot diameter in physical pixels.
|
||||
@@ -118,23 +120,19 @@ private:
|
||||
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;
|
||||
// Per-group draw-call record. setOverlayLines populates one of these
|
||||
// per LineGroup, with `first` indexing into a shared expanded-vertex
|
||||
// VBO. At render time we iterate them, set per-group uniforms, and
|
||||
// issue one glDrawArrays each.
|
||||
struct LineDrawCall {
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float line_width = 1.5f;
|
||||
float stroke_extra = 0.5f;
|
||||
float dash_period_px = 0.0f;
|
||||
float dash_on_ratio = 0.6f;
|
||||
GLint first = 0;
|
||||
GLsizei count = 0;
|
||||
};
|
||||
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
@@ -160,6 +158,8 @@ private:
|
||||
GLint u_ln_stroke_extra_ = -1;
|
||||
GLint u_ln_inner_color_ = -1;
|
||||
GLint u_ln_stroke_color_ = -1;
|
||||
GLint u_ln_dash_period_ = -1;
|
||||
GLint u_ln_dash_on_ratio_ = -1;
|
||||
|
||||
// Screen-space rect program (label + HUD backgrounds). Vertex
|
||||
// attribute is vec2 NDC; fragment outputs a uniform color. Drawn
|
||||
@@ -172,7 +172,13 @@ private:
|
||||
|
||||
TriBundle triangles_;
|
||||
PointBundle points_;
|
||||
LineBundle lines_;
|
||||
|
||||
// Lines: one shared VAO/VBO holding the concatenated expanded
|
||||
// vertices of every group; line_draws_ records each group's slice.
|
||||
GLuint vao_lines_ = 0;
|
||||
GLuint vbo_lines_ = 0;
|
||||
size_t vbo_lines_capacity_ = 0;
|
||||
std::vector<LineDrawCall> line_draws_;
|
||||
|
||||
std::vector<Label> labels_;
|
||||
QString hud_text_;
|
||||
|
||||
@@ -3881,6 +3881,169 @@ void ViewportWindow::setToolMode(ToolMode mode) {
|
||||
emit toolModeChanged(mode);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Slab method ray-AABB. inv_d is precomputed 1/dir per axis.
|
||||
bool rayAabb(const float ro[3], const float inv_d[3],
|
||||
const float bmin[3], const float bmax[3]) {
|
||||
float tmin = 0.0f, tmax = std::numeric_limits<float>::infinity();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
float t1 = (bmin[i] - ro[i]) * inv_d[i];
|
||||
float t2 = (bmax[i] - ro[i]) * inv_d[i];
|
||||
tmin = std::max(tmin, std::min(t1, t2));
|
||||
tmax = std::min(tmax, std::max(t1, t2));
|
||||
}
|
||||
return tmax >= tmin && tmax >= 0.0f;
|
||||
}
|
||||
|
||||
// Möller-Trumbore. Returns true on hit; t is in dir-units.
|
||||
bool rayTri(const float ro[3], const float rd[3],
|
||||
const float v0[3], const float v1[3], const float v2[3],
|
||||
float& t_out) {
|
||||
constexpr float EPS = 1e-7f;
|
||||
float e1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
|
||||
float e2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
|
||||
float h[3] = {
|
||||
rd[1]*e2[2] - rd[2]*e2[1],
|
||||
rd[2]*e2[0] - rd[0]*e2[2],
|
||||
rd[0]*e2[1] - rd[1]*e2[0]
|
||||
};
|
||||
float a = e1[0]*h[0] + e1[1]*h[1] + e1[2]*h[2];
|
||||
if (a > -EPS && a < EPS) return false;
|
||||
float f = 1.0f / a;
|
||||
float s[3] = {ro[0]-v0[0], ro[1]-v0[1], ro[2]-v0[2]};
|
||||
float u = f * (s[0]*h[0] + s[1]*h[1] + s[2]*h[2]);
|
||||
if (u < 0.0f || u > 1.0f) return false;
|
||||
float q[3] = {
|
||||
s[1]*e1[2] - s[2]*e1[1],
|
||||
s[2]*e1[0] - s[0]*e1[2],
|
||||
s[0]*e1[1] - s[1]*e1[0]
|
||||
};
|
||||
float v = f * (rd[0]*q[0] + rd[1]*q[1] + rd[2]*q[2]);
|
||||
if (v < 0.0f || u + v > 1.0f) return false;
|
||||
float t = f * (e2[0]*q[0] + e2[1]*q[1] + e2[2]*q[2]);
|
||||
if (t > EPS) { t_out = t; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stack-based BVH walk — collects the item indices of every leaf whose
|
||||
// AABB the ray intersects. Caller filters down to actual triangle hits.
|
||||
void bvhCollectRayCandidates(const ModelBvh& bvh,
|
||||
const float ro[3], const float inv_d[3],
|
||||
std::vector<uint32_t>& out) {
|
||||
if (bvh.nodes.empty()) return;
|
||||
std::vector<uint32_t> stack;
|
||||
stack.reserve(64);
|
||||
stack.push_back(0);
|
||||
while (!stack.empty()) {
|
||||
uint32_t idx = stack.back(); stack.pop_back();
|
||||
const BvhNode& node = bvh.nodes[idx];
|
||||
if (!rayAabb(ro, inv_d, node.aabb_min, node.aabb_max)) continue;
|
||||
if (node.count > 0) {
|
||||
for (uint32_t i = 0; i < node.count; ++i) {
|
||||
out.push_back(bvh.item_indices[node.right_or_first + i]);
|
||||
}
|
||||
} else {
|
||||
stack.push_back(idx + 1); // left child
|
||||
stack.push_back(node.right_or_first); // right child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ViewportWindow::raycast(const float origin[3], const float dir[3],
|
||||
RaycastHit& out) {
|
||||
if (!gl_initialized_) return false;
|
||||
|
||||
float inv_d[3];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
inv_d[i] = (std::abs(dir[i]) > 1e-12f)
|
||||
? 1.0f / dir[i] : std::numeric_limits<float>::infinity();
|
||||
}
|
||||
|
||||
// Local mesh-triangle cache shared across candidate instances of the
|
||||
// same mesh in this single call. Avoids re-stalling the GL pipeline
|
||||
// for repeated readbacks of the same VBO/EBO range.
|
||||
std::unordered_map<uint64_t, MeshTriangles> mesh_cache;
|
||||
|
||||
float closest_t = std::numeric_limits<float>::infinity();
|
||||
bool any_hit = false;
|
||||
using Mat4f = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
|
||||
|
||||
for (auto& kv : models_gpu_) {
|
||||
ModelGpuData& m = kv.second;
|
||||
if (!m.finalized || m.bvh.nodes.empty()) continue;
|
||||
|
||||
std::vector<uint32_t> candidates;
|
||||
bvhCollectRayCandidates(m.bvh, origin, inv_d, candidates);
|
||||
if (candidates.empty()) continue;
|
||||
|
||||
for (uint32_t inst_idx : candidates) {
|
||||
if (inst_idx >= m.instances.size()) continue;
|
||||
const InstanceCpu& inst = m.instances[inst_idx];
|
||||
|
||||
// Transform ray into this instance's mesh-local space. The
|
||||
// returned t parameter is identical in world and mesh-local
|
||||
// (both are along the same parametric line) so we don't need
|
||||
// to convert it back — assumes caller passed a unit world dir.
|
||||
const Eigen::Matrix4f T = Eigen::Map<const Mat4f>(inst.transform);
|
||||
const Eigen::Matrix4f Ti = T.inverse();
|
||||
const Eigen::Vector4f wo(origin[0], origin[1], origin[2], 1.0f);
|
||||
const Eigen::Vector4f wd(dir[0], dir[1], dir[2], 0.0f);
|
||||
const Eigen::Vector4f lo = Ti * wo;
|
||||
const Eigen::Vector4f ld = Ti * wd;
|
||||
const float ro_l[3] = {lo.x(), lo.y(), lo.z()};
|
||||
const float rd_l[3] = {ld.x(), ld.y(), ld.z()};
|
||||
|
||||
const uint64_t key = (uint64_t(inst.model_id) << 32) | inst.mesh_id;
|
||||
auto it = mesh_cache.find(key);
|
||||
if (it == mesh_cache.end()) {
|
||||
MeshTriangles tris;
|
||||
if (!readbackMeshTriangles(inst.model_id, inst.mesh_id, tris)) continue;
|
||||
it = mesh_cache.emplace(key, std::move(tris)).first;
|
||||
}
|
||||
const MeshTriangles& tris = it->second;
|
||||
|
||||
// Möller-Trumbore against every triangle. No per-mesh BVH
|
||||
// here yet — buildings rarely have meshes with > a few
|
||||
// thousand tris; if this becomes a hotspot we can add one.
|
||||
for (size_t i = 0; i + 2 < tris.indices.size(); i += 3) {
|
||||
const uint32_t ia = tris.indices[i + 0];
|
||||
const uint32_t ib = tris.indices[i + 1];
|
||||
const uint32_t ic = tris.indices[i + 2];
|
||||
const float* a = &tris.positions[3 * ia];
|
||||
const float* b = &tris.positions[3 * ib];
|
||||
const float* c = &tris.positions[3 * ic];
|
||||
float t;
|
||||
if (!rayTri(ro_l, rd_l, a, b, c, t)) continue;
|
||||
if (t >= closest_t) continue;
|
||||
closest_t = t;
|
||||
any_hit = true;
|
||||
out.object_id = inst.object_id;
|
||||
out.distance = t;
|
||||
out.world_pos[0] = origin[0] + t * dir[0];
|
||||
out.world_pos[1] = origin[1] + t * dir[1];
|
||||
out.world_pos[2] = origin[2] + t * dir[2];
|
||||
// World normal: triangle normal in mesh-local, transformed
|
||||
// by inverse-transpose of the 3x3.
|
||||
const float e1x = b[0]-a[0], e1y = b[1]-a[1], e1z = b[2]-a[2];
|
||||
const float e2x = c[0]-a[0], e2y = c[1]-a[1], e2z = c[2]-a[2];
|
||||
Eigen::Vector3f n_local(
|
||||
e1y*e2z - e1z*e2y,
|
||||
e1z*e2x - e1x*e2z,
|
||||
e1x*e2y - e1y*e2x);
|
||||
Eigen::Matrix3f N = T.block<3, 3>(0, 0).inverse().transpose();
|
||||
Eigen::Vector3f world_n = (N * n_local).normalized();
|
||||
out.world_normal[0] = world_n.x();
|
||||
out.world_normal[1] = world_n.y();
|
||||
out.world_normal[2] = world_n.z();
|
||||
}
|
||||
}
|
||||
}
|
||||
return any_hit;
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleAreaTool() {
|
||||
setToolMode(tool_mode_ == ToolMode::Area ? ToolMode::None : ToolMode::Area);
|
||||
}
|
||||
@@ -3897,15 +4060,10 @@ 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) {
|
||||
void ViewportWindow::setOverlayLines(const std::vector<OverlayRenderer::LineGroup>& groups) {
|
||||
if (!gl_initialized_) return;
|
||||
context_->makeCurrent(this);
|
||||
overlay_renderer_.setOverlayLines(world_xyz, r, g, b, a, line_width,
|
||||
sr, sg, sb, sa, stroke_extra);
|
||||
overlay_renderer_.setOverlayLines(groups);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,24 @@ public:
|
||||
};
|
||||
bool pickMeshLocalAt(int x, int y, MeshLocalPick& out);
|
||||
|
||||
// CPU raycast against the per-model BVHs. Walks the BVH for each
|
||||
// finalised model, transforms the world ray into mesh-local space
|
||||
// for each candidate instance, reads back its triangles, and runs
|
||||
// Möller-Trumbore against them. Returns the closest hit overall.
|
||||
//
|
||||
// `dir` MUST be a unit vector — distance is reported as the t value
|
||||
// along the ray, which equals world distance only when |dir|=1.
|
||||
// Stalls the GL pipeline once per unique (model, mesh) candidate
|
||||
// because triangle data is read back lazily; budget ~1ms for
|
||||
// typical BIM scenes.
|
||||
struct RaycastHit {
|
||||
uint32_t object_id = 0;
|
||||
float distance = 0.0f;
|
||||
float world_pos[3] = {0, 0, 0};
|
||||
float world_normal[3]= {0, 0, 0};
|
||||
};
|
||||
bool raycast(const float origin[3], const float dir[3], RaycastHit& out);
|
||||
|
||||
// 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
|
||||
@@ -250,15 +268,10 @@ 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 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 line groups. Each group has its own segments
|
||||
// + style (color/halo/width/dash) — see OverlayRenderer::LineGroup.
|
||||
// Empty disables every line.
|
||||
void setOverlayLines(const std::vector<OverlayRenderer::LineGroup>& groups);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// pixel_size is the inner-disc diameter in physical pixels; when
|
||||
|
||||
Reference in New Issue
Block a user