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:
Dion Moult
2026-05-08 08:50:02 +10:00
parent acfcf6e14e
commit d8f37b2376
6 changed files with 702 additions and 137 deletions
+17 -4
View File
@@ -21,14 +21,13 @@
#define IFCVIEWER_FULL_MEASUREMENT_H
#include <cstddef>
#include "ViewportWindow.h"
#include <QString>
#include <array>
#include <cstdint>
#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 +109,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 +122,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 +136,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