mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
wgpu: length measurement tool (L hotkey, adaptive 1/2/3/4+ point readout)
Ports Bonsai's LengthMeasurement onto WgpuViewportWindow as a new
WgpuLengthMeasurement class. Each LMB appends a world-space pick point
and the readout adapts to the running count:
1 pt → laser-measure: coplanar-patch BFS on the click's surface
projects every patch vertex into the surface's own tangent
basis to get face extents (X/Y/Z bars dashed in world space),
plus ENH coords for the picked point, plus a vertical
raycast for floor/ceiling distance on horizontal surfaces.
2 pts → distance A→B + axis-coloured ΔX/ΔY/ΔZ stair-step + dashed
perpendicular projection when both picks landed on
near-parallel surfaces.
3 pts → angle at middle vertex + triangle area + perimeter.
4+ pts → polygon area via best-fit-plane shoelace (Jacobi-3x3
eigendecomp inline; no Eigen dep) or fan-triangulated
fallback for non-planar loops, plus closed-loop perimeter.
Backspace / Del removes the last point; Esc / L again exits.
Dependencies layered in:
- pickMeshLocalAt now refines the AABB-coarse pickSurfaceAt hit into a
real triangle hit via Möller-Trumbore against the picked instance's
CPU mesh shadow. Without this the BFS seeds with whatever triangle
is closest to the bounding-box corner — producing patches and
extents shaped like the AABB instead of the surface.
- meshLocalToGlobal: applies the instance's placement_transformation
only (no per-model CoordinateOperation in wgpu yet). ENH equals
IFC-world for non-federated loads, which is what the minimal viewer
handles.
- raycast: brute-force world-AABB cull + Möller-Trumbore over the CPU
mesh shadow. Used by the laser-measure ceiling/floor distance.
- ToolMode gains Length; click handler routes plain/Alt LMB through
onLengthPick, Backspace through onLengthBackspace. Marquee-arm is
gated off in Length mode.
Volume HUD now shows "Volume: 0.0000 m³ (0 objects)" the moment V is
pressed, matching how A primes "Area: 0.0000 m²" — gives the user a
visible cue the tool is active before any selection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,738 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "WgpuLengthMeasurement.h"
|
||||
|
||||
#include "WgpuOverlayRenderer.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
#include <QtMath>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Symmetric 3×3 eigendecomposition via Jacobi rotations. Tiny inline
|
||||
// alternative to pulling Eigen into the wgpu module for the single
|
||||
// polygon-planarity check. Converges in <10 sweeps for 3×3.
|
||||
void jacobiEigen3(double m00, double m01, double m02,
|
||||
double m11, double m12, double m22,
|
||||
double eigvals[3], double eigvecs[3][3]) {
|
||||
double a[3][3] = {{m00, m01, m02},
|
||||
{m01, m11, m12},
|
||||
{m02, m12, m22}};
|
||||
double v[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
|
||||
for (int iter = 0; iter < 50; ++iter) {
|
||||
// Pick largest off-diagonal magnitude.
|
||||
int p = 0, q = 1;
|
||||
double off = std::abs(a[0][1]);
|
||||
if (std::abs(a[0][2]) > off) { p = 0; q = 2; off = std::abs(a[0][2]); }
|
||||
if (std::abs(a[1][2]) > off) { p = 1; q = 2; off = std::abs(a[1][2]); }
|
||||
if (off < 1e-12) break;
|
||||
const double app = a[p][p], aqq = a[q][q], apq = a[p][q];
|
||||
const double theta = (aqq - app) / (2.0 * apq);
|
||||
double t = (theta >= 0.0) ? 1.0 / (theta + std::sqrt(1.0 + theta*theta))
|
||||
: 1.0 / (theta - std::sqrt(1.0 + theta*theta));
|
||||
const double c = 1.0 / std::sqrt(1.0 + t*t);
|
||||
const double s = t * c;
|
||||
a[p][p] = app - t * apq;
|
||||
a[q][q] = aqq + t * apq;
|
||||
a[p][q] = a[q][p] = 0.0;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
if (k == p || k == q) continue;
|
||||
const double akp = a[k][p], akq = a[k][q];
|
||||
a[k][p] = a[p][k] = c * akp - s * akq;
|
||||
a[k][q] = a[q][k] = s * akp + c * akq;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const double vkp = v[k][p], vkq = v[k][q];
|
||||
v[k][p] = c * vkp - s * vkq;
|
||||
v[k][q] = s * vkp + c * vkq;
|
||||
}
|
||||
}
|
||||
eigvals[0] = a[0][0];
|
||||
eigvals[1] = a[1][1];
|
||||
eigvals[2] = a[2][2];
|
||||
std::memcpy(eigvecs, v, sizeof(v));
|
||||
}
|
||||
|
||||
struct PolygonAreaResult {
|
||||
double area_m2;
|
||||
const char* method;
|
||||
};
|
||||
|
||||
PolygonAreaResult polygonArea(const std::vector<std::array<float, 3>>& pts) {
|
||||
const size_t n = pts.size();
|
||||
|
||||
// Centroid + bounding box (for the planarity threshold).
|
||||
double centroid[3] = {0, 0, 0};
|
||||
double bbox_min[3] = { std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity() };
|
||||
double bbox_max[3] = {-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity() };
|
||||
for (const auto& p : pts) {
|
||||
centroid[0] += p[0]; centroid[1] += p[1]; centroid[2] += p[2];
|
||||
bbox_min[0] = std::min(bbox_min[0], double(p[0]));
|
||||
bbox_min[1] = std::min(bbox_min[1], double(p[1]));
|
||||
bbox_min[2] = std::min(bbox_min[2], double(p[2]));
|
||||
bbox_max[0] = std::max(bbox_max[0], double(p[0]));
|
||||
bbox_max[1] = std::max(bbox_max[1], double(p[1]));
|
||||
bbox_max[2] = std::max(bbox_max[2], double(p[2]));
|
||||
}
|
||||
centroid[0] /= double(n);
|
||||
centroid[1] /= double(n);
|
||||
centroid[2] /= double(n);
|
||||
const double bbx = bbox_max[0] - bbox_min[0];
|
||||
const double bby = bbox_max[1] - bbox_min[1];
|
||||
const double bbz = bbox_max[2] - bbox_min[2];
|
||||
const double bbox_diag = std::sqrt(bbx*bbx + bby*bby + bbz*bbz);
|
||||
|
||||
// 3×3 symmetric covariance. Smallest eigenvector → plane normal.
|
||||
double c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0;
|
||||
for (const auto& p : pts) {
|
||||
const double dx = double(p[0]) - centroid[0];
|
||||
const double dy = double(p[1]) - centroid[1];
|
||||
const double dz = double(p[2]) - centroid[2];
|
||||
c00 += dx*dx; c01 += dx*dy; c02 += dx*dz;
|
||||
c11 += dy*dy; c12 += dy*dz; c22 += dz*dz;
|
||||
}
|
||||
double eigvals[3];
|
||||
double eigvecs[3][3];
|
||||
jacobiEigen3(c00, c01, c02, c11, c12, c22, eigvals, eigvecs);
|
||||
int min_i = 0;
|
||||
if (eigvals[1] < eigvals[min_i]) min_i = 1;
|
||||
if (eigvals[2] < eigvals[min_i]) min_i = 2;
|
||||
const double normal[3] = { eigvecs[0][min_i],
|
||||
eigvecs[1][min_i],
|
||||
eigvecs[2][min_i] };
|
||||
|
||||
double sq_sum = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
const double d = (double(p[0]) - centroid[0]) * normal[0]
|
||||
+ (double(p[1]) - centroid[1]) * normal[1]
|
||||
+ (double(p[2]) - centroid[2]) * normal[2];
|
||||
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) {
|
||||
// In-plane orthonormal basis. Cross with whichever world axis is
|
||||
// least parallel to the normal so u doesn't collapse.
|
||||
double u[3];
|
||||
u[0] = normal[1] * 0.0 - normal[2] * 0.0; // normal × X
|
||||
u[1] = normal[2] * 1.0 - normal[0] * 0.0;
|
||||
u[2] = normal[0] * 0.0 - normal[1] * 1.0;
|
||||
double ul2 = u[0]*u[0] + u[1]*u[1] + u[2]*u[2];
|
||||
if (ul2 < 1e-6) {
|
||||
u[0] = normal[1] * 0.0 - normal[2] * 1.0; // normal × Y
|
||||
u[1] = normal[2] * 0.0 - normal[0] * 0.0;
|
||||
u[2] = normal[0] * 1.0 - normal[1] * 0.0;
|
||||
ul2 = u[0]*u[0] + u[1]*u[1] + u[2]*u[2];
|
||||
}
|
||||
const double ul = std::sqrt(ul2);
|
||||
u[0] /= ul; u[1] /= ul; u[2] /= ul;
|
||||
const double v[3] = {
|
||||
normal[1]*u[2] - normal[2]*u[1],
|
||||
normal[2]*u[0] - normal[0]*u[2],
|
||||
normal[0]*u[1] - normal[1]*u[0]
|
||||
};
|
||||
// Project + shoelace.
|
||||
std::vector<std::array<double, 2>> uv(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const double dx = double(pts[i][0]) - centroid[0];
|
||||
const double dy = double(pts[i][1]) - centroid[1];
|
||||
const double dz = double(pts[i][2]) - centroid[2];
|
||||
uv[i][0] = dx*u[0] + dy*u[1] + dz*u[2];
|
||||
uv[i][1] = dx*v[0] + dy*v[1] + dz*v[2];
|
||||
}
|
||||
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 — heuristic for non-planar / star-shaped 3D loops.
|
||||
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)" };
|
||||
}
|
||||
|
||||
uint64_t edgeKey(uint32_t a, uint32_t b) {
|
||||
if (a > b) std::swap(a, b);
|
||||
return (uint64_t(a) << 32) | uint64_t(b);
|
||||
}
|
||||
|
||||
double pointTriangleDistSq(const float p[3],
|
||||
const float a[3], const float b[3], const float c[3]) {
|
||||
auto sub = [](const float u[3], const float v[3], double r[3]) {
|
||||
r[0] = double(u[0]) - v[0];
|
||||
r[1] = double(u[1]) - v[1];
|
||||
r[2] = double(u[2]) - v[2];
|
||||
};
|
||||
auto dot = [](const double u[3], const double v[3]) {
|
||||
return u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
|
||||
};
|
||||
double ab[3], ac[3], ap[3];
|
||||
sub(b, a, ab);
|
||||
sub(c, a, ac);
|
||||
sub(p, a, ap);
|
||||
const double d1 = dot(ab, ap);
|
||||
const double d2 = dot(ac, ap);
|
||||
if (d1 <= 0.0 && d2 <= 0.0) {
|
||||
return ap[0]*ap[0] + ap[1]*ap[1] + ap[2]*ap[2];
|
||||
}
|
||||
double bp[3];
|
||||
sub(p, b, bp);
|
||||
const double d3 = dot(ab, bp);
|
||||
const double d4 = dot(ac, bp);
|
||||
if (d3 >= 0.0 && d4 <= d3) {
|
||||
return bp[0]*bp[0] + bp[1]*bp[1] + bp[2]*bp[2];
|
||||
}
|
||||
const double vc = d1 * d4 - d3 * d2;
|
||||
if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
|
||||
const double v = d1 / (d1 - d3);
|
||||
const double qx = ap[0] - v * ab[0];
|
||||
const double qy = ap[1] - v * ab[1];
|
||||
const double qz = ap[2] - v * ab[2];
|
||||
return qx*qx + qy*qy + qz*qz;
|
||||
}
|
||||
double cp[3];
|
||||
sub(p, c, cp);
|
||||
const double d5 = dot(ab, cp);
|
||||
const double d6 = dot(ac, cp);
|
||||
if (d6 >= 0.0 && d5 <= d6) {
|
||||
return cp[0]*cp[0] + cp[1]*cp[1] + cp[2]*cp[2];
|
||||
}
|
||||
const double vb = d5 * d2 - d1 * d6;
|
||||
if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
|
||||
const double w = d2 / (d2 - d6);
|
||||
const double qx = ap[0] - w * ac[0];
|
||||
const double qy = ap[1] - w * ac[1];
|
||||
const double qz = ap[2] - w * ac[2];
|
||||
return qx*qx + qy*qy + qz*qz;
|
||||
}
|
||||
const double va = d3 * d6 - d5 * d4;
|
||||
if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
|
||||
const double w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||
const double qx = double(b[0]) + w * (double(c[0]) - b[0]) - p[0];
|
||||
const double qy = double(b[1]) + w * (double(c[1]) - b[1]) - p[1];
|
||||
const double qz = double(b[2]) + w * (double(c[2]) - b[2]) - p[2];
|
||||
return qx*qx + qy*qy + qz*qz;
|
||||
}
|
||||
const double denom = 1.0 / (va + vb + vc);
|
||||
const double v = vb * denom;
|
||||
const double w = vc * denom;
|
||||
const double qx = double(a[0]) + v * ab[0] + w * ac[0] - p[0];
|
||||
const double qy = double(a[1]) + v * ab[1] + w * ac[1] - p[1];
|
||||
const double qz = double(a[2]) + v * ab[2] + w * ac[2] - p[2];
|
||||
return qx*qx + qy*qy + qz*qz;
|
||||
}
|
||||
|
||||
constexpr double kCoplanarDot = 0.9999; // ~0.81° tolerance
|
||||
|
||||
// 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
|
||||
|
||||
WgpuOverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
|
||||
float r, float g, float b,
|
||||
bool dashed = false) {
|
||||
WgpuOverlayRenderer::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());
|
||||
}
|
||||
|
||||
WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const QString& text) {
|
||||
WgpuOverlayRenderer::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(WgpuViewportWindow& 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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
WgpuLengthMeasurement::WgpuLengthMeasurement() = default;
|
||||
|
||||
void WgpuLengthMeasurement::clear(WgpuViewportWindow& vp) {
|
||||
points_.clear();
|
||||
normals_.clear();
|
||||
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
|
||||
vp.setOverlayLines({});
|
||||
vp.setOverlayLabels({});
|
||||
vp.setHudText(QString());
|
||||
}
|
||||
|
||||
void WgpuLengthMeasurement::onPick(WgpuViewportWindow& vp,
|
||||
int x_phys, int y_phys, bool /*alt*/) {
|
||||
WgpuViewportWindow::MeshLocalPick pick;
|
||||
if (!vp.pickMeshLocalAt(x_phys, y_phys, 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 WgpuLengthMeasurement::removeLastPoint(WgpuViewportWindow& vp) {
|
||||
if (points_.empty()) return;
|
||||
points_.pop_back();
|
||||
if (!normals_.empty()) normals_.pop_back();
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void WgpuLengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
|
||||
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_) pushDot(pts_xyz, p);
|
||||
pushDots(vp, pts_xyz);
|
||||
|
||||
std::vector<WgpuOverlayRenderer::LineGroup> groups;
|
||||
std::vector<WgpuOverlayRenderer::Label> labels;
|
||||
const size_t n = points_.size();
|
||||
|
||||
if (n == 2) {
|
||||
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) + QStringLiteral(" m")));
|
||||
|
||||
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,
|
||||
QStringLiteral("ΔX: ") + QString::number(dx, 'f', 3) + QStringLiteral(" 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,
|
||||
QStringLiteral("ΔY: ") + QString::number(dy, 'f', 3) + QStringLiteral(" 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,
|
||||
QStringLiteral("ΔZ: ") + QString::number(dz, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
|
||||
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];
|
||||
const double abs_perp = std::abs(perp);
|
||||
constexpr double kAxisCollapseTol = 1e-3;
|
||||
const bool redundant =
|
||||
std::abs(abs_perp - dx) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dy) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dz) < kAxisCollapseTol;
|
||||
if (abs_perp > 1e-6 && !redundant) {
|
||||
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,
|
||||
QStringLiteral("perp: ") + QString::number(abs_perp, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (n >= 3) {
|
||||
std::vector<float> seg_xyz;
|
||||
seg_xyz.reserve(n * 6);
|
||||
labels.reserve(n);
|
||||
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) + QStringLiteral(" m")));
|
||||
};
|
||||
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(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(formatReadout());
|
||||
}
|
||||
|
||||
void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
|
||||
const auto& wp = first_pick_.world_pos;
|
||||
const auto& n = first_pick_.world_normal;
|
||||
|
||||
// Tangent basis in world space.
|
||||
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<WgpuOverlayRenderer::LineGroup> groups;
|
||||
std::vector<WgpuOverlayRenderer::Label> labels;
|
||||
QStringList hud_lines;
|
||||
hud_lines << QStringLiteral("Laser measure (click another point for distance)");
|
||||
double enh[3] = {0.0, 0.0, 0.0};
|
||||
if (vp.meshLocalToGlobal(first_pick_.object_id, first_pick_.mesh_local, enh)) {
|
||||
hud_lines << QStringLiteral("ENH: %1, %2, %3")
|
||||
.arg(enh[0], 0, 'f', 3)
|
||||
.arg(enh[1], 0, 'f', 3)
|
||||
.arg(enh[2], 0, 'f', 3);
|
||||
}
|
||||
|
||||
// Coplanar-patch BFS for face extent.
|
||||
WgpuViewportWindow::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) {
|
||||
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];
|
||||
}
|
||||
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));
|
||||
}
|
||||
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); }
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
QStringLiteral("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3)));
|
||||
hud_lines << QStringLiteral("%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 floors / ceilings.
|
||||
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],
|
||||
};
|
||||
WgpuViewportWindow::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,
|
||||
QStringLiteral("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3)));
|
||||
hud_lines << QStringLiteral("%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 WgpuLengthMeasurement::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 QStringLiteral("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];
|
||||
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 QStringLiteral("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);
|
||||
}
|
||||
|
||||
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 QStringLiteral("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);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef WGPULENGTHMEASUREMENT_H
|
||||
#define WGPULENGTHMEASUREMENT_H
|
||||
|
||||
#include "WgpuViewportWindow.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
// Click-to-place length / angle / area measurement for the wgpu viewport.
|
||||
// Mirrors src/bonsaiviewer/Measurement.h's LengthMeasurement — the
|
||||
// readout adapts to the running point count:
|
||||
//
|
||||
// 1 point → laser-measure: BFS the coplanar surface patch the click
|
||||
// landed on, project its vertices into the surface's own
|
||||
// tangent basis to get the face extent, plus a vertical
|
||||
// raycast for floor/ceiling distance on horizontal surfaces.
|
||||
// 2 points → straight-line distance + ΔX/ΔY/ΔZ stair-step + optional
|
||||
// perpendicular projection when both picks landed on
|
||||
// near-parallel surfaces.
|
||||
// 3 points → angle at the middle vertex + triangle area + perimeter.
|
||||
// 4+ pts → polygon area via best-fit-plane shoelace if near-planar,
|
||||
// fan-triangulated otherwise; perimeter on the closed loop.
|
||||
//
|
||||
// Pushes the running set as overlay points + the connecting polyline +
|
||||
// per-segment labels to the viewport; the multi-line HUD carries the
|
||||
// adaptive readout.
|
||||
class WgpuLengthMeasurement {
|
||||
public:
|
||||
WgpuLengthMeasurement();
|
||||
|
||||
// Pixel coords are physical (post-DPR). `alt` is currently unused
|
||||
// (kept for API symmetry with the Area tool).
|
||||
void onPick(WgpuViewportWindow& vp, int x_phys, int y_phys, bool alt);
|
||||
void removeLastPoint(WgpuViewportWindow& vp);
|
||||
void clear(WgpuViewportWindow& vp);
|
||||
|
||||
size_t pointCount() const { return points_.size(); }
|
||||
|
||||
private:
|
||||
void rebuildOverlay(WgpuViewportWindow& vp);
|
||||
void rebuildLaserOverlay(WgpuViewportWindow& 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 locate the
|
||||
// mesh-local position of points_[0] without re-picking. Stays valid
|
||||
// while points_[0] does (pop_back never touches the first element).
|
||||
WgpuViewportWindow::MeshLocalPick first_pick_{};
|
||||
};
|
||||
|
||||
#endif // WGPULENGTHMEASUREMENT_H
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "WgpuViewportWindow.h"
|
||||
#include "WgpuAreaMeasurement.h"
|
||||
#include "WgpuLengthMeasurement.h"
|
||||
#include "WgpuStreamingLoader.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
@@ -103,6 +104,54 @@ static double computeMeshLocalVolumeQuantised(
|
||||
const uint8_t* vbase, const uint32_t* ibase, uint32_t n_indices,
|
||||
WgpuModelGpuData::MeshTriangles* out_tris);
|
||||
|
||||
// Ray-AABB (slab) + ray-triangle (Möller-Trumbore). Used by raycast()
|
||||
// AND by pickMeshLocalAt to refine the AABB-coarse surface hit into a
|
||||
// real triangle hit — see pickMeshLocalAt's refinement block.
|
||||
|
||||
// Slab method ray-AABB. inv_d is precomputed 1/dir per axis.
|
||||
static bool rayAabbSlab(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) {
|
||||
const float t1 = (bmin[i] - ro[i]) * inv_d[i];
|
||||
const 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.
|
||||
static bool rayTriMT(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;
|
||||
const float e1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
|
||||
const float e2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
|
||||
const 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]
|
||||
};
|
||||
const float a = e1[0]*h[0] + e1[1]*h[1] + e1[2]*h[2];
|
||||
if (a > -EPS && a < EPS) return false;
|
||||
const float f = 1.0f / a;
|
||||
const float s[3] = {ro[0]-v0[0], ro[1]-v0[1], ro[2]-v0[2]};
|
||||
const float u = f * (s[0]*h[0] + s[1]*h[1] + s[2]*h[2]);
|
||||
if (u < 0.0f || u > 1.0f) return false;
|
||||
const 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]
|
||||
};
|
||||
const 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;
|
||||
const float t = f * (e2[0]*q[0] + e2[1]*q[1] + e2[2]*q[2]);
|
||||
if (t <= EPS) return false;
|
||||
t_out = t;
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Small helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -2956,22 +3005,130 @@ bool WgpuViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) {
|
||||
bool ok = false;
|
||||
const QMatrix4x4 Ti = T.inverted(&ok);
|
||||
if (!ok) return false;
|
||||
const QVector4D mp = Ti * QVector4D(world_pos.x(), world_pos.y(), world_pos.z(), 1.0f);
|
||||
|
||||
if (inst.mesh_id >= m.meshes.size()) return false;
|
||||
|
||||
// pickSurfaceAt returns a bounding-box hit (WebGPU bans the
|
||||
// depth readback that would give us a real surface point), so
|
||||
// world_pos sits on the AABB face — not on any triangle of the
|
||||
// mesh. Refine against the picked instance's CPU mesh shadow:
|
||||
// re-project the click into a world ray and Möller-Trumbore it
|
||||
// against every triangle of this mesh. On a hit, replace
|
||||
// world_pos with the real surface point and world_normal with
|
||||
// the transformed face normal. Without this, the Area/Length
|
||||
// BFS seeds with whatever triangle is closest to the AABB
|
||||
// corner — often a perpendicular face, which produces
|
||||
// bounding-box-shaped patches instead of surface patches.
|
||||
QVector3D refined_world_pos = world_pos;
|
||||
QVector3D refined_world_normal = world_normal;
|
||||
if (inst.mesh_id < m.mesh_triangles_cache.size()) {
|
||||
const auto& tris = m.mesh_triangles_cache[inst.mesh_id];
|
||||
if (!tris.indices.empty() && configured_w_ > 0 && configured_h_ > 0) {
|
||||
QMatrix4x4 view, proj;
|
||||
buildViewProj(view, proj);
|
||||
bool inv_ok = false;
|
||||
const QMatrix4x4 inv_vp = (proj * view).inverted(&inv_ok);
|
||||
if (inv_ok) {
|
||||
const float ndc_x = (2.0f * float(x) / float(configured_w_)) - 1.0f;
|
||||
const float ndc_y = 1.0f - (2.0f * float(y) / float(configured_h_));
|
||||
const QVector4D far_clip(ndc_x, ndc_y, 1.0f, 1.0f);
|
||||
const QVector4D far_w = inv_vp * far_clip;
|
||||
if (std::abs(far_w.w()) >= 1e-6f) {
|
||||
const QVector3D far_world = far_w.toVector3D() / far_w.w();
|
||||
const QVector3D eye = orbitEye(
|
||||
camera_target_, camera_distance_,
|
||||
camera_yaw_deg_, camera_pitch_deg_);
|
||||
QVector3D ray_dir = far_world - eye;
|
||||
if (ray_dir.lengthSquared() > 1e-8f) {
|
||||
ray_dir.normalize();
|
||||
// Inverse-transform the world ray into mesh-local.
|
||||
const QVector4D ro_l4 = Ti * QVector4D(eye.x(), eye.y(), eye.z(), 1.0f);
|
||||
const QVector4D rd_l4 = Ti * QVector4D(ray_dir.x(), ray_dir.y(), ray_dir.z(), 0.0f);
|
||||
const float ro_l[3] = { ro_l4.x(), ro_l4.y(), ro_l4.z() };
|
||||
const float rd_l[3] = { rd_l4.x(), rd_l4.y(), rd_l4.z() };
|
||||
const float ldn = std::sqrt(
|
||||
rd_l[0]*rd_l[0] + rd_l[1]*rd_l[1] + rd_l[2]*rd_l[2]);
|
||||
if (ldn > 0.0f) {
|
||||
float best_t_world = std::numeric_limits<float>::infinity();
|
||||
uint32_t best_tri = UINT32_MAX;
|
||||
const size_t n_tris = tris.indices.size() / 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];
|
||||
if (3 * ia + 2 >= tris.positions.size()
|
||||
|| 3 * ib + 2 >= tris.positions.size()
|
||||
|| 3 * ic + 2 >= tris.positions.size()) continue;
|
||||
const float* va = &tris.positions[3 * ia];
|
||||
const float* vb = &tris.positions[3 * ib];
|
||||
const float* vc = &tris.positions[3 * ic];
|
||||
float t_local = 0.0f;
|
||||
if (!rayTriMT(ro_l, rd_l, va, vb, vc, t_local)) continue;
|
||||
const float t_world = t_local / ldn;
|
||||
if (t_world < best_t_world) {
|
||||
best_t_world = t_world;
|
||||
best_tri = uint32_t(t);
|
||||
}
|
||||
}
|
||||
if (best_tri != UINT32_MAX) {
|
||||
refined_world_pos = eye + ray_dir * best_t_world;
|
||||
// Face normal of the chosen tri,
|
||||
// transformed back to world.
|
||||
const uint32_t ia = tris.indices[3 * best_tri + 0];
|
||||
const uint32_t ib = tris.indices[3 * best_tri + 1];
|
||||
const uint32_t ic = tris.indices[3 * best_tri + 2];
|
||||
const float* va = &tris.positions[3 * ia];
|
||||
const float* vb = &tris.positions[3 * ib];
|
||||
const float* vc = &tris.positions[3 * ic];
|
||||
const float bax = vb[0]-va[0], bay = vb[1]-va[1], baz = vb[2]-va[2];
|
||||
const float cax = vc[0]-va[0], cay = vc[1]-va[1], caz = vc[2]-va[2];
|
||||
float n_local[3] = {
|
||||
bay*caz - baz*cay,
|
||||
baz*cax - bax*caz,
|
||||
bax*cay - bay*cax,
|
||||
};
|
||||
const float nl = std::sqrt(
|
||||
n_local[0]*n_local[0]
|
||||
+ n_local[1]*n_local[1]
|
||||
+ n_local[2]*n_local[2]);
|
||||
if (nl > 0.0f) {
|
||||
n_local[0] /= nl;
|
||||
n_local[1] /= nl;
|
||||
n_local[2] /= nl;
|
||||
}
|
||||
const float* M = inst.transform;
|
||||
QVector3D n_world(
|
||||
M[0]*n_local[0] + M[4]*n_local[1] + M[8] *n_local[2],
|
||||
M[1]*n_local[0] + M[5]*n_local[1] + M[9] *n_local[2],
|
||||
M[2]*n_local[0] + M[6]*n_local[1] + M[10]*n_local[2]);
|
||||
if (n_world.lengthSquared() > 1e-12f) {
|
||||
n_world.normalize();
|
||||
refined_world_normal = n_world;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QVector4D mp = Ti * QVector4D(refined_world_pos.x(),
|
||||
refined_world_pos.y(),
|
||||
refined_world_pos.z(), 1.0f);
|
||||
|
||||
out.object_id = obj_id;
|
||||
out.model_id = mid;
|
||||
out.mesh_id = inst.mesh_id;
|
||||
out.mesh_local[0] = mp.x();
|
||||
out.mesh_local[1] = mp.y();
|
||||
out.mesh_local[2] = mp.z();
|
||||
out.world_pos [0] = world_pos.x();
|
||||
out.world_pos [1] = world_pos.y();
|
||||
out.world_pos [2] = world_pos.z();
|
||||
out.world_normal[0] = world_normal.x();
|
||||
out.world_normal[1] = world_normal.y();
|
||||
out.world_normal[2] = world_normal.z();
|
||||
out.world_pos [0] = refined_world_pos.x();
|
||||
out.world_pos [1] = refined_world_pos.y();
|
||||
out.world_pos [2] = refined_world_pos.z();
|
||||
out.world_normal[0] = refined_world_normal.x();
|
||||
out.world_normal[1] = refined_world_normal.y();
|
||||
out.world_normal[2] = refined_world_normal.z();
|
||||
std::memcpy(out.composed_transform, inst.transform,
|
||||
sizeof(out.composed_transform));
|
||||
return true;
|
||||
@@ -2985,6 +3142,155 @@ void WgpuViewportWindow::onAreaPick(int x_phys, int y_phys, bool alt) {
|
||||
updateAreaHud();
|
||||
}
|
||||
|
||||
bool WgpuViewportWindow::meshLocalToGlobal(uint32_t object_id,
|
||||
const float mesh_local[3],
|
||||
double global_out[3]) const {
|
||||
// Find the instance via the per-model object_id_to_instance map.
|
||||
// Use the live map key (`mid`) — see pickMeshLocalAt comment about
|
||||
// stale InstanceCpu::model_id from sidecar writes.
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(object_id);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
// GL composes coordinate_operation · placement · local; the wgpu
|
||||
// viewer doesn't carry per-model CoordinateOperation yet, so
|
||||
// apply just the placement_transformation (double precision —
|
||||
// matches the IFC's own world coordinates for a non-federated
|
||||
// load).
|
||||
const double* P = inst.placement_transformation; // column-major
|
||||
const double lx = double(mesh_local[0]);
|
||||
const double ly = double(mesh_local[1]);
|
||||
const double lz = double(mesh_local[2]);
|
||||
global_out[0] = P[0]*lx + P[4]*ly + P[8]*lz + P[12];
|
||||
global_out[1] = P[1]*lx + P[5]*ly + P[9]*lz + P[13];
|
||||
global_out[2] = P[2]*lx + P[6]*ly + P[10]*lz + P[14];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WgpuViewportWindow::raycast(const float origin[3], const float dir[3],
|
||||
RaycastHit& out) const {
|
||||
// World-AABB cull per instance, then transform the ray into the
|
||||
// mesh's local frame and intersect every triangle. No BVH — typical
|
||||
// BIM scenes have enough AABB-cull to make this acceptable (~ms);
|
||||
// a per-model BVH would be the next optimisation.
|
||||
float inv_d[3] = {
|
||||
std::abs(dir[0]) > 1e-20f ? 1.0f / dir[0] : std::numeric_limits<float>::infinity(),
|
||||
std::abs(dir[1]) > 1e-20f ? 1.0f / dir[1] : std::numeric_limits<float>::infinity(),
|
||||
std::abs(dir[2]) > 1e-20f ? 1.0f / dir[2] : std::numeric_limits<float>::infinity(),
|
||||
};
|
||||
|
||||
float best_t = std::numeric_limits<float>::infinity();
|
||||
uint32_t best_oid = 0;
|
||||
float best_normal[3] = {0, 0, 0};
|
||||
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (uint32_t inst_idx = 0; inst_idx < uint32_t(m.instances.size()); ++inst_idx) {
|
||||
const InstanceCpu& inst = m.instances[inst_idx];
|
||||
if (!rayAabbSlab(origin, inv_d, inst.world_aabb_min, inst.world_aabb_max)) {
|
||||
continue;
|
||||
}
|
||||
if (inst.mesh_id >= m.mesh_triangles_cache.size()) continue;
|
||||
const auto& tris = m.mesh_triangles_cache[inst.mesh_id];
|
||||
if (tris.indices.empty()) continue;
|
||||
|
||||
// Transform ray into mesh-local frame. We need both a point
|
||||
// (origin) and a direction (dir) inverse-transformed; dir is
|
||||
// a vector so the translation drops out.
|
||||
QMatrix4x4 T(inst.transform[0], inst.transform[4], inst.transform[8], inst.transform[12],
|
||||
inst.transform[1], inst.transform[5], inst.transform[9], inst.transform[13],
|
||||
inst.transform[2], inst.transform[6], inst.transform[10], inst.transform[14],
|
||||
inst.transform[3], inst.transform[7], inst.transform[11], inst.transform[15]);
|
||||
bool ok = false;
|
||||
const QMatrix4x4 Ti = T.inverted(&ok);
|
||||
if (!ok) continue;
|
||||
const QVector4D ro_local4 = Ti * QVector4D(origin[0], origin[1], origin[2], 1.0f);
|
||||
const QVector4D rd_local4 = Ti * QVector4D(dir[0], dir[1], dir[2], 0.0f);
|
||||
const float ro_local[3] = { ro_local4.x(), ro_local4.y(), ro_local4.z() };
|
||||
const float rd_local[3] = { rd_local4.x(), rd_local4.y(), rd_local4.z() };
|
||||
|
||||
const size_t n_tris = tris.indices.size() / 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];
|
||||
if (3 * ia + 2 >= tris.positions.size()
|
||||
|| 3 * ib + 2 >= tris.positions.size()
|
||||
|| 3 * ic + 2 >= tris.positions.size()) continue;
|
||||
const float* va = &tris.positions[3 * ia];
|
||||
const float* vb = &tris.positions[3 * ib];
|
||||
const float* vc = &tris.positions[3 * ic];
|
||||
float t_local = 0.0f;
|
||||
if (!rayTriMT(ro_local, rd_local, va, vb, vc, t_local)) continue;
|
||||
// Convert t_local into world units. Because we
|
||||
// inverse-transformed dir without normalising, world-t =
|
||||
// local-t × (|world-dir| / |local-dir|). The caller
|
||||
// guarantees world-dir is unit; we compute local-dir
|
||||
// length here.
|
||||
const float ldn = std::sqrt(rd_local[0]*rd_local[0]
|
||||
+ rd_local[1]*rd_local[1]
|
||||
+ rd_local[2]*rd_local[2]);
|
||||
if (ldn <= 0.0f) continue;
|
||||
const float t_world = t_local / ldn;
|
||||
if (t_world >= best_t) continue;
|
||||
best_t = t_world;
|
||||
best_oid = inst.object_id;
|
||||
|
||||
// Mesh-local triangle normal → world via the transform's
|
||||
// rotation block. Same column-major math as
|
||||
// applyCachedModel uses for AABB normals.
|
||||
const float bax = vb[0]-va[0], bay = vb[1]-va[1], baz = vb[2]-va[2];
|
||||
const float cax = vc[0]-va[0], cay = vc[1]-va[1], caz = vc[2]-va[2];
|
||||
float n_local[3] = {
|
||||
bay * caz - baz * cay,
|
||||
baz * cax - bax * caz,
|
||||
bax * cay - bay * cax,
|
||||
};
|
||||
const float nl = std::sqrt(n_local[0]*n_local[0]
|
||||
+ n_local[1]*n_local[1]
|
||||
+ n_local[2]*n_local[2]);
|
||||
if (nl > 0.0f) { n_local[0] /= nl; n_local[1] /= nl; n_local[2] /= nl; }
|
||||
// Normal transform = inverse-transpose; for a rigid +
|
||||
// uniform-scale transform the upper-left 3×3 is fine.
|
||||
const float* M = inst.transform;
|
||||
best_normal[0] = M[0]*n_local[0] + M[4]*n_local[1] + M[8]*n_local[2];
|
||||
best_normal[1] = M[1]*n_local[0] + M[5]*n_local[1] + M[9]*n_local[2];
|
||||
best_normal[2] = M[2]*n_local[0] + M[6]*n_local[1] + M[10]*n_local[2];
|
||||
const float wnl = std::sqrt(best_normal[0]*best_normal[0]
|
||||
+ best_normal[1]*best_normal[1]
|
||||
+ best_normal[2]*best_normal[2]);
|
||||
if (wnl > 0.0f) {
|
||||
best_normal[0] /= wnl;
|
||||
best_normal[1] /= wnl;
|
||||
best_normal[2] /= wnl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!std::isfinite(best_t)) return false;
|
||||
out.object_id = best_oid;
|
||||
out.distance = best_t;
|
||||
out.world_pos[0] = origin[0] + best_t * dir[0];
|
||||
out.world_pos[1] = origin[1] + best_t * dir[1];
|
||||
out.world_pos[2] = origin[2] + best_t * dir[2];
|
||||
out.world_normal[0] = best_normal[0];
|
||||
out.world_normal[1] = best_normal[1];
|
||||
out.world_normal[2] = best_normal[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::onLengthPick(int x_phys, int y_phys, bool alt) {
|
||||
if (!length_tool_) return;
|
||||
length_tool_->onPick(*this, x_phys, y_phys, alt);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::onLengthBackspace() {
|
||||
if (!length_tool_) return;
|
||||
length_tool_->removeLastPoint(*this);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::updateAreaHud() {
|
||||
if (tool_mode_ != ToolMode::Area || !area_tool_) return;
|
||||
overlays_.setHudText(
|
||||
@@ -3076,9 +3382,12 @@ void WgpuViewportWindow::setToolMode(ToolMode m) {
|
||||
// Always tear down the previous tool's overlay artefacts before
|
||||
// switching — easier than per-from-state branching, and the new
|
||||
// tool re-primes whatever it owns on its first update.
|
||||
if (area_tool_) area_tool_->clear(*this);
|
||||
if (area_tool_) area_tool_->clear(*this);
|
||||
if (length_tool_) length_tool_->clear(*this);
|
||||
overlays_.setHudText(QString());
|
||||
overlays_.setOverlayLabels({});
|
||||
overlays_.setOverlayLines({});
|
||||
overlays_.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
|
||||
overlays_.setHighlightTriangles({}, 0, 0, 0, 0);
|
||||
|
||||
switch (tool_mode_) {
|
||||
@@ -3087,6 +3396,7 @@ void WgpuViewportWindow::setToolMode(ToolMode m) {
|
||||
break;
|
||||
case ToolMode::Volume:
|
||||
qInfo() << "[wgpu measure] volume tool — pick / marquee objects, Esc to exit";
|
||||
overlays_.setHudText(QStringLiteral("Volume: 0.0000 m³ (0 objects)"));
|
||||
updateVolumeReadout();
|
||||
break;
|
||||
case ToolMode::Area:
|
||||
@@ -3094,6 +3404,11 @@ void WgpuViewportWindow::setToolMode(ToolMode m) {
|
||||
qInfo() << "[wgpu measure] area tool — LMB pick coplanar patch, Alt+LMB single tri, click again to remove, Esc exits";
|
||||
overlays_.setHudText(QStringLiteral("Area: 0.0000 m² (0 tris)"));
|
||||
break;
|
||||
case ToolMode::Length:
|
||||
if (!length_tool_) length_tool_ = std::make_unique<WgpuLengthMeasurement>();
|
||||
qInfo() << "[wgpu measure] length tool — LMB add point, Backspace remove last, Esc exits";
|
||||
overlays_.setHudText(QStringLiteral("Length tool: click first point"));
|
||||
break;
|
||||
}
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
@@ -6370,6 +6685,7 @@ void WgpuViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
} else if (event->button() == Qt::LeftButton
|
||||
&& !section_tool_active_
|
||||
&& tool_mode_ != ToolMode::Area
|
||||
&& tool_mode_ != ToolMode::Length
|
||||
&& nav_drag_kind_ == NavDrag::Inactive) {
|
||||
// Arm marquee box-select. Plain / Shift / Ctrl LMB without a tool
|
||||
// intercepting the click; if the cursor never moves past the
|
||||
@@ -6480,6 +6796,20 @@ void WgpuViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Length tool: plain LMB appends a world-space pick point;
|
||||
// the readout adapts to the running count (laser / distance
|
||||
// / angle / polygon). Shift/Ctrl fall through to selection.
|
||||
if (tool_mode_ == ToolMode::Length
|
||||
&& (event->modifiers() == Qt::NoModifier
|
||||
|| event->modifiers() == Qt::AltModifier)) {
|
||||
const bool alt = (event->modifiers() & Qt::AltModifier) != 0;
|
||||
onLengthPick(px, py, alt);
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
setPivotIndicatorVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t id = pickObjectAt(px, py);
|
||||
const auto mods = event->modifiers();
|
||||
if (id == 0) {
|
||||
@@ -6804,6 +7134,17 @@ void WgpuViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
: ToolMode::Area);
|
||||
return;
|
||||
}
|
||||
if (key == Qt::Key_L && mods == Qt::NoModifier && !event->isAutoRepeat()) {
|
||||
setToolMode(tool_mode_ == ToolMode::Length ? ToolMode::NoTool
|
||||
: ToolMode::Length);
|
||||
return;
|
||||
}
|
||||
if (tool_mode_ == ToolMode::Length
|
||||
&& (key == Qt::Key_Backspace || key == Qt::Key_Delete)
|
||||
&& !event->isAutoRepeat()) {
|
||||
onLengthBackspace();
|
||||
return;
|
||||
}
|
||||
if (tool_mode_ != ToolMode::NoTool && key == Qt::Key_Escape
|
||||
&& !event->isAutoRepeat()) {
|
||||
setToolMode(ToolMode::NoTool);
|
||||
|
||||
@@ -326,14 +326,39 @@ public:
|
||||
};
|
||||
bool pickMeshLocalAt(int x, int y, MeshLocalPick& out);
|
||||
|
||||
// Resolve a mesh-local point to a (placement-applied) global frame.
|
||||
// Matches GL ViewportWindow::meshLocalToGlobal's shape so the Length
|
||||
// tool's ENH readout ports unchanged. The wgpu viewer doesn't carry
|
||||
// per-model CoordinateOperation yet, so this currently outputs
|
||||
// placement_transformation · mesh_local (i.e. the IFC's own world
|
||||
// coords pre-georeferencing); ENH and IFC-world coincide for the
|
||||
// non-federated case the minimal viewer handles today.
|
||||
bool meshLocalToGlobal(uint32_t object_id, const float mesh_local[3],
|
||||
double global_out[3]) const;
|
||||
|
||||
// CPU world-space raycast. Brute-force: per-instance world-AABB
|
||||
// reject, then ray-into-mesh-local + Möller-Trumbore against the
|
||||
// CPU mesh shadow. `dir` must be a unit vector — distance is the
|
||||
// ray's t parameter, which equals world distance only at |dir|=1.
|
||||
// Used by the Length tool's 1-point laser-measure overlay to find
|
||||
// the ceiling/floor counterpart of a horizontal-surface click.
|
||||
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) const;
|
||||
|
||||
// Measurement tools. Mirrors GL ViewportWindow::ToolMode. Volume is
|
||||
// selection-driven (LMB / marquee). Area is click-to-accumulate:
|
||||
// each LMB picks a triangle and either adds or removes its
|
||||
// coplanar patch via BFS over shared edges; Alt+LMB skips the BFS.
|
||||
// V/A toggle, Esc exits.
|
||||
// V/A/L toggle, Esc exits. Length consumes Backspace too for
|
||||
// remove-last-point semantics.
|
||||
// NoTool (not None) because X11/X.h #define's None as 0L; including
|
||||
// it transitively via Qt's xcb back-end breaks any enum named None.
|
||||
enum class ToolMode { NoTool, Volume, Area };
|
||||
enum class ToolMode { NoTool, Volume, Area, Length };
|
||||
ToolMode toolMode() const { return tool_mode_; }
|
||||
void setToolMode(ToolMode m);
|
||||
|
||||
@@ -539,6 +564,14 @@ private:
|
||||
void onAreaPick(int x_phys, int y_phys, bool alt);
|
||||
void updateAreaHud();
|
||||
|
||||
// Length tool state lives in WgpuLengthMeasurement. Same lifecycle
|
||||
// pattern: lazily constructed on first L press, cleared on tool
|
||||
// exit, click handler routes LMB through onLengthPick + Backspace
|
||||
// through onLengthBackspace.
|
||||
std::unique_ptr<class WgpuLengthMeasurement> length_tool_;
|
||||
void onLengthPick(int x_phys, int y_phys, bool alt);
|
||||
void onLengthBackspace();
|
||||
|
||||
// Pick pass (stage 4). Single-sample R32UInt target + depth, vertex-
|
||||
// pulled from the same visible_draws / instances buffers as the main
|
||||
// pass — pick fragment outputs the instance's object_id. The pick
|
||||
|
||||
Reference in New Issue
Block a user