mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-09 13:52:23 +00:00
Route bonsai through wgpu; delete the GL backend
Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.
SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
the StreamingSidecar off to the new applyCachedModel. Field accesses
inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
finalizeModel). The applyLodExtension call is dropped — wgpu has no
live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
next open.
Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
duration of the migration; the GL-side IfcViewer also publicly links
IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.
GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
removed. The first has no replacement (wgpu doesn't use a per-instance
BVH); the latter two are ported separately. test_lod_builder,
test_sidecar_cache, test_instanced_geometry, test_federation remain
(backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
of the surviving translation units reach for them.
Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,145 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "BvhAccel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Centroid {
|
||||
float x, y, z;
|
||||
};
|
||||
|
||||
Centroid computeCentroid(const BvhItem& it) {
|
||||
return {
|
||||
(it.aabb_min[0] + it.aabb_max[0]) * 0.5f,
|
||||
(it.aabb_min[1] + it.aabb_max[1]) * 0.5f,
|
||||
(it.aabb_min[2] + it.aabb_max[2]) * 0.5f
|
||||
};
|
||||
}
|
||||
|
||||
void computeAABB(const std::vector<BvhItem>& items,
|
||||
const uint32_t* indices, uint32_t count,
|
||||
float out_min[3], float out_max[3]) {
|
||||
out_min[0] = out_min[1] = out_min[2] = std::numeric_limits<float>::max();
|
||||
out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits<float>::max();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const auto& it = items[indices[i]];
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (it.aabb_min[a] < out_min[a]) out_min[a] = it.aabb_min[a];
|
||||
if (it.aabb_max[a] > out_max[a]) out_max[a] = it.aabb_max[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void buildRecursive(ModelBvh& mbvh,
|
||||
const std::vector<BvhItem>& items,
|
||||
uint32_t start, uint32_t count) {
|
||||
uint32_t node_idx = static_cast<uint32_t>(mbvh.nodes.size());
|
||||
mbvh.nodes.emplace_back();
|
||||
BvhNode& node = mbvh.nodes[node_idx];
|
||||
|
||||
computeAABB(items, &mbvh.item_indices[start], count,
|
||||
node.aabb_min, node.aabb_max);
|
||||
|
||||
if (count <= BVH_MAX_LEAF_SIZE) {
|
||||
node.right_or_first = start;
|
||||
node.count = static_cast<uint16_t>(count);
|
||||
node.axis = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
float extent[3] = {
|
||||
node.aabb_max[0] - node.aabb_min[0],
|
||||
node.aabb_max[1] - node.aabb_min[1],
|
||||
node.aabb_max[2] - node.aabb_min[2]
|
||||
};
|
||||
int axis = 0;
|
||||
if (extent[1] > extent[axis]) axis = 1;
|
||||
if (extent[2] > extent[axis]) axis = 2;
|
||||
|
||||
uint32_t mid = count / 2;
|
||||
std::nth_element(
|
||||
mbvh.item_indices.begin() + start,
|
||||
mbvh.item_indices.begin() + start + mid,
|
||||
mbvh.item_indices.begin() + start + count,
|
||||
[&](uint32_t a, uint32_t b) {
|
||||
Centroid ca = computeCentroid(items[a]);
|
||||
Centroid cb = computeCentroid(items[b]);
|
||||
return (&ca.x)[axis] < (&cb.x)[axis];
|
||||
});
|
||||
|
||||
node.count = 0;
|
||||
node.axis = static_cast<uint16_t>(axis);
|
||||
|
||||
buildRecursive(mbvh, items, start, mid);
|
||||
|
||||
uint32_t right_child_idx = static_cast<uint32_t>(mbvh.nodes.size());
|
||||
buildRecursive(mbvh, items, start + mid, count - mid);
|
||||
|
||||
mbvh.nodes[node_idx].right_or_first = right_child_idx;
|
||||
}
|
||||
|
||||
ModelBvh buildModelBvh(const std::vector<BvhItem>& items,
|
||||
const std::vector<uint32_t>& model_item_indices,
|
||||
uint32_t model_id) {
|
||||
ModelBvh mbvh;
|
||||
mbvh.model_id = model_id;
|
||||
mbvh.item_indices = model_item_indices;
|
||||
|
||||
uint32_t count = static_cast<uint32_t>(model_item_indices.size());
|
||||
if (count == 0) return mbvh;
|
||||
|
||||
mbvh.nodes.reserve(count * 2);
|
||||
buildRecursive(mbvh, items, 0, count);
|
||||
|
||||
assert(!mbvh.nodes.empty());
|
||||
return mbvh;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ModelBvh buildModelBvhOne(const std::vector<BvhItem>& items, uint32_t model_id) {
|
||||
std::vector<uint32_t> idxs(items.size());
|
||||
for (uint32_t i = 0; i < items.size(); ++i) idxs[i] = i;
|
||||
return buildModelBvh(items, idxs, model_id);
|
||||
}
|
||||
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<BvhItem>& items) {
|
||||
auto bvh_set = std::make_shared<BvhSet>();
|
||||
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> model_items;
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(items.size()); ++i) {
|
||||
model_items[items[i].model_id].push_back(i);
|
||||
}
|
||||
|
||||
for (auto& [model_id, idxs] : model_items) {
|
||||
if (idxs.size() < BVH_MIN_OBJECTS) continue;
|
||||
|
||||
ModelBvh mbvh = buildModelBvh(items, idxs, model_id);
|
||||
bvh_set->bvh_model_ids.insert(model_id);
|
||||
bvh_set->models[model_id] = std::move(mbvh);
|
||||
}
|
||||
|
||||
return bvh_set;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 BVHACCEL_H
|
||||
#define BVHACCEL_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
|
||||
// Generic BVH item — anything with a world AABB and a model_id.
|
||||
// For the instanced renderer each item represents one InstanceCpu.
|
||||
struct BvhItem {
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
uint32_t model_id;
|
||||
};
|
||||
|
||||
static constexpr uint32_t BVH_MAX_LEAF_SIZE = 8;
|
||||
static constexpr uint32_t BVH_MIN_OBJECTS = 32;
|
||||
|
||||
struct BvhNode {
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
uint32_t right_or_first; // interior: right child index (left is always this_index+1); leaf: first item index
|
||||
uint16_t count; // 0 = interior; >0 = leaf with this many items
|
||||
uint16_t axis; // split axis (0/1/2) for interior; unused for leaf
|
||||
};
|
||||
static_assert(sizeof(BvhNode) == 32, "BvhNode must be 32 bytes for cache alignment and sidecar format");
|
||||
|
||||
struct ModelBvh {
|
||||
uint32_t model_id = 0;
|
||||
std::vector<BvhNode> nodes;
|
||||
std::vector<uint32_t> item_indices; // indices into the model's InstanceCpu array
|
||||
};
|
||||
|
||||
struct BvhSet {
|
||||
std::unordered_map<uint32_t, ModelBvh> models;
|
||||
std::unordered_set<uint32_t> bvh_model_ids;
|
||||
};
|
||||
|
||||
// Build BVH trees for all models in the given item snapshot.
|
||||
// Items are expected to already be grouped/filtered by caller if needed.
|
||||
// item_indices in the result reference positions within the full `items`
|
||||
// vector — callers providing a single model's items will see 0..N-1.
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<BvhItem>& items);
|
||||
|
||||
// Build a single-model BVH over `items`. model_id is stored on the result
|
||||
// for identification; item_indices will be 0..items.size()-1.
|
||||
ModelBvh buildModelBvhOne(const std::vector<BvhItem>& items, uint32_t model_id);
|
||||
|
||||
#endif // BVHACCEL_H
|
||||
@@ -23,9 +23,7 @@ set(QT_VERSION 6 CACHE STRING "Qt version")
|
||||
# IfcViewerLib always needs OpenGL in addition to Core/Gui/Widgets. We don't
|
||||
# use the CACHE'd QT_COMPONENTS here because it may have been set by another
|
||||
# target (e.g. qtviewer) without the OpenGL component.
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Widgets OpenGL REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
file(GLOB IFCVIEWER_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
|
||||
file(GLOB IFCVIEWER_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
|
||||
@@ -59,10 +57,10 @@ target_link_libraries(IfcViewer PUBLIC
|
||||
${CGAL_LIBRARIES}
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Gui
|
||||
Qt${QT_VERSION}::Widgets
|
||||
Qt${QT_VERSION}::OpenGL
|
||||
OpenGL::GL
|
||||
${MESH_OPTIMIZER_LIB}
|
||||
# SceneLoader drives WgpuViewportWindow; the wgpu lib also provides
|
||||
# the include path for WgpuViewportWindow.h that SceneLoader.h pulls in.
|
||||
IfcViewerWgpu
|
||||
)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
|
||||
@@ -1,725 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "OverlayRenderer.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontDatabase>
|
||||
#include <QFontMetrics>
|
||||
#include <QPainter>
|
||||
#include <QtGlobal>
|
||||
#include <QtOpenGL/QOpenGLPaintDevice>
|
||||
|
||||
namespace {
|
||||
|
||||
GLuint compile(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* src) {
|
||||
GLuint s = gl->glCreateShader(type);
|
||||
gl->glShaderSource(s, 1, &src, nullptr);
|
||||
gl->glCompileShader(s);
|
||||
GLint ok = 0;
|
||||
gl->glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
gl->glGetShaderInfoLog(s, sizeof(log), nullptr, log);
|
||||
qWarning("OverlayRenderer shader compile error: %s", log);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
GLuint link(QOpenGLFunctions_4_5_Core* gl, GLuint vs, GLuint fs) {
|
||||
GLuint p = gl->glCreateProgram();
|
||||
gl->glAttachShader(p, vs);
|
||||
gl->glAttachShader(p, fs);
|
||||
gl->glLinkProgram(p);
|
||||
GLint ok = 0;
|
||||
gl->glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
gl->glGetProgramInfoLog(p, sizeof(log), nullptr, log);
|
||||
qWarning("OverlayRenderer program link error: %s", log);
|
||||
}
|
||||
gl->glDeleteShader(vs);
|
||||
gl->glDeleteShader(fs);
|
||||
return p;
|
||||
}
|
||||
|
||||
QFont overlayTextFont(int point_size) {
|
||||
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
font.setPointSize(point_size);
|
||||
font.setStyleHint(QFont::TypeWriter);
|
||||
return font;
|
||||
}
|
||||
|
||||
// ---- Triangle program (flat color) ----
|
||||
|
||||
const char* TRI_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
void main() {
|
||||
gl_Position = u_view_proj * vec4(in_pos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* TRI_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
frag_color = u_color;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Point sprite program (outlined disc via gl_PointCoord) ----
|
||||
|
||||
const char* POINT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
uniform float u_point_size;
|
||||
void main() {
|
||||
gl_Position = u_view_proj * vec4(in_pos, 1.0);
|
||||
gl_PointSize = u_point_size;
|
||||
}
|
||||
)";
|
||||
|
||||
// inner_radius_norm is the inner-disc radius as a fraction of the
|
||||
// half-sprite (so 1.0 = no stroke, smaller = thicker stroke). The
|
||||
// fragment shader reads gl_PointCoord (range [0,1] across the sprite),
|
||||
// computes the distance from the centre normalised against the half-
|
||||
// sprite, 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;
|
||||
uniform vec4 u_stroke_color;
|
||||
uniform float u_inner_radius_norm;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
vec2 c = gl_PointCoord - 0.5;
|
||||
float d = length(c) * 2.0; // 0 at centre, 1 at sprite edge
|
||||
if (d > 1.0) discard;
|
||||
float 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);
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Line program (screen-space-expanded quads with outline) ----
|
||||
//
|
||||
// Per-vertex layout: (in_a, in_b, in_side, in_along), 8 floats total.
|
||||
// The vertex shader projects both endpoints to screen pixels, computes
|
||||
// the screen-space perpendicular, and offsets *this* corner accordingly.
|
||||
// Output v_dist_px is the signed perpendicular distance from the line
|
||||
// axis at this corner; linear interpolation across the quad gives the
|
||||
// per-fragment distance the FS uses to discard / pick inner vs stroke.
|
||||
|
||||
const char* LINE_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_a;
|
||||
layout(location = 1) in vec3 in_b;
|
||||
layout(location = 2) in float in_side; // -1 or +1
|
||||
layout(location = 3) in float in_along; // 0 (at a) or 1 (at b)
|
||||
uniform mat4 u_view_proj;
|
||||
uniform vec2 u_screen_size; // physical pixels
|
||||
uniform float u_half_width; // inner half-width (px)
|
||||
uniform float u_stroke_extra; // halo per side (px)
|
||||
out float v_dist_px;
|
||||
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);
|
||||
|
||||
// Project to screen pixels.
|
||||
vec2 screen_a = (clip_a.xy / clip_a.w) * 0.5 * u_screen_size;
|
||||
vec2 screen_b = (clip_b.xy / clip_b.w) * 0.5 * u_screen_size;
|
||||
|
||||
vec2 delta = screen_b - screen_a;
|
||||
float len = length(delta);
|
||||
vec2 dir = (len > 1e-6) ? (delta / len) : vec2(1.0, 0.0);
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
|
||||
// Offset this corner perpendicular to the line.
|
||||
vec4 clip_self = mix(clip_a, clip_b, in_along);
|
||||
vec2 screen_self = (clip_self.xy / clip_self.w) * 0.5 * u_screen_size;
|
||||
float total_half = u_half_width + u_stroke_extra;
|
||||
screen_self += perp * in_side * total_half;
|
||||
|
||||
// Back to NDC, then to clip space (multiply by w to undo the w-divide
|
||||
// GL is about to apply). Depth is preserved from the picked endpoint.
|
||||
vec2 ndc_out = screen_self / (u_screen_size * 0.5);
|
||||
gl_Position = vec4(ndc_out * clip_self.w, clip_self.z, clip_self.w);
|
||||
|
||||
v_dist_px = in_side * total_half;
|
||||
v_along_px = in_along * len;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Screen-space rect program (label + HUD backgrounds) ----
|
||||
//
|
||||
// Skip QPainter::fillRect entirely — on QOpenGLPaintDevice it's
|
||||
// unreliable across drivers. Backgrounds are drawn as raw GL quads
|
||||
// using NDC-space coordinates; QPainter only renders the text on top.
|
||||
|
||||
const char* RECT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec2 in_ndc;
|
||||
void main() {
|
||||
gl_Position = vec4(in_ndc, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* RECT_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
frag_color = u_color;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* LINE_FS = R"(
|
||||
#version 450 core
|
||||
in float v_dist_px;
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
)";
|
||||
|
||||
void uploadFloats(QOpenGLFunctions_4_5_Core* gl,
|
||||
GLuint vbo, size_t& capacity_bytes,
|
||||
const std::vector<float>& data) {
|
||||
const size_t bytes = data.size() * sizeof(float);
|
||||
if (bytes == 0) return;
|
||||
if (bytes > capacity_bytes) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
|
||||
capacity_bytes = new_cap;
|
||||
}
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data.data());
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
void uploadFloatBytes(QOpenGLFunctions_4_5_Core* gl,
|
||||
GLuint vbo, size_t& capacity_bytes,
|
||||
const float* data, size_t float_count) {
|
||||
const size_t bytes = float_count * sizeof(float);
|
||||
if (bytes == 0) return;
|
||||
if (bytes > capacity_bytes) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
|
||||
capacity_bytes = new_cap;
|
||||
}
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data);
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
// CPU expansion of N segments (3 floats * 2 verts per segment, packed) into
|
||||
// 6 vertices per segment, each carrying (a, b, side, along) = 8 floats.
|
||||
void expandLineSegments(const std::vector<float>& endpoints,
|
||||
std::vector<float>& out) {
|
||||
out.clear();
|
||||
if (endpoints.size() < 6) return;
|
||||
const size_t n_segs = endpoints.size() / 6;
|
||||
out.reserve(n_segs * 6 * 8);
|
||||
static const float CORNERS[6][2] = {
|
||||
{-1.0f, 0.0f}, {+1.0f, 0.0f}, {-1.0f, 1.0f},
|
||||
{-1.0f, 1.0f}, {+1.0f, 0.0f}, {+1.0f, 1.0f},
|
||||
};
|
||||
for (size_t s = 0; s < n_segs; ++s) {
|
||||
const float* a = &endpoints[s * 6 + 0];
|
||||
const float* b = &endpoints[s * 6 + 3];
|
||||
for (int c = 0; c < 6; ++c) {
|
||||
out.push_back(a[0]); out.push_back(a[1]); out.push_back(a[2]);
|
||||
out.push_back(b[0]); out.push_back(b[1]); out.push_back(b[2]);
|
||||
out.push_back(CORNERS[c][0]);
|
||||
out.push_back(CORNERS[c][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
|
||||
if (gl_) return;
|
||||
gl_ = gl;
|
||||
|
||||
// Triangle program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, TRI_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, TRI_FS);
|
||||
program_tri_ = link(gl_, vs, fs);
|
||||
u_tri_view_proj_ = gl_->glGetUniformLocation(program_tri_, "u_view_proj");
|
||||
u_tri_color_ = gl_->glGetUniformLocation(program_tri_, "u_color");
|
||||
}
|
||||
// Point program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, POINT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, POINT_FS);
|
||||
program_pt_ = link(gl_, vs, fs);
|
||||
u_pt_view_proj_ = gl_->glGetUniformLocation(program_pt_, "u_view_proj");
|
||||
u_pt_point_size_ = gl_->glGetUniformLocation(program_pt_, "u_point_size");
|
||||
u_pt_inner_color_ = gl_->glGetUniformLocation(program_pt_, "u_inner_color");
|
||||
u_pt_stroke_color_ = gl_->glGetUniformLocation(program_pt_, "u_stroke_color");
|
||||
u_pt_inner_radius_ = gl_->glGetUniformLocation(program_pt_, "u_inner_radius_norm");
|
||||
}
|
||||
// Line program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, LINE_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, LINE_FS);
|
||||
program_ln_ = link(gl_, vs, fs);
|
||||
u_ln_view_proj_ = gl_->glGetUniformLocation(program_ln_, "u_view_proj");
|
||||
u_ln_screen_size_ = gl_->glGetUniformLocation(program_ln_, "u_screen_size");
|
||||
u_ln_half_width_ = gl_->glGetUniformLocation(program_ln_, "u_half_width");
|
||||
u_ln_stroke_extra_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_extra");
|
||||
u_ln_inner_color_ = gl_->glGetUniformLocation(program_ln_, "u_inner_color");
|
||||
u_ln_stroke_color_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_color");
|
||||
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.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, RECT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, RECT_FS);
|
||||
program_rect_ = link(gl_, vs, fs);
|
||||
u_rect_color_ = gl_->glGetUniformLocation(program_rect_, "u_color");
|
||||
}
|
||||
|
||||
// Triangle VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &triangles_.vao);
|
||||
gl_->glCreateBuffers(1, &triangles_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(triangles_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(triangles_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(triangles_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(triangles_.vao, 0, triangles_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Point VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &points_.vao);
|
||||
gl_->glCreateBuffers(1, &points_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(points_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(points_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(points_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(points_.vao, 0, points_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Line VAO/VBO: 8 floats per vertex (a:vec3, b:vec3, side, along).
|
||||
// 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(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_);
|
||||
gl_->glCreateBuffers(1, &vbo_rect_);
|
||||
gl_->glEnableVertexArrayAttrib(vao_rect_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_rect_, 0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_rect_, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_rect_, 0, vbo_rect_, 0, 2 * sizeof(float));
|
||||
}
|
||||
|
||||
void OverlayRenderer::release() {
|
||||
if (!gl_) return;
|
||||
if (triangles_.vbo) gl_->glDeleteBuffers(1, &triangles_.vbo);
|
||||
if (triangles_.vao) gl_->glDeleteVertexArrays(1, &triangles_.vao);
|
||||
if (points_.vbo) gl_->glDeleteBuffers(1, &points_.vbo);
|
||||
if (points_.vao) gl_->glDeleteVertexArrays(1, &points_.vao);
|
||||
if (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_);
|
||||
if (program_pt_) gl_->glDeleteProgram(program_pt_);
|
||||
if (program_ln_) gl_->glDeleteProgram(program_ln_);
|
||||
if (program_rect_) gl_->glDeleteProgram(program_rect_);
|
||||
triangles_ = {};
|
||||
points_ = {};
|
||||
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;
|
||||
gl_ = nullptr;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setHudText(const QString& text) {
|
||||
hud_text_ = text;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLabels(const std::vector<Label>& labels) {
|
||||
labels_ = labels;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setSelectionRect(const QRect& rect_logical) {
|
||||
selection_rect_ = rect_logical;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a) {
|
||||
if (!gl_) return;
|
||||
triangles_.color[0] = r; triangles_.color[1] = g;
|
||||
triangles_.color[2] = b; triangles_.color[3] = a;
|
||||
triangles_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, triangles_.vbo, triangles_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_) return;
|
||||
points_.inner_color[0] = r; points_.inner_color[1] = g;
|
||||
points_.inner_color[2] = b; points_.inner_color[3] = a;
|
||||
points_.stroke_color[0] = sr; points_.stroke_color[1] = sg;
|
||||
points_.stroke_color[2] = sb; points_.stroke_color[3] = sa;
|
||||
points_.pixel_size = pixel_size;
|
||||
points_.stroke_extra = stroke_extra;
|
||||
points_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, points_.vbo, points_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
|
||||
if (!gl_) return;
|
||||
line_draws_.clear();
|
||||
|
||||
// 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],
|
||||
int pixel_w, int pixel_h, qreal dpr) {
|
||||
if (!gl_) return;
|
||||
|
||||
// Save GL state we touch.
|
||||
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
|
||||
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_pt_size = gl_->glIsEnabled(GL_PROGRAM_POINT_SIZE);
|
||||
GLboolean prev_depth_msk = GL_TRUE;
|
||||
gl_->glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_msk);
|
||||
GLint prev_depth_func = GL_LESS;
|
||||
gl_->glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
|
||||
GLint prev_blend_src = GL_ONE, prev_blend_dst = GL_ZERO;
|
||||
gl_->glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
|
||||
gl_->glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
|
||||
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDepthMask(GL_FALSE);
|
||||
gl_->glDepthFunc(GL_LEQUAL);
|
||||
gl_->glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
if (triangles_.vertex_count > 0 && triangles_.color[3] > 0.0f) {
|
||||
// Highlight triangles stay depth-aware (GL_LEQUAL) so they tint
|
||||
// the surface in place rather than poking through walls.
|
||||
gl_->glUseProgram(program_tri_);
|
||||
gl_->glUniformMatrix4fv(u_tri_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform4fv(u_tri_color_, 1, triangles_.color);
|
||||
gl_->glBindVertexArray(triangles_.vao);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, triangles_.vertex_count);
|
||||
}
|
||||
// Measurement annotations (lines + points) draw on top of every other
|
||||
// pass — the standard CAD convention. GL_ALWAYS wins every depth
|
||||
// compare; GL_LEQUAL is restored at the end of the function.
|
||||
gl_->glDepthFunc(GL_ALWAYS);
|
||||
if (!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_->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
|
||||
// inner colour vs the stroke band. pixel_size is the inner-disc
|
||||
// diameter; the sprite (and gl_PointSize) is enlarged by
|
||||
// 2*stroke_extra so the halo has somewhere to draw.
|
||||
const float total = points_.pixel_size + 2.0f * points_.stroke_extra;
|
||||
const float inner_ratio = (total > 0.0f)
|
||||
? (points_.pixel_size / total) : 1.0f;
|
||||
gl_->glUseProgram(program_pt_);
|
||||
gl_->glUniformMatrix4fv(u_pt_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform1f(u_pt_point_size_, total);
|
||||
gl_->glUniform1f(u_pt_inner_radius_, inner_ratio);
|
||||
gl_->glUniform4fv(u_pt_inner_color_, 1, points_.inner_color);
|
||||
gl_->glUniform4fv(u_pt_stroke_color_, 1, points_.stroke_color);
|
||||
gl_->glBindVertexArray(points_.vao);
|
||||
gl_->glDrawArrays(GL_POINTS, 0, points_.vertex_count);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
|
||||
if (!prev_blend) gl_->glDisable(GL_BLEND);
|
||||
gl_->glBlendFunc(prev_blend_src, prev_blend_dst);
|
||||
if (prev_cull) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_pt_size) gl_->glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
gl_->glDepthMask(prev_depth_msk);
|
||||
gl_->glDepthFunc(prev_depth_func);
|
||||
|
||||
if (pixel_w <= 0 || pixel_h <= 0) return;
|
||||
|
||||
const float logical_w = float(pixel_w) / float(dpr ? dpr : 1.0);
|
||||
const float logical_h = float(pixel_h) / float(dpr ? dpr : 1.0);
|
||||
auto px_to_ndc_x = [logical_w](float px) {
|
||||
return (px / logical_w) * 2.0f - 1.0f;
|
||||
};
|
||||
auto px_to_ndc_y = [logical_h](float px) {
|
||||
return 1.0f - (px / logical_h) * 2.0f;
|
||||
};
|
||||
|
||||
// Box-select rectangle: translucent fill + 1-px outline drawn as
|
||||
// four thin rects. Comes before the HUD/label pass so the HUD
|
||||
// backgrounds still render on top of the rectangle if they overlap.
|
||||
if (selection_rect_.isValid()
|
||||
&& selection_rect_.width() > 0
|
||||
&& selection_rect_.height() > 0) {
|
||||
struct RectPx { float x0, y0, x1, y1; };
|
||||
const QRect& sr = selection_rect_;
|
||||
const float sx0 = float(sr.left());
|
||||
const float sy0 = float(sr.top());
|
||||
const float sx1 = float(sr.right() + 1);
|
||||
const float sy1 = float(sr.bottom() + 1);
|
||||
const RectPx pieces[5] = {
|
||||
// Fill
|
||||
{sx0, sy0, sx1, sy1},
|
||||
// Top edge
|
||||
{sx0, sy0, sx1, sy0 + 1.0f},
|
||||
// Bottom edge
|
||||
{sx0, sy1 - 1.0f, sx1, sy1},
|
||||
// Left edge
|
||||
{sx0, sy0, sx0 + 1.0f, sy1},
|
||||
// Right edge
|
||||
{sx1 - 1.0f, sy0, sx1, sy1},
|
||||
};
|
||||
const float colors[5][4] = {
|
||||
{0.30f, 0.65f, 1.0f, 0.18f}, // fill
|
||||
{0.30f, 0.65f, 1.0f, 0.9f}, // outline (each edge)
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
};
|
||||
|
||||
GLboolean prev_dt2 = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cf2 = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_bl2 = gl_->glIsEnabled(GL_BLEND);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
|
||||
// Each piece is its own draw so we can switch alpha between
|
||||
// fill and outline. All five share the same VBO slot — we
|
||||
// stream-overwrite per draw.
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const float x0 = px_to_ndc_x(pieces[i].x0);
|
||||
const float x1 = px_to_ndc_x(pieces[i].x1);
|
||||
const float y0 = px_to_ndc_y(pieces[i].y0);
|
||||
const float y1 = px_to_ndc_y(pieces[i].y1);
|
||||
const float ndc[12] = {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
};
|
||||
uploadFloatBytes(gl_, vbo_rect_, vbo_rect_capacity_, ndc, 12);
|
||||
gl_->glUniform4f(u_rect_color_,
|
||||
colors[i][0], colors[i][1],
|
||||
colors[i][2], colors[i][3]);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_dt2) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cf2) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_bl2) gl_->glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// Two-stage HUD/label pass: collect rect bounds (in logical pixels) +
|
||||
// text strings, draw all rect backgrounds via GL (screen-space NDC
|
||||
// quads, depth test off), then run a QPainter pass that *only* draws
|
||||
// text on top. Side-stepping QPainter::fillRect entirely avoids the
|
||||
// QOpenGLPaintDevice quirk where solid fills silently drop while
|
||||
// text continues to render.
|
||||
const bool any_painter = !hud_text_.isEmpty() || !labels_.empty();
|
||||
if (!any_painter) return;
|
||||
|
||||
QFont label_font = overlayTextFont(9);
|
||||
QFont hud_font = overlayTextFont(11);
|
||||
const QFontMetrics lfm(label_font);
|
||||
const QFontMetrics hfm(hud_font);
|
||||
|
||||
const int label_pad_x = 4, label_pad_y = 2;
|
||||
const int hud_pad_x = 10, hud_pad_y = 6;
|
||||
const int hud_margin = 12;
|
||||
|
||||
struct PaintItem { QRect bg; QString text; const QFont* font; int align; };
|
||||
std::vector<PaintItem> items;
|
||||
items.reserve(labels_.size() + 1);
|
||||
|
||||
// World-anchored label rects.
|
||||
for (const auto& lbl : labels_) {
|
||||
const float* p = lbl.world_pos;
|
||||
// Column-major: M[col*4 + row].
|
||||
const float wx = view_proj[0]*p[0] + view_proj[4]*p[1] + view_proj[8]*p[2] + view_proj[12];
|
||||
const float wy = view_proj[1]*p[0] + view_proj[5]*p[1] + view_proj[9]*p[2] + view_proj[13];
|
||||
const float ww = view_proj[3]*p[0] + view_proj[7]*p[1] + view_proj[11]*p[2] + view_proj[15];
|
||||
if (ww <= 0.0f) continue; // behind camera
|
||||
const float ndc_x = wx / ww;
|
||||
const float ndc_y = wy / ww;
|
||||
if (ndc_x < -1.0f || ndc_x > 1.0f
|
||||
|| ndc_y < -1.0f || ndc_y > 1.0f) continue;
|
||||
const float sx = (ndc_x * 0.5f + 0.5f) * logical_w;
|
||||
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * logical_h;
|
||||
const int tw = lfm.horizontalAdvance(lbl.text);
|
||||
const int th = lfm.height();
|
||||
QRect bg(int(sx) - tw / 2 - label_pad_x,
|
||||
int(sy) - th / 2 - label_pad_y,
|
||||
tw + 2 * label_pad_x,
|
||||
th + 2 * label_pad_y);
|
||||
items.push_back({bg, lbl.text, &label_font, Qt::AlignCenter});
|
||||
}
|
||||
// HUD rect (always top-left if any text).
|
||||
if (!hud_text_.isEmpty()) {
|
||||
const QStringList lines = hud_text_.split('\n');
|
||||
int tw = 0;
|
||||
for (const auto& ln : lines) tw = qMax(tw, hfm.horizontalAdvance(ln));
|
||||
const int th = hfm.height() * lines.size();
|
||||
QRect bg(hud_margin, hud_margin,
|
||||
tw + 2 * hud_pad_x,
|
||||
th + 2 * hud_pad_y);
|
||||
items.push_back({bg, hud_text_, &hud_font, int(Qt::AlignLeft | Qt::AlignTop)});
|
||||
}
|
||||
|
||||
// GL pass: draw all background rects as NDC-space triangles.
|
||||
if (!items.empty()) {
|
||||
std::vector<float> ndc;
|
||||
ndc.reserve(items.size() * 12); // 6 verts * 2 floats per rect
|
||||
for (const auto& it : items) {
|
||||
const float x0 = px_to_ndc_x(float(it.bg.left()));
|
||||
const float x1 = px_to_ndc_x(float(it.bg.right() + 1));
|
||||
const float y0 = px_to_ndc_y(float(it.bg.top()));
|
||||
const float y1 = px_to_ndc_y(float(it.bg.bottom() + 1));
|
||||
ndc.insert(ndc.end(), {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
});
|
||||
}
|
||||
uploadFloats(gl_, vbo_rect_, vbo_rect_capacity_, ndc);
|
||||
|
||||
// GL_TRIANGLES respects GL_CULL_FACE; the NDC→window y-flip turns
|
||||
// our CCW NDC quads into window-CW which get back-culled if cull
|
||||
// is on (which it is by default in this app). Disable cull for
|
||||
// the rect pass — lines+points above were unaffected since
|
||||
// GL_LINES / GL_POINTS skip face culling entirely.
|
||||
GLboolean prev_depth_test = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cull_face = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_BLEND);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glUniform4f(u_rect_color_, 0.08f, 0.08f, 0.08f, 1.0f);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, GLsizei(items.size() * 6));
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_depth_test) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cull_face) gl_->glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
// QPainter pass: text only, on top of the GL-drawn backgrounds.
|
||||
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
|
||||
device.setDevicePixelRatio(dpr);
|
||||
QPainter painter(&device);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
painter.setPen(Qt::white);
|
||||
for (const auto& it : items) {
|
||||
painter.setFont(*it.font);
|
||||
const int px = it.font == &hud_font ? hud_pad_x : label_pad_x;
|
||||
const int py = it.font == &hud_font ? hud_pad_y : label_pad_y;
|
||||
painter.drawText(it.bg.adjusted(px, py, -px, -py), it.align, it.text);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 IFCVIEWER_OVERLAYRENDERER_H
|
||||
#define IFCVIEWER_OVERLAYRENDERER_H
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
|
||||
#include <vector>
|
||||
|
||||
// Neutral overlay-primitive renderer attached to ViewportWindow. Today it
|
||||
// draws a single tinted, translucent triangle list in world space — used by
|
||||
// the area-measurement tool to shade selected coplanar patches. More
|
||||
// primitives (lines, points, world-anchored labels) will land here as
|
||||
// future tools require them.
|
||||
//
|
||||
// Lifetime: owned by ViewportWindow, initialised in the same context. All
|
||||
// public methods assume the caller has already made the GL context current.
|
||||
class OverlayRenderer {
|
||||
public:
|
||||
void initialize(QOpenGLFunctions_4_5_Core* gl);
|
||||
void release();
|
||||
|
||||
// Replace the highlight-triangle list. `world_xyz` is 3 floats per
|
||||
// vertex, 3 verts per triangle, in world space (post-composed-transform).
|
||||
// Empty disables the overlay. Color is RGBA in [0, 1].
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// 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.
|
||||
//
|
||||
// When stroke_a > 0, every point is rendered twice: a wider
|
||||
// (pixel_size + 2*stroke_extra) outer dot in stroke_color, then the
|
||||
// pixel_size inner dot in the main color — giving a crisp halo that
|
||||
// reads on any background.
|
||||
void setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// World-anchored text labels: each one is projected to screen space
|
||||
// and drawn via QPainter at that pixel. Used today for per-segment
|
||||
// length readouts in the length tool.
|
||||
struct Label {
|
||||
float world_pos[3];
|
||||
QString text;
|
||||
};
|
||||
void setOverlayLabels(const std::vector<Label>& labels);
|
||||
|
||||
// Top-left HUD text drawn via QPainter on the GL surface as part of
|
||||
// render(). Empty hides the HUD.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Box-select rectangle (in logical pixel coords, top-left origin).
|
||||
// Drawn as a translucent fill + 1-px outline using the same
|
||||
// screen-space rect program that draws label/HUD backgrounds.
|
||||
// Empty rect hides it.
|
||||
void setSelectionRect(const QRect& rect_logical);
|
||||
|
||||
// Render every overlay primitive in order: GL highlight triangles
|
||||
// (using `view_proj`, column-major float[16]), then HUD text via
|
||||
// QPainter on a QOpenGLPaintDevice sized to (pixel_w × pixel_h)
|
||||
// with the supplied device pixel ratio. Caller is responsible for
|
||||
// ensuring glViewport covers the full surface — the QPainter pass
|
||||
// after this call leaves GL state in an undefined shape, so treat
|
||||
// this as the last GL operation per frame before swapBuffers (or
|
||||
// sandwich it before any pass that re-binds its own programs).
|
||||
void render(const float view_proj[16],
|
||||
int pixel_w, int pixel_h, qreal device_pixel_ratio);
|
||||
|
||||
private:
|
||||
// Triangle bundle: position-only VBO, single-color shader.
|
||||
struct TriBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float color[4] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Point bundle: position-only VBO, sprite shader uses gl_PointCoord
|
||||
// to draw an antialiased disc with an outlined halo.
|
||||
struct PointBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float inner_color[4] = {0, 0, 0, 0};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float pixel_size = 8.0f;
|
||||
float stroke_extra = 0.0f;
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// Triangle program (highlight tris).
|
||||
GLuint program_tri_ = 0;
|
||||
GLint u_tri_view_proj_ = -1;
|
||||
GLint u_tri_color_ = -1;
|
||||
|
||||
// Point program (sprite).
|
||||
GLuint program_pt_ = 0;
|
||||
GLint u_pt_view_proj_ = -1;
|
||||
GLint u_pt_point_size_ = -1;
|
||||
GLint u_pt_inner_color_ = -1;
|
||||
GLint u_pt_stroke_color_ = -1;
|
||||
GLint u_pt_inner_radius_ = -1;
|
||||
|
||||
// Line program (screen-space expanded quads).
|
||||
GLuint program_ln_ = 0;
|
||||
GLint u_ln_view_proj_ = -1;
|
||||
GLint u_ln_screen_size_ = -1;
|
||||
GLint u_ln_half_width_ = -1;
|
||||
GLint u_ln_stroke_extra_ = -1;
|
||||
GLint u_ln_inner_color_ = -1;
|
||||
GLint u_ln_stroke_color_ = -1;
|
||||
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
|
||||
// with depth test off so rects stack on top of the entire scene.
|
||||
GLuint program_rect_ = 0;
|
||||
GLint u_rect_color_ = -1;
|
||||
GLuint vao_rect_ = 0;
|
||||
GLuint vbo_rect_ = 0;
|
||||
size_t vbo_rect_capacity_ = 0;
|
||||
|
||||
TriBundle triangles_;
|
||||
PointBundle points_;
|
||||
|
||||
// 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_;
|
||||
|
||||
// Box-select rectangle in logical pixels. Null/empty = hidden.
|
||||
QRect selection_rect_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_OVERLAYRENDERER_H
|
||||
@@ -37,7 +37,7 @@ void SceneLoader::setShouldWriteSidecar(bool enabled) {
|
||||
should_write_sidecar_ = enabled;
|
||||
}
|
||||
|
||||
SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent)
|
||||
SceneLoader::SceneLoader(WgpuViewportWindow* viewport, QObject* parent)
|
||||
: QObject(parent), viewport_(viewport)
|
||||
{
|
||||
connect(&element_poll_timer_, &QTimer::timeout,
|
||||
@@ -189,11 +189,11 @@ void SceneLoader::startNextLoad() {
|
||||
joinSidecarThread();
|
||||
sidecar_read_thread_ = std::thread([this, ifc_path, mid, is_sidecar_source]() {
|
||||
QElapsedTimer rt; rt.start();
|
||||
auto cached = readSidecar(ifc_path);
|
||||
qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
|
||||
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
|
||||
auto cached = readSidecarMetadataOnly(ifc_path);
|
||||
qDebug(" Sidecar metadata read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
|
||||
auto result = std::make_shared<std::optional<StreamingSidecar>>(std::move(cached));
|
||||
QMetaObject::invokeMethod(this, [this, mid, result, is_sidecar_source]() {
|
||||
if (*result && !(*result)->instances.empty()) {
|
||||
if (*result && !(*result)->meta.instances.empty()) {
|
||||
applySidecarData(mid, std::move(**result));
|
||||
if (!is_sidecar_source) {
|
||||
startDataSourceLoad(mid);
|
||||
@@ -233,36 +233,37 @@ void SceneLoader::startStreamLoadFor(uint32_t mid) {
|
||||
m.file_path.toStdString(), next_object_id_, loading_model_id_);
|
||||
}
|
||||
|
||||
void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
|
||||
void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) {
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
auto& model = it->second;
|
||||
SidecarData& d = metadata.meta;
|
||||
|
||||
qDebug("Sidecar hit: %s (%zu verts, %zu indices, %zu meshes, %zu instances, %zu elements)",
|
||||
qDebug("Sidecar hit: %s (%zu metadata bytes, %zu indices, %zu meshes, %zu instances, %zu elements)",
|
||||
model.file_path.toStdString().c_str(),
|
||||
data.vertices.size() / INSTANCED_VERTEX_STRIDE_BYTES,
|
||||
data.indices.size(),
|
||||
data.meshes.size(),
|
||||
data.instances.size(),
|
||||
data.elements.size());
|
||||
size_t(metadata.vertex_total_bytes),
|
||||
size_t(metadata.index_total_count),
|
||||
d.meshes.size(),
|
||||
d.instances.size(),
|
||||
d.elements.size());
|
||||
|
||||
// Rebase object/model IDs onto the current session's ID space. Two
|
||||
// cached models both starting at object_id=1 would collide otherwise.
|
||||
uint32_t min_oid = UINT32_MAX;
|
||||
for (const auto& pe : data.elements) {
|
||||
for (const auto& pe : d.elements) {
|
||||
if (pe.object_id < min_oid) min_oid = pe.object_id;
|
||||
}
|
||||
uint32_t oid_offset = 0;
|
||||
if (!data.elements.empty() && min_oid < UINT32_MAX) {
|
||||
if (!d.elements.empty() && min_oid < UINT32_MAX) {
|
||||
oid_offset = next_object_id_ - min_oid;
|
||||
}
|
||||
for (auto& pe : data.elements) {
|
||||
for (auto& pe : d.elements) {
|
||||
pe.object_id += oid_offset;
|
||||
pe.model_id = mid;
|
||||
if (pe.object_id >= next_object_id_)
|
||||
next_object_id_ = pe.object_id + 1;
|
||||
}
|
||||
for (auto& inst : data.instances) {
|
||||
for (auto& inst : d.instances) {
|
||||
inst.object_id += oid_offset;
|
||||
inst.model_id = mid;
|
||||
}
|
||||
@@ -273,26 +274,26 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
|
||||
// .ifc/.rdb sibling is absent.
|
||||
{
|
||||
ModelGeoref& gr = model.georef;
|
||||
gr.has_coordinate_operation = data.has_coordinate_operation != 0;
|
||||
gr.has_coordinate_operation = d.has_coordinate_operation != 0;
|
||||
Eigen::Map<const Eigen::Matrix<double, 4, 4, Eigen::ColMajor>> M(
|
||||
data.coordinate_operation_meters);
|
||||
d.coordinate_operation_meters);
|
||||
gr.coordinate_operation_meters = M;
|
||||
gr.units.project_length_to_meters = data.project_length_to_meters;
|
||||
gr.units.map_unit_to_meters = data.map_unit_to_meters;
|
||||
gr.units.project_length_to_meters = d.project_length_to_meters;
|
||||
gr.units.map_unit_to_meters = d.map_unit_to_meters;
|
||||
model.has_georef = true;
|
||||
}
|
||||
|
||||
if (!data.instances.empty() && !model.has_first_placement) {
|
||||
if (!d.instances.empty() && !model.has_first_placement) {
|
||||
using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
|
||||
model.first_placement =
|
||||
Eigen::Map<const Mat4dCol>(data.instances[0].placement_transformation);
|
||||
Eigen::Map<const Mat4dCol>(d.instances[0].placement_transformation);
|
||||
model.has_first_placement = true;
|
||||
}
|
||||
|
||||
std::vector<PackedElementInfo> elements = std::move(data.elements);
|
||||
std::string stbl = std::move(data.string_table);
|
||||
std::vector<PackedElementInfo> elements = std::move(d.elements);
|
||||
std::string stbl = std::move(d.string_table);
|
||||
|
||||
viewport_->applyCachedModel(mid, std::move(data));
|
||||
viewport_->applyCachedModel(mid, std::move(metadata));
|
||||
|
||||
emit sidecarElementsReady(mid, std::move(elements), std::move(stbl));
|
||||
|
||||
@@ -397,8 +398,11 @@ void SceneLoader::onStreamerFinished() {
|
||||
next_object_id_ = m.streamer->lastObjectId();
|
||||
viewport_->finalizeModel(mid);
|
||||
|
||||
// Sidecar finalize + LOD application + disk write. Runs on the
|
||||
// GUI thread so applyLodExtension's GL touches are safe.
|
||||
// Sidecar finalize + disk write. Wgpu has no live LOD1 apply —
|
||||
// LOD1 indices land in the on-disk sidecar and are picked up
|
||||
// on the *next* open of this file; first-session view is
|
||||
// LOD0-only. Acceptable trade-off vs reallocating chunk index
|
||||
// slices live to splice LOD1 in.
|
||||
if (m.sidecar_builder) {
|
||||
ModelGeoref georef;
|
||||
if (auto* file = m.streamer->ifcFile()) {
|
||||
@@ -406,7 +410,6 @@ void SceneLoader::onStreamerFinished() {
|
||||
}
|
||||
QElapsedTimer wt; wt.start();
|
||||
SidecarData data = m.sidecar_builder->finalize(georef, m.streamed_elements);
|
||||
viewport_->applyLodExtension(mid, data);
|
||||
const bool ok = writeSidecar(m.file_path.toStdString(), data);
|
||||
qDebug(" Sidecar finalize + write: %lld ms (%s)",
|
||||
wt.elapsed(), ok ? "ok" : "FAILED");
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "Federation.h"
|
||||
#include "ViewportWindow.h"
|
||||
#include "../ifcviewer-wgpu/WgpuViewportWindow.h"
|
||||
#include "../ifcviewer-wgpu/WgpuStreamingLoader.h"
|
||||
#include "GeometryStreamer.h"
|
||||
#include "SidecarBuilder.h"
|
||||
#include "SidecarCache.h"
|
||||
|
||||
// Drives IFC file loading into a ViewportWindow. Owns the per-model
|
||||
// Drives IFC file loading into a WgpuViewportWindow. Owns the per-model
|
||||
// GeometryStreamer, the load queue, the sidecar read thread, and the
|
||||
// next-free object_id counter used to rebase cached models onto the
|
||||
// current session's ID space.
|
||||
@@ -54,7 +55,7 @@
|
||||
class SceneLoader : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SceneLoader(ViewportWindow* viewport, QObject* parent = nullptr);
|
||||
explicit SceneLoader(WgpuViewportWindow* viewport, QObject* parent = nullptr);
|
||||
~SceneLoader();
|
||||
|
||||
// Sidecar cache use is opt-in per direction. Embedders that don't care
|
||||
@@ -175,10 +176,10 @@ private:
|
||||
void connectStreamer(GeometryStreamer* streamer);
|
||||
void joinSidecarThread();
|
||||
void joinDataSourceThreads();
|
||||
void applySidecarData(uint32_t mid, SidecarData data);
|
||||
void applySidecarData(uint32_t mid, StreamingSidecar metadata);
|
||||
void startDataSourceLoad(uint32_t mid);
|
||||
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
WgpuViewportWindow* viewport_ = nullptr;
|
||||
bool should_read_sidecar_ = false;
|
||||
bool should_write_sidecar_ = false;
|
||||
std::map<uint32_t, Model> models_;
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "Selection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
SelectionState::SelectionState(QObject* parent) : QObject(parent) {
|
||||
// Reserve slot 0 — object_id 0 is the "no object" sentinel and the
|
||||
// shader still indexes the buffer with v_object_id, so the slot must
|
||||
// exist (and be zero) to avoid OOB reads.
|
||||
cpu_flags_.assign(1, 0u);
|
||||
}
|
||||
|
||||
SelectionState::~SelectionState() = default;
|
||||
|
||||
void SelectionState::initializeGl(QOpenGLFunctions_4_5_Core* gl) {
|
||||
gl_ = gl;
|
||||
if (ssbo_ == 0) {
|
||||
gl_->glCreateBuffers(1, &ssbo_);
|
||||
}
|
||||
// Force a fresh upload against the new context.
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::releaseGl() {
|
||||
if (gl_ && ssbo_) {
|
||||
gl_->glDeleteBuffers(1, &ssbo_);
|
||||
}
|
||||
ssbo_ = 0;
|
||||
ssbo_capacity_slots_ = 0;
|
||||
gl_ = nullptr;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::reset() {
|
||||
const bool had_state = !selected_ids_.empty() || active_id_ != 0;
|
||||
selected_ids_.clear();
|
||||
active_id_ = 0;
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
markDirty();
|
||||
if (had_state) emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::noteObjectId(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
// Grow CPU side; the SSBO is sized lazily inside uploadFlags so
|
||||
// we don't realloc GL on every streamed instance.
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void SelectionState::setSelectedObjectId(uint32_t id) {
|
||||
if (id == 0) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
setSelection({id}, id);
|
||||
}
|
||||
|
||||
void SelectionState::setSelection(const std::unordered_set<uint32_t>& ids,
|
||||
uint32_t active) {
|
||||
// Avoid emitting churn when the call is a no-op.
|
||||
if (ids == selected_ids_ && active == active_id_) return;
|
||||
selected_ids_ = ids;
|
||||
selected_ids_.erase(0);
|
||||
active_id_ = (active != 0 && selected_ids_.count(active)) ? active : 0u;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::addToSelection(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any_added = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (id == 0) continue;
|
||||
if (selected_ids_.insert(id).second) any_added = true;
|
||||
}
|
||||
if (!any_added) return;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::removeFromSelection(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any_removed = false;
|
||||
bool active_removed = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (selected_ids_.erase(id) > 0) {
|
||||
any_removed = true;
|
||||
if (id == active_id_) active_removed = true;
|
||||
}
|
||||
}
|
||||
if (!any_removed) return;
|
||||
if (active_removed) active_id_ = 0;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::toggleInSelection(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (selected_ids_.erase(id) > 0) {
|
||||
// Removed. If it was active, drop active.
|
||||
if (active_id_ == id) active_id_ = 0;
|
||||
} else {
|
||||
// Added. Last-toggled becomes active so the properties panel
|
||||
// tracks the most recent click — matches the user's "last single
|
||||
// clicked is active" expectation.
|
||||
selected_ids_.insert(id);
|
||||
active_id_ = id;
|
||||
}
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::clearSelection() {
|
||||
if (selected_ids_.empty() && active_id_ == 0) return;
|
||||
selected_ids_.clear();
|
||||
active_id_ = 0;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::markDirty() {
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::growTo(uint32_t capacity_ids) {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
if (capacity_ids <= ssbo_capacity_slots_) return;
|
||||
// Round up to a power-of-two-ish step so streamed scenes don't realloc
|
||||
// every few instances. Floor at 1024 slots = 4 KB.
|
||||
size_t new_cap = std::max<size_t>(1024, ssbo_capacity_slots_ * 2);
|
||||
while (new_cap < capacity_ids) new_cap *= 2;
|
||||
gl_->glNamedBufferData(ssbo_,
|
||||
GLsizeiptr(new_cap * sizeof(uint32_t)),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
ssbo_capacity_slots_ = new_cap;
|
||||
}
|
||||
|
||||
void SelectionState::uploadFlags() {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
|
||||
// Rebuild the CPU flag vector from the canonical selected_ids_. The
|
||||
// overhead is O(|cpu_flags_|), which scales with max object_id rather
|
||||
// than with selection size — acceptable: object_ids are dense so this
|
||||
// is just a memset + a handful of writes for the selected set.
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
for (uint32_t id : selected_ids_) {
|
||||
if (id < cpu_flags_.size()) cpu_flags_[id] = 1u;
|
||||
}
|
||||
|
||||
growTo(static_cast<uint32_t>(cpu_flags_.size()));
|
||||
if (ssbo_capacity_slots_ == 0) return;
|
||||
|
||||
const GLsizeiptr bytes =
|
||||
GLsizeiptr(cpu_flags_.size() * sizeof(uint32_t));
|
||||
if (bytes > 0) {
|
||||
gl_->glNamedBufferSubData(ssbo_, 0, bytes, cpu_flags_.data());
|
||||
}
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
void SelectionState::bindForRender(GLuint binding_index) {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
if (dirty_) uploadFlags();
|
||||
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_index, ssbo_);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 IFCVIEWER_SELECTION_H
|
||||
#define IFCVIEWER_SELECTION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Multi-selection state for the viewport, owned by ViewportWindow.
|
||||
//
|
||||
// Tracks the *set* of currently selected object_ids plus a single "active"
|
||||
// id — the last single-clicked one. The active id is what the properties
|
||||
// panel and tree mirror; the full set is what the viewport highlights.
|
||||
//
|
||||
// Selection state is published to the main shader through a per-object_id
|
||||
// flags SSBO bound at a caller-chosen index (binding=3 today). The buffer
|
||||
// is sized to max(object_id) + 1 and grown on demand via noteObjectId,
|
||||
// which the viewport calls for every appended instance.
|
||||
//
|
||||
// On every mutation the selection emits `changed(active_id)`. Consumers
|
||||
// (MainWindow, the viewport itself for requestUpdate) connect to it.
|
||||
class SelectionState : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SelectionState(QObject* parent = nullptr);
|
||||
~SelectionState() override;
|
||||
|
||||
// Wire up the GL context. Must be called once the viewport's GL
|
||||
// context is current. release() drops GL resources before context
|
||||
// teardown.
|
||||
void initializeGl(QOpenGLFunctions_4_5_Core* gl);
|
||||
void releaseGl();
|
||||
|
||||
// Tell the manager about a newly added object_id so the flag buffer
|
||||
// can grow ahead of the next render. Cheap when the id fits in the
|
||||
// already-allocated CPU vector; otherwise resizes (and marks dirty).
|
||||
void noteObjectId(uint32_t id);
|
||||
|
||||
// Clear everything — both the selection set and the per-object flags.
|
||||
// Called from clearScene.
|
||||
void reset();
|
||||
|
||||
// ---- Mutation API ----
|
||||
//
|
||||
// Plain LMB click → setSelectedObjectId(id) (or clearSelection() for 0).
|
||||
// Modifier+click → toggleInSelection(id).
|
||||
// Box-select on release → setSelection / addToSelection / removeFromSelection
|
||||
// depending on the modifier held when the drag started.
|
||||
//
|
||||
// setSelection's `active` should be in `ids` or 0; if it isn't, the
|
||||
// active is silently coerced to 0.
|
||||
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
void setSelection(const std::unordered_set<uint32_t>& ids, uint32_t active);
|
||||
void addToSelection(const std::unordered_set<uint32_t>& ids);
|
||||
void removeFromSelection(const std::unordered_set<uint32_t>& ids);
|
||||
void toggleInSelection(uint32_t id);
|
||||
void clearSelection();
|
||||
|
||||
// ---- Accessors ----
|
||||
|
||||
bool isSelected(uint32_t id) const { return selected_ids_.count(id) > 0; }
|
||||
bool empty() const { return selected_ids_.empty(); }
|
||||
size_t size() const { return selected_ids_.size(); }
|
||||
const std::unordered_set<uint32_t>& selectionIds() const { return selected_ids_; }
|
||||
uint32_t activeObjectId() const { return active_id_; }
|
||||
|
||||
// ---- GL binding ----
|
||||
//
|
||||
// Bind the selection-flags SSBO at the given binding index for the
|
||||
// upcoming draw. Lazily uploads any pending flag changes. Caller
|
||||
// must have GL context current.
|
||||
void bindForRender(GLuint binding_index);
|
||||
|
||||
signals:
|
||||
// Emitted on any mutation that changes either the set or the active.
|
||||
// Carries the new active id for convenience (consumers usually only
|
||||
// care about the active for properties/tree sync).
|
||||
void changed(uint32_t active_id);
|
||||
|
||||
private:
|
||||
// Mark the SSBO dirty so the next bindForRender() re-uploads it.
|
||||
void markDirty();
|
||||
// Resize the CPU flag vector + GL buffer to hold up to capacity_ids
|
||||
// entries. Called when noteObjectId outgrows the current capacity.
|
||||
void growTo(uint32_t capacity_ids);
|
||||
// Fully overwrite cpu_flags_ from selected_ids_, then upload to GL.
|
||||
void uploadFlags();
|
||||
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
|
||||
std::unordered_set<uint32_t> selected_ids_;
|
||||
uint32_t active_id_ = 0;
|
||||
|
||||
// Per-object_id flag, indexed by id directly (slot 0 unused — object_id 0
|
||||
// means "no object"). Bit 0 = selected. Stored as uint32 per slot for
|
||||
// std430 alignment simplicity; the byte cost (~4 MB at 1M objects) is
|
||||
// negligible compared to the instance SSBO.
|
||||
std::vector<uint32_t> cpu_flags_;
|
||||
GLuint ssbo_ = 0;
|
||||
size_t ssbo_capacity_slots_ = 0;
|
||||
bool dirty_ = true;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_SELECTION_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,834 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 VIEWPORTWINDOW_H
|
||||
#define VIEWPORTWINDOW_H
|
||||
|
||||
#include <QWindow>
|
||||
#include <QOpenGLContext>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector3D>
|
||||
#include <QSet>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QTimer;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
#include <deque>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include "BvhAccel.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "OverlayRenderer.h"
|
||||
#include "Selection.h"
|
||||
#include "SidecarCache.h"
|
||||
#include "Visibility.h"
|
||||
|
||||
// Matches GL_DRAW_INDIRECT_BUFFER layout for glMultiDrawElementsIndirect.
|
||||
struct DrawElementsIndirectCommand {
|
||||
uint32_t count;
|
||||
uint32_t instanceCount;
|
||||
uint32_t firstIndex;
|
||||
uint32_t baseVertex;
|
||||
uint32_t baseInstance;
|
||||
};
|
||||
|
||||
// Per-model GPU state for the instanced render path.
|
||||
//
|
||||
// VBO: local-coord interleaved verts (pos3 + normal3 + color1_packed) — 28 B.
|
||||
// EBO: mesh-local indices (uint32).
|
||||
// meshes[]: per-unique-representation metadata; indexed by local_mesh_id.
|
||||
// instances[]: CPU-side per-instance records; sorted by mesh_id at finalize.
|
||||
// ssbo: InstanceGpu[]; populated at finalize.
|
||||
//
|
||||
// A model is drawable once `finalized == true`.
|
||||
struct ModelGpuData {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
GLuint ebo = 0;
|
||||
GLuint ssbo = 0;
|
||||
GLuint mesh_info_ssbo = 0; // MeshGpu[] — per-mesh quantization basis
|
||||
size_t mesh_info_capacity = 0; // bytes
|
||||
|
||||
size_t vbo_capacity = 0;
|
||||
size_t ebo_capacity = 0;
|
||||
size_t ssbo_capacity = 0; // bytes
|
||||
size_t vbo_used = 0;
|
||||
size_t ebo_used = 0;
|
||||
uint32_t vertex_count = 0; // total (across all meshes)
|
||||
uint32_t total_triangles = 0;
|
||||
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances; // unsorted
|
||||
// 1:1 with instances[] — true when the instance transform has
|
||||
// det < 0 (a reflection). Reflected instances need their
|
||||
// triangle winding treated as reversed so GL_CULL_FACE culls
|
||||
// the correct side.
|
||||
std::vector<uint8_t> instance_reflected;
|
||||
uint32_t ssbo_instance_count = 0;
|
||||
|
||||
// Stats snapshot from the last cullAndUploadVisible call. Cached so we
|
||||
// can report the same numbers on skipped-cull frames (see
|
||||
// have_cached_cull_ on ViewportWindow) without iterating the per-model
|
||||
// scratch array again.
|
||||
uint32_t cached_visible_objects = 0;
|
||||
uint32_t cached_visible_triangles = 0;
|
||||
|
||||
// Per-instance world AABB + BVH (built at finalize). The BVH is the
|
||||
// same ordering as `instances`; bvh_items[i] corresponds to instances[i].
|
||||
std::vector<BvhItem> bvh_items;
|
||||
ModelBvh bvh;
|
||||
|
||||
// Dynamic visible-instance index buffer (std430, binding = 1).
|
||||
// Re-uploaded each frame from visible_flat_.
|
||||
GLuint visible_ssbo = 0;
|
||||
size_t visible_ssbo_capacity = 0; // bytes
|
||||
|
||||
// GL_DRAW_INDIRECT_BUFFER of DrawElementsIndirectCommand[], one per
|
||||
// non-empty mesh. Re-uploaded each frame.
|
||||
GLuint indirect_buffer = 0;
|
||||
size_t indirect_capacity = 0; // bytes
|
||||
uint32_t indirect_command_count = 0; // total valid commands this frame
|
||||
uint32_t indirect_forward_count = 0; // first N are CCW-winding draws
|
||||
|
||||
// Per-model cull scratch — owned by the model so each cull job runs
|
||||
// without sharing mutable state. Four buckets = {fwd, rev} × {LOD0, LOD1}.
|
||||
std::vector<std::vector<uint32_t>> vis_fwd_lod0;
|
||||
std::vector<std::vector<uint32_t>> vis_fwd_lod1;
|
||||
std::vector<std::vector<uint32_t>> vis_rev_lod0;
|
||||
std::vector<std::vector<uint32_t>> vis_rev_lod1;
|
||||
std::vector<uint32_t> visible_flat;
|
||||
std::vector<DrawElementsIndirectCommand> indirect_scratch;
|
||||
std::vector<uint32_t> dirty_meshes;
|
||||
|
||||
bool finalized = false;
|
||||
bool hidden = false;
|
||||
|
||||
// Per-model federation-pipeline matrices in metres. Default identity
|
||||
// → no per-model contribution to the composed transform. See
|
||||
// Federation.h for the full pipeline composition order.
|
||||
Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity();
|
||||
Eigen::Matrix4d model_transformation_meters = Eigen::Matrix4d::Identity();
|
||||
};
|
||||
|
||||
// Rendering is event-driven: render() runs only when QEvent::UpdateRequest
|
||||
// is delivered, posted via requestUpdate(). An idle scene costs zero CPU.
|
||||
// INVARIANT: every public mutator that changes what should be on screen
|
||||
// (camera, selection, model lifecycle, visibility) MUST call requestUpdate()
|
||||
// before returning, or the viewport will go silently stale.
|
||||
class ViewportWindow : public QWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ViewportWindow(QWindow* parent = nullptr);
|
||||
~ViewportWindow();
|
||||
|
||||
// Streaming ingress.
|
||||
void uploadMeshChunk(const MeshChunk& chunk);
|
||||
void uploadInstanceChunk(const InstanceChunk& chunk);
|
||||
|
||||
// Called once all chunks for a model have arrived: sorts instances by
|
||||
// mesh_id, assigns each mesh its contiguous range, and uploads the
|
||||
// instance SSBO. The model becomes drawable.
|
||||
void finalizeModel(uint32_t model_id);
|
||||
|
||||
void resetScene();
|
||||
|
||||
// Restore a finalised model from a cached SidecarData struct. Replaces
|
||||
// any existing state for model_id and marks it drawable.
|
||||
void applyCachedModel(uint32_t model_id, SidecarData data);
|
||||
|
||||
// After buildLods() has extended sd.indices + populated lod1_* fields,
|
||||
// push just the appended index slice + the refreshed mesh metadata onto
|
||||
// the live GPU state for model_id. VBO / SSBO / instance array are left
|
||||
// alone; only the EBO grows and m.meshes is replaced. No-op if the
|
||||
// model isn't finalised on the viewport.
|
||||
void applyLodExtension(uint32_t model_id, const SidecarData& sd);
|
||||
|
||||
void hideModel(uint32_t model_id);
|
||||
void showModel(uint32_t model_id);
|
||||
void removeModel(uint32_t model_id);
|
||||
|
||||
// Debug helper: walk to the currently selected object's instance and
|
||||
// qInfo a sample vertex (decoded from the VBO), the placement
|
||||
// transformation matrix, and the global matrix
|
||||
// (CoordinateOperation · placement_transformation), in metres.
|
||||
void printSelectedObjectCoords();
|
||||
|
||||
// Mesh-local CPU triangles read back from the VBO/EBO. Positions are
|
||||
// dequantised against the mesh's local AABB; units match the streamer
|
||||
// (metres). Indices are mesh-local (0..vertex_count-1).
|
||||
struct MeshTriangles {
|
||||
std::vector<float> positions; // 3 * vertex_count
|
||||
std::vector<uint32_t> indices; // 3 * triangle_count
|
||||
};
|
||||
|
||||
// Lazy GPU readback of one mesh's triangles. Fails if model_id /
|
||||
// mesh_id aren't live, the model isn't finalised, or GL isn't ready.
|
||||
// Stalls the GL pipeline for the readback — call from the main thread
|
||||
// and not from inside render().
|
||||
bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
|
||||
MeshTriangles& out);
|
||||
|
||||
// Pure CPU lookup: object_id → owning model + mesh + raw streamer
|
||||
// placement matrix (column-major, pre-CoordinateOperation /
|
||||
// FederatedFalseOrigin / ModelTransformation).
|
||||
struct InstanceLookup {
|
||||
uint32_t model_id = 0;
|
||||
uint32_t mesh_id = 0;
|
||||
double placement_transformation[16]{};
|
||||
};
|
||||
bool findInstance(uint32_t object_id, InstanceLookup& out) const;
|
||||
|
||||
// Pick + resolve to mesh-local space. Runs pickSurfaceAt to get the
|
||||
// world-space hit, then inverts the instance's composed transform
|
||||
// (FederatedFalseOrigin · ModelTransformation · CoordinateOperation
|
||||
// · placement_transformation) to express the hit in the mesh's own
|
||||
// coordinates — what readbackMeshTriangles returns. Returns false if
|
||||
// the click missed geometry or its object_id has no live instance.
|
||||
struct MeshLocalPick {
|
||||
uint32_t object_id = 0;
|
||||
uint32_t model_id = 0;
|
||||
uint32_t mesh_id = 0;
|
||||
float mesh_local[3] = {0, 0, 0};
|
||||
float world_pos[3] = {0, 0, 0};
|
||||
float world_normal[3]= {0, 0, 0};
|
||||
// The instance's composed (FederatedFalseOrigin · ModelTransformation
|
||||
// · CoordinateOperation · placement_transformation) matrix in
|
||||
// column-major form. Mesh-local positions × this = world. Caching
|
||||
// by callers becomes stale if any federation matrix is later edited.
|
||||
float composed_transform[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
};
|
||||
bool pickMeshLocalAt(int x, int y, MeshLocalPick& out);
|
||||
// Resolve a mesh-local point to the IFC's own georeferenced global
|
||||
// frame: CoordinateOperation · placement_transformation · mesh_local.
|
||||
// Excludes FederatedFalseOrigin and ModelTransformation.
|
||||
bool meshLocalToGlobal(uint32_t object_id, const float mesh_local[3],
|
||||
double global_out[3]) const;
|
||||
|
||||
// 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
|
||||
// active tool. Backspace/Delete in length mode emits
|
||||
// toolBackspacePressed for "remove last point" semantics.
|
||||
// Area / Length consume LMB clicks via surfacePickedInTool; Volume
|
||||
// is passive — selection behaves as in None and the host just gates
|
||||
// its volume HUD/labels on this mode.
|
||||
enum class ToolMode { None, Area, Length, Volume };
|
||||
Q_ENUM(ToolMode)
|
||||
|
||||
void toggleAreaTool();
|
||||
void toggleLengthTool();
|
||||
void toggleVolumeTool();
|
||||
void setToolMode(ToolMode mode);
|
||||
ToolMode toolMode() const { return tool_mode_; }
|
||||
|
||||
// Replace the overlay highlight-triangle list rendered after the main
|
||||
// pass. `world_xyz` is 3 floats per vertex, 3 verts per triangle, in
|
||||
// world space. Empty disables the overlay. Triggers a viewport
|
||||
// update so the change becomes visible immediately.
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// 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
|
||||
// stroke_a > 0 the sprite is enlarged by 2*stroke_extra to draw a
|
||||
// halo around the inner disc — proper outlined points via the
|
||||
// sprite shader (no two-pass glPointSize trickery).
|
||||
void setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float stroke_r, float stroke_g, float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// Replace the world-anchored label list — projected to screen space
|
||||
// and drawn via QPainter inside the same overlay pass as the HUD.
|
||||
// Used today for per-segment length readouts.
|
||||
void setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels);
|
||||
|
||||
// Multi-line HUD text drawn via QPainter on top of the GL surface at
|
||||
// the end of each frame. Empty hides the HUD. Newlines split the
|
||||
// string into separate rows in the same translucent box.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Federation pipeline: composed instance transform =
|
||||
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation
|
||||
// · placement_transformation
|
||||
// setFederatedFalseOrigin affects every model; the per-model setters
|
||||
// affect a single model. Each setter rewrites the SSBO, recomputes
|
||||
// world AABBs, rebuilds the BVH, and posts an update. Defaults are
|
||||
// identity, so until a setter is called the composed transform equals
|
||||
// placement_transformation.
|
||||
void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters);
|
||||
void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters);
|
||||
void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters);
|
||||
void setBackgroundColor(const QColor& color);
|
||||
void enterFpsMode();
|
||||
|
||||
// Selection. The viewport owns a SelectionState that tracks both
|
||||
// the multi-set and the "active" (last single-clicked) id. External
|
||||
// callers (MainWindow / tree sync) drive selection through it.
|
||||
SelectionState& selection() { return selection_; }
|
||||
const SelectionState& selection() const { return selection_; }
|
||||
// Convenience: replace the selection with {id} (or clear if id==0).
|
||||
// Used by callers that have not yet been ported off the single-id API.
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
|
||||
uint32_t pickObjectAt(int x, int y);
|
||||
// Render the pick pass and collect every distinct non-zero object_id
|
||||
// covered by the pixels inside `rect` (logical coords, top-left origin).
|
||||
// Used by the box-select drag. Returns an empty set if rect is empty
|
||||
// or off-surface.
|
||||
std::unordered_set<uint32_t> picksInRect(const QRect& rect);
|
||||
|
||||
// Per-element visibility (independent of model-level hidden flag).
|
||||
// External callers go through the convenience verbs below; the
|
||||
// VisibilityState getter is exposed for read access (e.g. the host
|
||||
// mirroring its state into a tree's grey-out style).
|
||||
VisibilityState& visibility() { return visibility_; }
|
||||
const VisibilityState& visibility() const { return visibility_; }
|
||||
// Hide every currently-selected element. Selection itself is
|
||||
// preserved — toggling them back on with showAllElements() leaves
|
||||
// the same items selected.
|
||||
void hideSelectedElements();
|
||||
// Hide every element NOT in the current selection (limited to
|
||||
// visible models — model-hidden objects are left as-is so they
|
||||
// stay model-hidden rather than picking up a redundant
|
||||
// element-hidden flag too). No-op when nothing is selected.
|
||||
void isolateSelectedElements();
|
||||
// Clear element-level visibility overrides for every loaded model.
|
||||
// Model-level `hidden` flags are not touched — a hidden model stays
|
||||
// hidden, matching the user's expectation of "show all *elements*".
|
||||
void showAllElements();
|
||||
// Flip element-level visibility across every live element in visible
|
||||
// models. Model-hidden objects are skipped and remain model-hidden.
|
||||
void invertElementVisibility();
|
||||
|
||||
// Extended pick: returns the object id, world-space hit point, and
|
||||
// world-space surface normal at (x, y). Renders the same pick pass as
|
||||
// pickObjectAt but reads back from two extra color attachments
|
||||
// (world_pos in RGB32F, world_normal in RGB16F). Returns false if the
|
||||
// click missed all geometry; the out-params are then untouched.
|
||||
bool pickSurfaceAt(int x, int y,
|
||||
uint32_t& object_id_out,
|
||||
QVector3D& world_pos_out,
|
||||
QVector3D& world_normal_out);
|
||||
|
||||
// Section planes — fragment-shader clipping with up to MaxSectionPlanes
|
||||
// active. Each plane clips the half-space dot(n, p) + d > 0. addSection-
|
||||
// PlaneAtSurface auto-flips the normal toward the camera so the first
|
||||
// click immediately cuts away the camera-facing side.
|
||||
static constexpr int MaxSectionPlanes = 8;
|
||||
struct SectionPlane {
|
||||
QVector3D n; // unit world-space normal
|
||||
QVector3D origin; // point on the plane — the gizmo's anchor
|
||||
float d; // = -dot(n, origin); kept in sync with origin
|
||||
};
|
||||
int sectionPlaneCount() const { return int(section_planes_.size()); }
|
||||
bool addSectionPlaneAtSurface(const QVector3D& point, const QVector3D& normal);
|
||||
void removeSectionPlane(int index);
|
||||
void clearSectionPlanes();
|
||||
|
||||
// Section tool: when active, LMB on geometry creates a new plane (using
|
||||
// pickSurfaceAt for hit-point + normal); LMB on an existing plane's
|
||||
// arrow gizmo selects + drags it; Delete removes the selected plane;
|
||||
// Esc or another K-press exits the tool.
|
||||
void toggleSectionTool();
|
||||
bool sectionToolActive() const { return section_tool_active_; }
|
||||
|
||||
// Projection: orthographic vs perspective. In ortho mode the visible
|
||||
// box is sized to match what the perspective camera would show at the
|
||||
// pivot's distance, so toggling at any zoom level keeps the framing.
|
||||
void toggleProjection();
|
||||
bool projectionOrtho() const { return projection_ortho_; }
|
||||
|
||||
// Snap the camera to a canonical axis-aligned view. Yaw/pitch are
|
||||
// clamped according to the orbit convention; target and distance are
|
||||
// preserved (the user explicitly asked for a rotate-only behavior).
|
||||
void setStandardView(float yaw_deg, float pitch_deg);
|
||||
|
||||
void setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch);
|
||||
void setBenchmarkFrames(int n);
|
||||
|
||||
// Queue a one-shot framebuffer capture: at the end of the next render()
|
||||
// (just before swapBuffers), the default framebuffer is read back with
|
||||
// glReadPixels, saved as PNG to `path`, and — if `quit_after` — the
|
||||
// QCoreApplication is asked to quit. Used by parity-diff harnesses to
|
||||
// produce a GL output PNG that's directly comparable to the wgpu
|
||||
// backend's --screenshot.
|
||||
void captureNextFrameToPng(const QString& path, bool quit_after = true);
|
||||
QString cameraString() const;
|
||||
|
||||
// Move camera_target_ to the selected set's world-AABB centroid and
|
||||
// dolly camera_distance_ so the union fits the current viewport.
|
||||
// Yaw/pitch are preserved. No-op if no selected object has an AABB.
|
||||
void focusOnSelectedObject();
|
||||
// Frame the union of all finalized models. No-op if the scene is empty.
|
||||
void viewAll();
|
||||
|
||||
// World-space AABB query. Returns false when the object has no live
|
||||
// instance or no mesh AABB yet (caller should treat as "unknown").
|
||||
bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const;
|
||||
|
||||
struct CameraState {
|
||||
QVector3D target;
|
||||
float distance;
|
||||
float yaw; // degrees
|
||||
float pitch; // degrees
|
||||
};
|
||||
CameraState cameraState() const;
|
||||
|
||||
struct FrameStats {
|
||||
float fps;
|
||||
float frame_time_ms;
|
||||
uint32_t total_objects;
|
||||
uint32_t visible_objects;
|
||||
uint32_t total_triangles;
|
||||
uint32_t visible_triangles;
|
||||
uint32_t unique_meshes;
|
||||
uint32_t gl_draw_calls; // actual glMultiDrawElementsIndirect issues per frame
|
||||
uint32_t indirect_sub_draws; // total commands packed into those indirect buffers
|
||||
};
|
||||
|
||||
signals:
|
||||
void objectPicked(uint32_t object_id);
|
||||
void initialized();
|
||||
void frameStatsUpdated(const ViewportWindow::FrameStats& stats);
|
||||
// Emitted instead of objectPicked when any measurement tool is active.
|
||||
// The app branches on toolMode() to decide what to do, calls
|
||||
// pickMeshLocalAt(x, y, ...) for hit details, and accumulates.
|
||||
// modifiers carries Qt::KeyboardModifiers at click time (Alt etc.).
|
||||
void surfacePickedInTool(int x, int y, int modifiers);
|
||||
// Emitted whenever the active tool changes (incl. on→off transitions).
|
||||
// The app uses this to reset accumulator state on entry/exit.
|
||||
void toolModeChanged(ViewportWindow::ToolMode mode);
|
||||
// Backspace/Delete pressed while a tool is active. Used by the length
|
||||
// tool to remove the last point; other tools may ignore it.
|
||||
void toolBackspacePressed();
|
||||
|
||||
protected:
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void keyReleaseEvent(QKeyEvent* event) override;
|
||||
bool event(QEvent* event) override;
|
||||
|
||||
private:
|
||||
// Resolved mouse-binding for the active AppSettings::NavPreset.
|
||||
// Selection always stays on LMB; only orbit and pan move around
|
||||
// (presets are picked so neither lands on plain LMB).
|
||||
struct NavBindings {
|
||||
Qt::MouseButton orbit_button;
|
||||
Qt::KeyboardModifiers orbit_mods;
|
||||
Qt::MouseButton pan_button;
|
||||
Qt::KeyboardModifiers pan_mods;
|
||||
};
|
||||
NavBindings currentNavBindings() const;
|
||||
|
||||
enum class PendingOpType {
|
||||
UploadMeshChunk,
|
||||
UploadInstanceChunk,
|
||||
FinalizeModel,
|
||||
ApplyCachedModel,
|
||||
ApplyLodExtension,
|
||||
ResetScene,
|
||||
HideModel,
|
||||
ShowModel,
|
||||
RemoveModel,
|
||||
};
|
||||
|
||||
struct PendingOperation {
|
||||
PendingOpType type;
|
||||
MeshChunk mesh_chunk;
|
||||
InstanceChunk instance_chunk;
|
||||
SidecarData sidecar_data;
|
||||
uint32_t model_id = 0;
|
||||
};
|
||||
|
||||
void initGL();
|
||||
void flushPendingOperations();
|
||||
void enqueuePendingOperation(PendingOperation op);
|
||||
void render();
|
||||
void renderPickPass();
|
||||
void renderAxisGizmo();
|
||||
void renderPivotIndicator();
|
||||
void renderSectionPlanes();
|
||||
void buildSectionPlaneGizmo();
|
||||
// Post-process edge enhancement: resolve MSAA depth into a single-
|
||||
// sample texture, then run a fullscreen pass that detects sharp
|
||||
// depth-laplacian peaks and darkens the colour buffer there. Catches
|
||||
// silhouettes and overlapping-surface boundaries as faint dark lines.
|
||||
void renderEdgePass();
|
||||
// Returns the index of the section plane whose arrow gizmo is under
|
||||
// (x, y), or -1 if none. Screen-space line-segment distance test.
|
||||
int hitTestSectionGizmo(int x, int y) const;
|
||||
// Update section_planes_[section_drag_index_] from the current cursor
|
||||
// position by projecting the move onto the plane's normal axis in
|
||||
// screen space.
|
||||
void updateSectionDrag(int x, int y);
|
||||
void updateCamera();
|
||||
|
||||
// Scene-wide AABB used by viewAll(). Returns false when the scene
|
||||
// has no finalized geometry (caller should leave the camera alone).
|
||||
bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const;
|
||||
// Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits
|
||||
// vertically and horizontally within the current FOV, with `padding`
|
||||
// headroom (1.0 = tight). Yaw/pitch are preserved; only target and
|
||||
// distance change.
|
||||
void frameAabb(const QVector3D& mn, const QVector3D& mx, float padding);
|
||||
void buildShaders();
|
||||
void buildAxisGizmo();
|
||||
void buildPivotIndicator();
|
||||
// Show/hide the orbit-pivot marker. hide_after_ms > 0 starts a single-shot
|
||||
// timer that auto-hides — used by the wheel handler to give the marker a
|
||||
// short afterglow after zoom. Drag-based callers pass 0 and toggle
|
||||
// visibility manually on press/release.
|
||||
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
|
||||
void setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo);
|
||||
|
||||
// Resolve the default framebuffer's MSAA depth into a single-sample
|
||||
// texture, read it back, and max-reduce a mip pyramid on the CPU. The
|
||||
// resulting pyramid is stored in hiz_pyramid_ along with the VP matrix
|
||||
// used to draw it; next frame's cullAndUploadVisible can test AABBs
|
||||
// against it. Synchronous readback — at 256×128 the cost is sub-ms
|
||||
// and not a measured bottleneck; Phase 3D's compute-shader cull will
|
||||
// eliminate the readback entirely.
|
||||
void buildHizPyramid();
|
||||
|
||||
// True if the AABB is fully occluded by the previous frame's depth.
|
||||
// Returns false when the HiZ is invalid, the AABB crosses the near
|
||||
// plane, or the projection falls outside NDC.
|
||||
bool aabbOccludedByHiz(const float mn[3], const float mx[3]) const;
|
||||
bool growModelVbo(ModelGpuData& m, size_t needed_total);
|
||||
bool growModelEbo(ModelGpuData& m, size_t needed_total);
|
||||
bool growModelSsbo(ModelGpuData& m, size_t needed_total);
|
||||
ModelGpuData& getOrCreateModel(uint32_t model_id);
|
||||
|
||||
// Frustum-cull m's instances (BVH if available, else linear scan),
|
||||
// build the per-mesh DrawElementsIndirectCommand array + flat visible
|
||||
// list, and upload both to m.indirect_buffer / m.visible_ssbo.
|
||||
//
|
||||
// `min_pixel_radius` controls contribution culling: instances (and BVH
|
||||
// subtrees) whose projected bounding-sphere radius would be below this
|
||||
// many pixels are dropped. 0 = disabled (all frustum-visible kept),
|
||||
// which is what the pick pass uses so clickable targets aren't filtered.
|
||||
void cullAndUploadVisible(ModelGpuData& m, const float planes[6][4],
|
||||
float focal_px, float min_pixel_radius);
|
||||
|
||||
// Thread-safe: CPU-only cull (frustum + contribution + HiZ + bucketing +
|
||||
// emit). Writes survivors into m.vis_* / m.visible_flat / m.indirect_scratch
|
||||
// and sets m.indirect_forward_count / m.indirect_command_count /
|
||||
// m.cached_visible_*. Touches no GL state and no ViewportWindow mutable
|
||||
// state other than the atomic counters below — safe to run on a worker.
|
||||
void cullModelCpu(ModelGpuData& m, const float planes[6][4],
|
||||
float focal_px, float min_pixel_radius);
|
||||
|
||||
// Main-thread only: uploads m.visible_flat / m.indirect_scratch into the
|
||||
// model's SSBO + indirect buffer, growing them if needed.
|
||||
void uploadCullResults(ModelGpuData& m);
|
||||
|
||||
// Compose
|
||||
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation
|
||||
// · placement_transformation
|
||||
// for one instance: writes inst.transform (composed, float) and
|
||||
// recomputes inst.world_aabb_* from the composed transform + the
|
||||
// mesh's local AABB. Maths runs in double; narrow to float at the end.
|
||||
void composeInstanceFromPlacement(InstanceCpu& inst, const ModelGpuData& m) const;
|
||||
|
||||
// Walk every instance of `model_id`, recompose `transform` from
|
||||
// `placement_transformation` + current stage matrices, recompute world
|
||||
// AABBs, re-upload the InstanceGpu SSBO, refresh instance_reflected,
|
||||
// and rebuild the BVH. Posts an update. No-op if model_id is unknown
|
||||
// or GL isn't initialised yet.
|
||||
void recomposeAndUploadModel(uint32_t model_id);
|
||||
|
||||
// Mouse interaction
|
||||
void handleMousePress(QMouseEvent* event);
|
||||
void handleMouseRelease(QMouseEvent* event);
|
||||
void handleMouseMove(QMouseEvent* event);
|
||||
void handleWheel(QWheelEvent* event);
|
||||
|
||||
// FPS/fly mode. Toggled with Shift+F; exits on any mouse click or Esc.
|
||||
// While active, WASD translates the camera in view-space, Q/E moves
|
||||
// world-down/up, mouse rotates the view (cursor hidden + recentered each
|
||||
// move), Shift accelerates, and the wheel scales the base move speed
|
||||
// instead of zooming. The underlying orbit state is preserved: movement
|
||||
// translates camera_target_ and rotation re-pins it so camera_eye_ stays
|
||||
// put, so exiting drops the user back into orbit at the same viewpoint.
|
||||
//
|
||||
// Movement is integrated inside render() using wall-clock dt and the
|
||||
// next frame is requestUpdate()'d while any movement key is held — that
|
||||
// way one long frame only produces a single catch-up step instead of
|
||||
// also missing a QTimer tick.
|
||||
enum class CameraMode { Orbit, Fps };
|
||||
void exitFpsMode();
|
||||
void fpsIntegrate(); // called from render()
|
||||
void recenterFpsCursor();
|
||||
|
||||
QOpenGLContext* context_ = nullptr;
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
bool gl_initialized_ = false;
|
||||
QColor background_color_ = QColor("#202329");
|
||||
std::deque<PendingOperation> pending_ops_;
|
||||
|
||||
// Shaders
|
||||
GLuint main_program_ = 0;
|
||||
GLuint pick_program_ = 0;
|
||||
GLuint axis_program_ = 0;
|
||||
|
||||
// Axis gizmo
|
||||
GLuint axis_vao_ = 0;
|
||||
GLuint axis_vbo_ = 0;
|
||||
|
||||
// Orbit-pivot indicator: 1 center vertex + (N+1) rim vertices on the unit
|
||||
// circle (last == first to close the triangle fan). Rendered as a screen-
|
||||
// space disc at camera_target_, visible only while the user is navigating.
|
||||
GLuint pivot_program_ = 0;
|
||||
GLuint pivot_vao_ = 0;
|
||||
GLuint pivot_vbo_ = 0;
|
||||
int pivot_rim_count_ = 0;
|
||||
|
||||
// Per-model GPU data
|
||||
std::unordered_map<uint32_t, ModelGpuData> models_gpu_;
|
||||
|
||||
// FederatedFalseOrigin matrix, in metres. Default identity → no
|
||||
// contribution to the composed transform. See Federation.h.
|
||||
Eigen::Matrix4d federated_false_origin_meters_ = Eigen::Matrix4d::Identity();
|
||||
|
||||
// Pick framebuffer. Three color attachments:
|
||||
// 0: R32UI — object_id
|
||||
// 1: RGB32F — world position at hit
|
||||
// 2: RGB16F — world normal at hit (already flipped for reflections in
|
||||
// the vertex shader)
|
||||
GLuint pick_fbo_ = 0;
|
||||
GLuint pick_color_tex_ = 0;
|
||||
GLuint pick_pos_tex_ = 0;
|
||||
GLuint pick_normal_tex_ = 0;
|
||||
GLuint pick_depth_rbo_ = 0;
|
||||
int pick_width_ = 0;
|
||||
int pick_height_ = 0;
|
||||
|
||||
// HiZ occlusion culling (Phase 3C).
|
||||
//
|
||||
// Each frame after the main draw we blit the MSAA depth buffer down
|
||||
// into a single-sample depth texture (hiz_fbo_ / hiz_depth_tex_), then
|
||||
// glReadPixels it into hiz_depth_readback_. We max-reduce that into a
|
||||
// mip pyramid (hiz_pyramid_) and remember the VP matrix used
|
||||
// (hiz_vp_ + hiz_vp_valid_) so next frame's cull can test AABBs
|
||||
// against a slightly-stale depth. Skipped for the pick pass and when
|
||||
// IFC_NO_HIZ=1.
|
||||
GLuint hiz_downsample_program_ = 0;
|
||||
GLuint hiz_downsample_vao_ = 0;
|
||||
GLuint hiz_fbo_ = 0;
|
||||
GLuint hiz_depth_tex_ = 0;
|
||||
GLuint hiz_resolve_fbo_ = 0; // full-size single-sample resolve
|
||||
GLuint hiz_resolve_depth_tex_ = 0;
|
||||
int hiz_resolve_w_ = 0;
|
||||
int hiz_resolve_h_ = 0;
|
||||
int hiz_base_w_ = 0;
|
||||
int hiz_base_h_ = 0;
|
||||
std::vector<float> hiz_depth_readback_; // hiz_base_w_ * hiz_base_h_ floats
|
||||
std::vector<float> hiz_pyramid_; // concatenated mip levels
|
||||
std::vector<uint32_t> hiz_mip_offset_; // into hiz_pyramid_
|
||||
std::vector<uint32_t> hiz_mip_w_;
|
||||
std::vector<uint32_t> hiz_mip_h_;
|
||||
QMatrix4x4 hiz_vp_;
|
||||
bool hiz_vp_valid_ = false;
|
||||
std::atomic<uint32_t> hiz_reject_count_{0}; // per-frame stat
|
||||
|
||||
// Cull-phase timers. Accumulated across all frames in the current
|
||||
// 1-second stats window; divided by frame_count_ at print time to
|
||||
// give per-frame average ms. Reset each window. Lets us see where
|
||||
// CPU time actually goes: bucket clears vs BVH traversal vs emit vs
|
||||
// GPU upload.
|
||||
// Atomic so parallel cull workers can fetch_add into them without
|
||||
// contending on a lock. clr/trv/emt are SUMS across all worker threads
|
||||
// for the frame — they describe total CPU work, not wall-clock. The
|
||||
// wall counter is measured once around the dispatch block in render()
|
||||
// and is what actually determines frame time.
|
||||
std::atomic<uint64_t> cull_clear_ns_{0};
|
||||
std::atomic<uint64_t> cull_traverse_ns_{0};
|
||||
std::atomic<uint64_t> cull_emit_ns_{0};
|
||||
std::atomic<uint64_t> cull_upload_ns_{0};
|
||||
uint64_t cull_wall_ns_ = 0; // main-thread only
|
||||
uint32_t cull_skipped_frames_ = 0;
|
||||
|
||||
// Skip cullAndUploadVisible + buildHizPyramid when the camera and scene
|
||||
// haven't changed since the last cull. The existing per-model
|
||||
// indirect_buffer / visible_ssbo are still correct and just get
|
||||
// redrawn. Invalidated by any function that mutates models_gpu_.
|
||||
QMatrix4x4 last_cull_view_;
|
||||
QMatrix4x4 last_cull_proj_;
|
||||
bool have_cached_cull_ = false;
|
||||
|
||||
// Motion-adaptive contribution culling. During camera motion, use a
|
||||
// larger pixel-radius threshold to aggressively cull small objects.
|
||||
// When the camera stops, re-cull once at the base threshold.
|
||||
bool last_cull_was_motion_ = false;
|
||||
|
||||
// Benchmark mode: render N frames, collect stats, then exit.
|
||||
int benchmark_total_ = 0;
|
||||
int benchmark_count_ = 0;
|
||||
int benchmark_warmup_ = 5;
|
||||
float benchmark_yaw_start_ = 0.0f;
|
||||
float benchmark_yaw_speed_ = 0.5f; // degrees per frame
|
||||
std::vector<float> benchmark_frame_times_;
|
||||
|
||||
// Queued one-shot framebuffer capture (see captureNextFrameToPng).
|
||||
QString pending_screenshot_path_;
|
||||
bool pending_screenshot_quit_ = false;
|
||||
|
||||
// Per-frame stats
|
||||
uint32_t visible_triangles_ = 0;
|
||||
uint32_t visible_objects_ = 0;
|
||||
uint32_t gl_draw_calls_ = 0;
|
||||
uint32_t indirect_sub_draws_ = 0;
|
||||
|
||||
// Camera
|
||||
QVector3D camera_target_{0, 0, 0};
|
||||
QVector3D camera_eye_{0, 0, 0}; // world-space eye, set in updateCamera
|
||||
float camera_distance_ = 50.0f;
|
||||
float camera_yaw_ = 45.0f;
|
||||
float camera_pitch_ = 30.0f;
|
||||
float camera_fov_y_deg_ = 45.0f;
|
||||
bool projection_ortho_ = false;
|
||||
QMatrix4x4 view_matrix_;
|
||||
QMatrix4x4 proj_matrix_;
|
||||
|
||||
// Mouse
|
||||
Qt::MouseButton active_button_ = Qt::NoButton;
|
||||
QPoint last_mouse_pos_;
|
||||
|
||||
// Pivot indicator visibility — true while drag-navigating, or briefly
|
||||
// after a wheel notch (the timer auto-clears it).
|
||||
bool pivot_indicator_visible_ = false;
|
||||
QTimer* pivot_indicator_hide_timer_ = nullptr;
|
||||
|
||||
// FPS/fly mode state. fps_keys_held_ tracks WASD/QE/Shift between
|
||||
// press+release; fps_last_tick_ gates dt inside render().
|
||||
// fps_ignore_next_mouse_move_ swallows the synthetic MouseMove that
|
||||
// QCursor::setPos() generates after we recenter.
|
||||
CameraMode camera_mode_ = CameraMode::Orbit;
|
||||
QSet<int> fps_keys_held_;
|
||||
float fps_move_speed_ = 5.0f; // m/s at speed=1
|
||||
QElapsedTimer fps_last_tick_;
|
||||
bool fps_ignore_next_mouse_move_ = false;
|
||||
|
||||
// Selection — set + active id + per-object_id flags SSBO (binding=3).
|
||||
SelectionState selection_;
|
||||
// Per-element visibility — CPU-only flag vector consulted by the
|
||||
// cull, plus a canonical hidden-id set for mutation/reporting.
|
||||
VisibilityState visibility_;
|
||||
|
||||
// LMB-press state. press_pick_id_ caches the object hit at press
|
||||
// time so the release path can apply click semantics without a
|
||||
// second pick pass. box_select_armed_ is set only when the press
|
||||
// landed on empty space — a subsequent drag past the click
|
||||
// threshold then promotes to box_select_active_, so a small wobble
|
||||
// on a click doesn't accidentally box-select. Modifiers captured
|
||||
// at press-time decide commit semantics: plain replace, Shift add,
|
||||
// Ctrl remove.
|
||||
uint32_t press_pick_id_ = 0;
|
||||
bool box_select_armed_ = false;
|
||||
bool box_select_active_ = false;
|
||||
QPoint box_select_start_pos_;
|
||||
QPoint box_select_current_pos_;
|
||||
Qt::KeyboardModifiers box_select_press_mods_ = Qt::NoModifier;
|
||||
|
||||
// Active measurement tool: see ToolMode / surfacePickedInTool.
|
||||
ToolMode tool_mode_ = ToolMode::None;
|
||||
|
||||
// Renders any client-supplied overlay primitives (highlight triangles
|
||||
// today; lines/points/labels later) in their own pass after the main
|
||||
// geometry, with depth-test on / depth-write off.
|
||||
OverlayRenderer overlay_renderer_;
|
||||
|
||||
// Active section planes. Uploaded as uniform array each frame to the
|
||||
// main + pick programs; capped at MaxSectionPlanes.
|
||||
std::vector<SectionPlane> section_planes_;
|
||||
bool section_tool_active_ = false;
|
||||
int section_plane_selected_ = -1;
|
||||
bool section_drag_active_ = false;
|
||||
int section_drag_index_ = -1;
|
||||
QVector3D section_drag_start_origin_;
|
||||
QPoint section_drag_start_mouse_;
|
||||
// Push the current plane list into a freshly-bound program. No-op if
|
||||
// the program does not declare u_clip_count / u_clip_planes.
|
||||
void uploadClipPlaneUniforms(GLuint program);
|
||||
|
||||
// GL resources for the per-plane visualization (quad outline + arrow).
|
||||
GLuint plane_program_ = 0;
|
||||
GLuint plane_vao_ = 0;
|
||||
GLuint plane_vbo_ = 0;
|
||||
int plane_quad_offset_ = 0;
|
||||
int plane_quad_count_ = 0;
|
||||
int plane_arrow_offset_ = 0;
|
||||
int plane_arrow_count_ = 0;
|
||||
|
||||
// Edge-enhancement pass resources. edge_depth_tex_ is a single-sample
|
||||
// resolve target the size of the window; we blit the default FB depth
|
||||
// into it each frame, then sample it from the fullscreen edge shader.
|
||||
GLuint edge_program_ = 0;
|
||||
GLuint edge_depth_fbo_ = 0;
|
||||
GLuint edge_depth_tex_ = 0;
|
||||
GLuint edge_vao_ = 0; // empty VAO for fullscreen-triangle draw
|
||||
int edge_w_ = 0;
|
||||
int edge_h_ = 0;
|
||||
|
||||
// FPS smoothing
|
||||
int frame_count_ = 0;
|
||||
float accumulated_time_ = 0.0f;
|
||||
float last_fps_ = 0.0f;
|
||||
};
|
||||
|
||||
#endif // VIEWPORTWINDOW_H
|
||||
@@ -1,93 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "Visibility.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
VisibilityState::VisibilityState(QObject* parent) : QObject(parent) {
|
||||
// Slot 0 is the "no object" sentinel; keep it present so isHidden(0)
|
||||
// is well-defined (returns false).
|
||||
cpu_flags_.assign(1, 0u);
|
||||
}
|
||||
|
||||
void VisibilityState::noteObjectId(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
}
|
||||
|
||||
void VisibilityState::reset() {
|
||||
const bool had_state = !hidden_ids_.empty();
|
||||
hidden_ids_.clear();
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
if (had_state) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::hideObjects(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (id == 0) continue;
|
||||
if (hidden_ids_.insert(id).second) {
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
cpu_flags_[id] = 1u;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::showObjects(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (hidden_ids_.erase(id) > 0) {
|
||||
if (id < cpu_flags_.size()) cpu_flags_[id] = 0u;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::setHidden(const std::unordered_set<uint32_t>& ids) {
|
||||
if (ids == hidden_ids_) return;
|
||||
hidden_ids_ = ids;
|
||||
hidden_ids_.erase(0);
|
||||
rebuildFlags();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::showAll() {
|
||||
if (hidden_ids_.empty()) return;
|
||||
hidden_ids_.clear();
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::rebuildFlags() {
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
for (uint32_t id : hidden_ids_) {
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
cpu_flags_[id] = 1u;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 IFCVIEWER_VISIBILITY_H
|
||||
#define IFCVIEWER_VISIBILITY_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Per-element visibility state, owned by ViewportWindow. Independent of
|
||||
// the per-model `hidden` flag — model-level hiding still wins (a hidden
|
||||
// model never draws regardless of its elements' visibility). This class
|
||||
// only tracks element-level overrides on top.
|
||||
//
|
||||
// Lookup is hot — the CPU cull queries `isHidden(object_id)` for every
|
||||
// surviving instance — so the canonical set is mirrored into a flat
|
||||
// per-id byte vector indexed directly by object_id. Ownership is purely
|
||||
// CPU: nothing on the GPU reads visibility, the cull just skips hidden
|
||||
// instances before they reach the visible[] SSBO.
|
||||
class VisibilityState : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VisibilityState(QObject* parent = nullptr);
|
||||
|
||||
// Tell the manager about a newly added object_id so the flag vector
|
||||
// can grow ahead of the next cull. Cheap when the id fits already.
|
||||
void noteObjectId(uint32_t id);
|
||||
|
||||
// Drop everything — clears the hidden set. Called from clearScene.
|
||||
void reset();
|
||||
|
||||
// ---- Mutation ----
|
||||
//
|
||||
// hideObjects: union into the hidden set.
|
||||
// showObjects: subtract from the hidden set.
|
||||
// setHidden: replace the hidden set wholesale (used by isolate).
|
||||
// showAll: equivalent to setHidden({}).
|
||||
void hideObjects(const std::unordered_set<uint32_t>& ids);
|
||||
void showObjects(const std::unordered_set<uint32_t>& ids);
|
||||
void setHidden(const std::unordered_set<uint32_t>& ids);
|
||||
void showAll();
|
||||
|
||||
// ---- Hot-path query ----
|
||||
//
|
||||
// Inline so the cull's `if (visibility_.isHidden(...)) return;`
|
||||
// compiles to a bounds check + a byte load + a compare.
|
||||
bool isHidden(uint32_t id) const {
|
||||
return id < cpu_flags_.size() && cpu_flags_[id] != 0;
|
||||
}
|
||||
|
||||
// ---- Accessors ----
|
||||
bool empty() const { return hidden_ids_.empty(); }
|
||||
size_t size() const { return hidden_ids_.size(); }
|
||||
const std::unordered_set<uint32_t>& hiddenIds() const { return hidden_ids_; }
|
||||
|
||||
signals:
|
||||
// Emitted on any mutation that changes the hidden set. Consumers
|
||||
// (the viewport) connect to invalidate cached cull state and
|
||||
// requestUpdate.
|
||||
void changed();
|
||||
|
||||
private:
|
||||
// Recompute cpu_flags_ from hidden_ids_. Cheap: O(|cpu_flags_|).
|
||||
void rebuildFlags();
|
||||
|
||||
std::unordered_set<uint32_t> hidden_ids_;
|
||||
std::vector<uint8_t> cpu_flags_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_VISIBILITY_H
|
||||
@@ -32,10 +32,6 @@ function(add_ifcviewer_unit_test name)
|
||||
catch_discover_tests(${name})
|
||||
endfunction()
|
||||
|
||||
add_ifcviewer_unit_test(test_bvh_accel
|
||||
SOURCES ${IFCVIEWER_SRC}/BvhAccel.cpp
|
||||
)
|
||||
|
||||
if(WITH_MESH_OPTIMIZER)
|
||||
add_ifcviewer_unit_test(test_lod_builder
|
||||
SOURCES
|
||||
@@ -50,11 +46,9 @@ add_ifcviewer_unit_test(test_sidecar_cache
|
||||
|
||||
add_ifcviewer_unit_test(test_instanced_geometry)
|
||||
|
||||
# Federation, Visibility and Selection are Qt-derived (QObject + signals).
|
||||
# Unlike the other Tier-1 tests they have to pull Qt6 in directly and enable
|
||||
# AUTOMOC for the Q_OBJECT moc-generation. OpenGL is needed only by the
|
||||
# Selection test (Selection.cpp references QOpenGLFunctions_4_5_Core).
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Test OpenGL REQUIRED PATHS ${QT_DIR})
|
||||
# Federation is Qt-derived (QObject + signals). It has to pull Qt6 in
|
||||
# directly and enable AUTOMOC for the Q_OBJECT moc-generation.
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Test REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
find_package(Eigen3 REQUIRED)
|
||||
|
||||
@@ -82,36 +76,3 @@ target_link_libraries(test_federation PRIVATE
|
||||
IfcParse # Unit.cpp uses express::Base / file APIs
|
||||
)
|
||||
catch_discover_tests(test_federation)
|
||||
|
||||
# VisibilityState / SelectionState are QObjects (for their changed() signal)
|
||||
# but their construction + mutation API touch no GL, so the tests exercise
|
||||
# the pure CPU state machine without ever creating a context. Selection.cpp
|
||||
# still references QOpenGLFunctions_4_5_Core, so test_selection has to link
|
||||
# Qt6::OpenGL even though no GL call is reached at runtime.
|
||||
add_executable(test_visibility
|
||||
test_visibility.cpp
|
||||
${IFCVIEWER_SRC}/Visibility.cpp
|
||||
)
|
||||
set_target_properties(test_visibility PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(test_visibility PRIVATE ${IFCVIEWER_SRC})
|
||||
target_link_libraries(test_visibility PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Test # QSignalSpy
|
||||
)
|
||||
catch_discover_tests(test_visibility)
|
||||
|
||||
add_executable(test_selection
|
||||
test_selection.cpp
|
||||
${IFCVIEWER_SRC}/Selection.cpp
|
||||
)
|
||||
set_target_properties(test_selection PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(test_selection PRIVATE ${IFCVIEWER_SRC})
|
||||
target_link_libraries(test_selection PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Gui # QtOpenGL depends on QtGui
|
||||
Qt${QT_VERSION}::OpenGL # Selection.cpp: QOpenGLFunctions_4_5_Core
|
||||
Qt${QT_VERSION}::Test # QSignalSpy
|
||||
)
|
||||
catch_discover_tests(test_selection)
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "BvhAccel.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
BvhItem makeItem(float x, float y, float z, float r, uint32_t model_id = 1) {
|
||||
BvhItem it{};
|
||||
it.aabb_min[0] = x - r;
|
||||
it.aabb_min[1] = y - r;
|
||||
it.aabb_min[2] = z - r;
|
||||
it.aabb_max[0] = x + r;
|
||||
it.aabb_max[1] = y + r;
|
||||
it.aabb_max[2] = z + r;
|
||||
it.model_id = model_id;
|
||||
return it;
|
||||
}
|
||||
|
||||
bool aabbContains(const float outer_min[3], const float outer_max[3],
|
||||
const float inner_min[3], const float inner_max[3]) {
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (inner_min[a] < outer_min[a]) return false;
|
||||
if (inner_max[a] > outer_max[a]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void verifyNode(const ModelBvh& mbvh,
|
||||
const std::vector<BvhItem>& items,
|
||||
uint32_t node_idx) {
|
||||
REQUIRE(node_idx < mbvh.nodes.size());
|
||||
const BvhNode& node = mbvh.nodes[node_idx];
|
||||
|
||||
if (node.count > 0) {
|
||||
// Leaf: every item's AABB must be inside the node AABB.
|
||||
REQUIRE(node.count <= BVH_MAX_LEAF_SIZE);
|
||||
for (uint32_t k = 0; k < node.count; ++k) {
|
||||
uint32_t idx = mbvh.item_indices[node.right_or_first + k];
|
||||
REQUIRE(idx < items.size());
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max,
|
||||
items[idx].aabb_min, items[idx].aabb_max));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Interior: left child is at node_idx + 1, right at node.right_or_first.
|
||||
uint32_t left_idx = node_idx + 1;
|
||||
uint32_t right_idx = node.right_or_first;
|
||||
REQUIRE(left_idx < mbvh.nodes.size());
|
||||
REQUIRE(right_idx < mbvh.nodes.size());
|
||||
REQUIRE(left_idx != right_idx);
|
||||
|
||||
const BvhNode& l = mbvh.nodes[left_idx];
|
||||
const BvhNode& r = mbvh.nodes[right_idx];
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max, l.aabb_min, l.aabb_max));
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max, r.aabb_min, r.aabb_max));
|
||||
REQUIRE(node.axis < 3);
|
||||
|
||||
verifyNode(mbvh, items, left_idx);
|
||||
verifyNode(mbvh, items, right_idx);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("BvhNode is 32 bytes (sidecar/cache layout invariant)", "[bvh]") {
|
||||
REQUIRE(sizeof(BvhNode) == 32);
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne on empty input produces no nodes", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
ModelBvh mbvh = buildModelBvhOne(items, /*model_id=*/42);
|
||||
REQUIRE(mbvh.model_id == 42);
|
||||
REQUIRE(mbvh.nodes.empty());
|
||||
REQUIRE(mbvh.item_indices.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne with <= BVH_MAX_LEAF_SIZE items yields a single leaf", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f));
|
||||
}
|
||||
ModelBvh mbvh = buildModelBvhOne(items, 1);
|
||||
REQUIRE(mbvh.nodes.size() == 1);
|
||||
REQUIRE(mbvh.nodes[0].count == 5);
|
||||
REQUIRE(mbvh.item_indices.size() == 5);
|
||||
verifyNode(mbvh, items, 0);
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne with many items splits and respects invariants", "[bvh]") {
|
||||
std::mt19937 rng(0xC0FFEE);
|
||||
std::uniform_real_distribution<float> coord(-100.0f, 100.0f);
|
||||
std::uniform_real_distribution<float> radius(0.1f, 1.0f);
|
||||
|
||||
constexpr int N = 256;
|
||||
std::vector<BvhItem> items;
|
||||
items.reserve(N);
|
||||
for (int i = 0; i < N; ++i) {
|
||||
items.push_back(makeItem(coord(rng), coord(rng), coord(rng), radius(rng)));
|
||||
}
|
||||
|
||||
ModelBvh mbvh = buildModelBvhOne(items, /*model_id=*/7);
|
||||
REQUIRE(mbvh.model_id == 7);
|
||||
REQUIRE(!mbvh.nodes.empty());
|
||||
REQUIRE(mbvh.item_indices.size() == N);
|
||||
|
||||
// Permutation invariant: each item must appear exactly once.
|
||||
std::vector<int> seen(N, 0);
|
||||
for (uint32_t idx : mbvh.item_indices) {
|
||||
REQUIRE(idx < uint32_t(N));
|
||||
seen[idx]++;
|
||||
}
|
||||
for (int s : seen) REQUIRE(s == 1);
|
||||
|
||||
// Recursive structural invariants.
|
||||
verifyNode(mbvh, items, 0);
|
||||
|
||||
// Sum of leaf counts must equal item count.
|
||||
uint32_t leaf_total = 0;
|
||||
for (const auto& n : mbvh.nodes) {
|
||||
if (n.count > 0) leaf_total += n.count;
|
||||
}
|
||||
REQUIRE(leaf_total == N);
|
||||
}
|
||||
|
||||
TEST_CASE("buildBvhSet partitions by model_id and gates on BVH_MIN_OBJECTS", "[bvh]") {
|
||||
// Model 1: well above BVH_MIN_OBJECTS — should get a BVH.
|
||||
// Model 2: a single item — below the gate, must be skipped.
|
||||
std::vector<BvhItem> items;
|
||||
for (uint32_t i = 0; i < BVH_MIN_OBJECTS + 4; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f, /*model_id=*/1));
|
||||
}
|
||||
items.push_back(makeItem(0.0f, 0.0f, 0.0f, 0.5f, /*model_id=*/2));
|
||||
|
||||
auto set = buildBvhSet(items);
|
||||
REQUIRE(set);
|
||||
REQUIRE(set->bvh_model_ids.count(1) == 1);
|
||||
REQUIRE(set->bvh_model_ids.count(2) == 0);
|
||||
REQUIRE(set->models.count(1) == 1);
|
||||
REQUIRE(set->models.count(2) == 0);
|
||||
|
||||
const auto& mbvh = set->models.at(1);
|
||||
REQUIRE(mbvh.model_id == 1);
|
||||
REQUIRE(mbvh.item_indices.size() == BVH_MIN_OBJECTS + 4);
|
||||
// item_indices reference positions in the *full* items array — the model-1
|
||||
// items are at indices [0, BVH_MIN_OBJECTS + 4), so every entry must be
|
||||
// less than that.
|
||||
for (uint32_t idx : mbvh.item_indices) {
|
||||
REQUIRE(idx < BVH_MIN_OBJECTS + 4);
|
||||
}
|
||||
verifyNode(mbvh, items, 0);
|
||||
}
|
||||
|
||||
TEST_CASE("buildBvhSet returns empty set when nothing meets the gate", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
for (uint32_t i = 0; i < BVH_MIN_OBJECTS - 1; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f));
|
||||
}
|
||||
auto set = buildBvhSet(items);
|
||||
REQUIRE(set);
|
||||
REQUIRE(set->bvh_model_ids.empty());
|
||||
REQUIRE(set->models.empty());
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of SelectionState — the viewport's multi-selection state
|
||||
// machine. The class also owns a GL flags SSBO, but every GL path guards on
|
||||
// a context that initializeGl() has wired up; the test never calls
|
||||
// initializeGl(), so the CPU-side selection set / active-id logic runs in
|
||||
// full isolation (gl_ stays null and bindForRender is simply not exercised).
|
||||
|
||||
#include "Selection.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
namespace {
|
||||
|
||||
// Catch2 owns main(), so QCoreApplication can't live in a TU constructor.
|
||||
// Lazily construct it (intentionally leaked) the first time any test asks.
|
||||
void ensureQApp() {
|
||||
if (QCoreApplication::instance()) return;
|
||||
static int argc = 1;
|
||||
static char arg0[] = "test_selection";
|
||||
static char* argv[] = {arg0, nullptr};
|
||||
new QCoreApplication(argc, argv);
|
||||
}
|
||||
|
||||
// First arg of the most recent changed(active_id) signal.
|
||||
uint32_t lastActive(QSignalSpy& spy) {
|
||||
REQUIRE(spy.count() > 0);
|
||||
return spy.takeLast().at(0).toUInt();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("SelectionState starts empty with no active id", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.size() == 0);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE_FALSE(sel.isSelected(1));
|
||||
}
|
||||
|
||||
TEST_CASE("setSelectedObjectId selects a single id and makes it active",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.setSelectedObjectId(5);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 1);
|
||||
REQUIRE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 5);
|
||||
REQUIRE(lastActive(spy) == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelectedObjectId(0) clears the selection", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelectedObjectId(5);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
sel.setSelectedObjectId(0);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE(lastActive(spy) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelection coerces active to 0 when it is not in the set",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
|
||||
sel.setSelection({1, 2, 3}, /*active=*/9); // 9 not in the set
|
||||
REQUIRE(sel.size() == 3);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
|
||||
sel.setSelection({1, 2, 3}, /*active=*/2); // 2 is in the set
|
||||
REQUIRE(sel.activeObjectId() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelection drops object_id 0 and no-ops on identical state",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.setSelection({0, 1, 2}, /*active=*/1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 2); // id 0 stripped
|
||||
REQUIRE_FALSE(sel.isSelected(0));
|
||||
REQUIRE(sel.activeObjectId() == 1);
|
||||
|
||||
// Same set + same active — no churn.
|
||||
sel.setSelection({1, 2}, /*active=*/1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("addToSelection adds ids, keeps active, ignores 0 and no-ops",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelectedObjectId(1);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
sel.addToSelection({2, 3});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 3);
|
||||
REQUIRE(sel.activeObjectId() == 1); // active unchanged by add
|
||||
REQUIRE(lastActive(spy) == 1);
|
||||
|
||||
// Nothing new -> no signal.
|
||||
sel.addToSelection({2});
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
// id 0 is never added.
|
||||
sel.addToSelection({0});
|
||||
REQUIRE(spy.count() == 0);
|
||||
REQUIRE_FALSE(sel.isSelected(0));
|
||||
REQUIRE(sel.size() == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("removeFromSelection clears active when the active id is removed",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelection({1, 2, 3}, /*active=*/2);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
// Removing a non-active id keeps the active id.
|
||||
sel.removeFromSelection({1});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 2);
|
||||
REQUIRE(sel.activeObjectId() == 2);
|
||||
|
||||
// Removing the active id drops the active.
|
||||
sel.removeFromSelection({2});
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE(sel.isSelected(3));
|
||||
|
||||
// Removing something absent -> no signal.
|
||||
sel.removeFromSelection({99});
|
||||
REQUIRE(spy.count() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("toggleInSelection adds-as-active then removes-and-clears-active",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.toggleInSelection(5); // add
|
||||
REQUIRE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 5); // last-toggled becomes active
|
||||
|
||||
sel.toggleInSelection(6); // add — active follows the click
|
||||
REQUIRE(sel.isSelected(6));
|
||||
REQUIRE(sel.activeObjectId() == 6);
|
||||
|
||||
sel.toggleInSelection(5); // remove non-active — active unchanged
|
||||
REQUIRE_FALSE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 6);
|
||||
|
||||
sel.toggleInSelection(6); // remove the active — active cleared
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
|
||||
REQUIRE(spy.count() == 4);
|
||||
|
||||
// id 0 is never toggled.
|
||||
spy.clear();
|
||||
sel.toggleInSelection(0);
|
||||
REQUIRE(spy.count() == 0);
|
||||
REQUIRE(sel.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("clearSelection empties the set; no-op when already empty",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.clearSelection(); // already empty
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
sel.setSelectedObjectId(1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
sel.clearSelection();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("reset clears state and emits only when state existed", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.reset(); // nothing to clear
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
sel.setSelectedObjectId(7);
|
||||
REQUIRE(spy.count() == 1);
|
||||
sel.reset();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of VisibilityState — the per-element hidden-set tracker.
|
||||
// It is a QObject (for the changed() signal) but touches no GL, so the test
|
||||
// exercises the full state machine directly and asserts the hot-path
|
||||
// isHidden() flag mirror stays consistent with the canonical hidden set.
|
||||
|
||||
#include "Visibility.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
namespace {
|
||||
|
||||
// Catch2 owns main(), so QCoreApplication can't live in a TU constructor.
|
||||
// Lazily construct it (intentionally leaked) the first time any test asks.
|
||||
void ensureQApp() {
|
||||
if (QCoreApplication::instance()) return;
|
||||
static int argc = 1;
|
||||
static char arg0[] = "test_visibility";
|
||||
static char* argv[] = {arg0, nullptr};
|
||||
new QCoreApplication(argc, argv);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("VisibilityState starts empty", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE(vis.size() == 0);
|
||||
REQUIRE_FALSE(vis.isHidden(0)); // 0 is the "no object" sentinel
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(1u << 20));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects unions ids, emits changed, and isHidden reflects it",
|
||||
"[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
|
||||
vis.hideObjects({1, 2, 3});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.size() == 3);
|
||||
REQUIRE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE_FALSE(vis.isHidden(4));
|
||||
|
||||
// Unioning in a fresh id signals once more and grows the set.
|
||||
vis.hideObjects({2, 4}); // 2 already hidden, 4 is new
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(vis.size() == 4);
|
||||
REQUIRE(vis.isHidden(4));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects ignores object_id 0 and is idempotent", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
|
||||
vis.hideObjects({0});
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE(spy.count() == 0); // nothing changed
|
||||
REQUIRE_FALSE(vis.isHidden(0));
|
||||
|
||||
vis.hideObjects({7});
|
||||
REQUIRE(spy.count() == 1);
|
||||
vis.hideObjects({7}); // already hidden — no-op
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("showObjects subtracts and emits only when something changes",
|
||||
"[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2, 3});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.showObjects({2});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE(vis.size() == 2);
|
||||
|
||||
// Showing an id that was never hidden changes nothing.
|
||||
vis.showObjects({99});
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("setHidden replaces the set wholesale and drops id 0", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.setHidden({3, 4, 0});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.size() == 2); // id 0 was stripped
|
||||
REQUIRE_FALSE(vis.isHidden(0));
|
||||
REQUIRE_FALSE(vis.isHidden(1)); // old members cleared
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE(vis.isHidden(4));
|
||||
|
||||
// Replacing with an identical set is a no-op.
|
||||
vis.setHidden({3, 4});
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("showAll clears the set; no-op when already empty", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.showAll();
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
|
||||
vis.showAll(); // already empty
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("reset clears state and emits only when state existed", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.reset(); // nothing to clear
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
vis.hideObjects({5});
|
||||
REQUIRE(spy.count() == 1);
|
||||
vis.reset();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE_FALSE(vis.isHidden(5));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects grows the flag mirror for large object ids", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
|
||||
// A high id forces the cpu_flags_ vector (used by the hot-path isHidden)
|
||||
// to resize. isHidden must report it correctly without going out of
|
||||
// bounds, and neighbouring ids must stay visible.
|
||||
const uint32_t big = 1'000'000;
|
||||
vis.hideObjects({big});
|
||||
REQUIRE(vis.isHidden(big));
|
||||
REQUIRE_FALSE(vis.isHidden(big - 1));
|
||||
REQUIRE_FALSE(vis.isHidden(big + 1));
|
||||
REQUIRE(vis.hiddenIds().count(big) == 1);
|
||||
|
||||
vis.noteObjectId(big * 2); // pre-grow only — id stays visible
|
||||
REQUIRE_FALSE(vis.isHidden(big * 2));
|
||||
}
|
||||
Reference in New Issue
Block a user