diff --git a/src/ifcviewer/CMakeLists.txt b/src/ifcviewer/CMakeLists.txt
index 9d7bbffd90..ec79fe1740 100644
--- a/src/ifcviewer/CMakeLists.txt
+++ b/src/ifcviewer/CMakeLists.txt
@@ -157,6 +157,7 @@ set(IFCVIEWER_CORE_HEADERS
LodBuilder.h
Stopwatch.h
ModelGpuData.h
+ SectionPlane.h
SelectionState.h
SidecarCache.h
StreamingLoader.h
diff --git a/src/ifcviewer/OverlayRenderer.h b/src/ifcviewer/OverlayRenderer.h
index cb9e94f73f..6963dfa2f3 100644
--- a/src/ifcviewer/OverlayRenderer.h
+++ b/src/ifcviewer/OverlayRenderer.h
@@ -45,17 +45,7 @@ struct OverlayFrame {
int device_pixel_ratio = 1;
};
-// One section plane as the visualizer consumes it. The viewport owns the
-// authoritative state vector (the section tool mutates it); the overlay
-// reads from a non-owning span every frame. Held by value because the
-// struct is small and copies happen at most six times per frame.
-struct SectionPlane {
- Eigen::Vector3f n = Eigen::Vector3f::UnitZ(); // unit normal
- float d = 0.0f; // -dot(n, origin)
- Eigen::Vector3f origin = Eigen::Vector3f::Zero(); // surface point at the
- // moment the plane was added
- float visual_radius = 0.0f;
-};
+#include "SectionPlane.h"
// All viewport overlays in one place: axis indicator (corner + pivot),
// section plane gizmos, and the marquee drag rect. Mirrors GL's
diff --git a/src/ifcviewer/SectionPlane.h b/src/ifcviewer/SectionPlane.h
new file mode 100644
index 0000000000..c37efc9461
--- /dev/null
+++ b/src/ifcviewer/SectionPlane.h
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * *
+ * 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 . *
+ * *
+ ********************************************************************************/
+
+#ifndef IFCVIEWER_SECTIONPLANE_H
+#define IFCVIEWER_SECTIONPLANE_H
+
+#include
+
+// One section plane as the renderer + overlay visualiser consume it. Lives
+// in its own Qt-free header so ViewportCore can include it without
+// dragging in OverlayRenderer's QString / QHash dependencies.
+//
+// The viewport owns the authoritative state vector (the section tool
+// mutates it; the per-frame uniform packs it for the WGSL shader; the
+// overlay reads from a non-owning span every frame). Held by value
+// because the struct is small and copies happen at most six times per
+// frame (kMaxSectionPlanes).
+struct SectionPlane {
+ Eigen::Vector3f n = Eigen::Vector3f::UnitZ(); // unit normal
+ float d = 0.0f; // -dot(n, origin)
+ Eigen::Vector3f origin = Eigen::Vector3f::Zero(); // surface point at the
+ // moment the plane was added
+ float visual_radius = 0.0f;
+};
+
+#endif
diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp
index b6f1763e65..6d62f365f0 100644
--- a/src/ifcviewer/ViewportCore.cpp
+++ b/src/ifcviewer/ViewportCore.cpp
@@ -1121,6 +1121,44 @@ void ViewportCore::uploadSelectionFlagsIfDirty() {
selection_.markClean();
}
+void ViewportCore::updateFrameUniforms() {
+ Eigen::Matrix4f view, proj;
+ buildViewProj(view, proj);
+
+ const Eigen::Matrix4f view_proj = proj * view;
+
+ FrameUniforms u = {};
+ std::memcpy(u.view_proj, view_proj.data(), 16 * sizeof(float));
+
+ // Values match the GL viewport's main fragment shader so a side-by-side
+ // diff of the two backends only shows what the wgpu pipeline has yet to
+ // implement (edge silhouette pass, MSAA polish, etc.) — not lighting
+ // model differences. Key + fill are ~unit-length, ~120° apart.
+ Eigen::Vector3f L( 0.3f, 0.5f, 0.8f); L.normalize();
+ Eigen::Vector3f F(-0.3f, -0.5f, 0.8f); F.normalize();
+ u.light_dir[0] = L.x(); u.light_dir[1] = L.y(); u.light_dir[2] = L.z(); u.light_dir[3] = 0;
+ u.fill_dir [0] = F.x(); u.fill_dir [1] = F.y(); u.fill_dir [2] = F.z(); u.fill_dir [3] = 0;
+ u.sky_color [0] = 0.55f; u.sky_color [1] = 0.60f; u.sky_color [2] = 0.70f;
+ u.ground_color[0] = 0.35f; u.ground_color[1] = 0.32f; u.ground_color[2] = 0.28f;
+
+ // Pack active section planes. `is_section_clipped` (WGSL) reads
+ // u.clip_count and u.clip_planes[0..clip_count) and discards
+ // fragments on the positive side.
+ const int n = std::min(int(section_planes_.size()), kMaxSectionPlanes);
+ u.clip_count = n;
+ for (int i = 0; i < n; ++i) {
+ const SectionPlane& p = section_planes_[i];
+ u.clip_planes[i][0] = p.n.x();
+ u.clip_planes[i][1] = p.n.y();
+ u.clip_planes[i][2] = p.n.z();
+ u.clip_planes[i][3] = p.d;
+ }
+ u.xray_alpha_cap = xray_alpha_cap_;
+ u._pad_xray[0] = u._pad_xray[1] = u._pad_xray[2] = 0.0f;
+
+ wgpuQueueWriteBuffer(queue_, frame_uniform_buffer_, 0, &u, sizeof(u));
+}
+
// ===========================================================================
// Lifecycle (#84-l): initWgpu + probeAndCreatePool + shutdown
// ===========================================================================
diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h
index 5428c58691..7dd8bf642f 100644
--- a/src/ifcviewer/ViewportCore.h
+++ b/src/ifcviewer/ViewportCore.h
@@ -47,6 +47,7 @@
#include "InstanceCompose.h"
#include "InstancedGeometry.h"
#include "ModelGpuData.h"
+#include "SectionPlane.h"
#include "SelectionState.h"
#include "StreamingThread.h"
#include "ViewportHost.h"
@@ -223,6 +224,12 @@ public:
void ensureSelectionFlagsBuffer();
void uploadSelectionFlagsIfDirty();
+ // Build the FrameUniforms struct (view-proj + lighting + section
+ // planes + xray cap) from current camera + section_planes_ +
+ // xray_alpha_cap_ and upload it via the queue. Called once per
+ // render() at frame start, before any draw encode.
+ void updateFrameUniforms();
+
// ---- wgpu lifecycle ----------------------------------------------------
//
// initWgpu brings up the wgpu instance, gets the platform surface
@@ -320,6 +327,20 @@ private:
uint32_t selection_flags_capacity_ = 0; // u32 entries
std::vector selection_flags_scratch_;
+ // Active world-space section planes (up to kMaxSectionPlanes); packed
+ // into the per-frame uniform every render and consumed by the WGSL
+ // is_section_clipped fragment gate. The section tool in
+ // ViewportWindow mutates this through addSectionPlaneAtSurface /
+ // removeSectionPlane (still Qt-bound — they wire into the input
+ // path). Reading happens here.
+ std::vector section_planes_;
+
+ // X-ray mode alpha clamp: when < 1.0 every instance routes through
+ // the transparent pass with fragment.a clamped to min(in.color.a, cap).
+ // Toggled by ViewportWindow::toggleXray; consumed by cull
+ // (transparent-pass classifier) and updateFrameUniforms.
+ float xray_alpha_cap_ = 1.0f;
+
// Selection + per-element visibility state machines. Pure CPU
// bookkeeping today (no GPU touch beyond the readback uploaded via
// selection_flags_buffer_). Mutated on the main thread between
diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp
index 0b107ad114..23eca3d5bd 100644
--- a/src/ifcviewer/ViewportWindow.cpp
+++ b/src/ifcviewer/ViewportWindow.cpp
@@ -271,6 +271,8 @@ ViewportWindow::ViewportWindow(QWindow* parent)
selection_flags_buffer_ (core_.selection_flags_buffer_),
selection_flags_capacity_(core_.selection_flags_capacity_),
selection_flags_scratch_ (core_.selection_flags_scratch_),
+ section_planes_ (core_.section_planes_),
+ xray_alpha_cap_ (core_.xray_alpha_cap_),
selection_ (core_.selection_),
visibility_ (core_.visibility_) {
// wgpu doesn't need a GL context; we just need a real native window
@@ -3941,7 +3943,7 @@ void ViewportWindow::render() {
WGPUTextureView view = wgpuTextureCreateView(surf_tex.texture, nullptr);
- updateFrameUniforms();
+ core_.updateFrameUniforms();
// Per-frame cull: extract frustum planes from the same VP we just wrote
// into the uniform, then run cullModelCpu on every visible model. The
@@ -5817,43 +5819,7 @@ static Eigen::Vector3f orbitEye(const float target[3], float dist,
// identically everywhere.
// buildViewProj moved to ViewportCore (#84-h).
-void ViewportWindow::updateFrameUniforms() {
- Eigen::Matrix4f view, proj;
- core_.buildViewProj(view, proj);
-
- const Eigen::Matrix4f view_proj = proj * view;
-
- FrameUniforms u = {};
- std::memcpy(u.view_proj, view_proj.data(), 16 * sizeof(float));
-
- // Values match the GL viewport's main fragment shader so a side-by-side
- // diff of the two backends only shows what the wgpu pipeline has yet to
- // implement (edge silhouette pass, MSAA polish, etc.) — not lighting
- // model differences. Key + fill are ~unit-length, ~120° apart.
- Eigen::Vector3f L( 0.3f, 0.5f, 0.8f); L.normalize();
- Eigen::Vector3f F(-0.3f, -0.5f, 0.8f); F.normalize();
- u.light_dir[0] = L.x(); u.light_dir[1] = L.y(); u.light_dir[2] = L.z(); u.light_dir[3] = 0;
- u.fill_dir [0] = F.x(); u.fill_dir [1] = F.y(); u.fill_dir [2] = F.z(); u.fill_dir [3] = 0;
- u.sky_color [0] = 0.55f; u.sky_color [1] = 0.60f; u.sky_color [2] = 0.70f;
- u.ground_color[0] = 0.35f; u.ground_color[1] = 0.32f; u.ground_color[2] = 0.28f;
-
- // Pack active section planes. `is_section_clipped` (WGSL) reads
- // u.clip_count and u.clip_planes[0..clip_count) and discards
- // fragments on the positive side.
- const int n = std::min(int(section_planes_.size()), kMaxSectionPlanes);
- u.clip_count = n;
- for (int i = 0; i < n; ++i) {
- const SectionPlane& p = section_planes_[i];
- u.clip_planes[i][0] = p.n.x();
- u.clip_planes[i][1] = p.n.y();
- u.clip_planes[i][2] = p.n.z();
- u.clip_planes[i][3] = p.d;
- }
- u.xray_alpha_cap = xray_alpha_cap_;
- u._pad_xray[0] = u._pad_xray[1] = u._pad_xray[2] = 0.0f;
-
- wgpuQueueWriteBuffer(queue_, frame_uniform_buffer_, 0, &u, sizeof(u));
-}
+// updateFrameUniforms moved to ViewportCore (#84-m).
// computeSceneAabb moved to ViewportCore (#84-h).
diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h
index efbb3223cb..a431ad4ceb 100644
--- a/src/ifcviewer/ViewportWindow.h
+++ b/src/ifcviewer/ViewportWindow.h
@@ -557,7 +557,6 @@ private:
// any projection is unreliable. True (cull) when AABB is provably
// behind every relevant pyramid cell.
bool aabbOccludedByHiz(const float mn[3], const float mx[3]) const;
- void updateFrameUniforms();
void flushPendingSidecarQueue();
// computeSceneAabb moved to ViewportCore (#84-h).
@@ -777,20 +776,22 @@ private:
int pick_w_ = 0;
int pick_h_ = 0;
- // Section-cutting state. SectionPlane lives in OverlayRenderer.h
- // because the visualiser reads it; the viewport owns the authoritative
- // vector that the section tool mutates.
- std::vector section_planes_;
+ // Section-cutting state aliases (storage in core_). The section tool
+ // mutates section_planes_ through addSectionPlaneAtSurface /
+ // removeSectionPlane; the per-frame uniform packs the same vector
+ // for the WGSL fragment-side clip gate.
+ std::vector& section_planes_;
bool section_tool_active_ = false;
- // X-ray mode. Default 1.0 = no effect (fragment shader clamps
- // alpha = min(in.color.a, xray_alpha_cap_), which returns in.color.a
- // when the cap is 1). Alt+X drops it to 0.3 to translucent the whole
- // scene; pressing again restores 1.0. When < 1.0, the cull
- // classifier also routes every instance into the transparent pass
- // so the blend stage actually fires (an opaque-pass fragment with
- // capped alpha would still overwrite the back buffer).
- float xray_alpha_cap_ = 1.0f;
+ // X-ray cap alias (storage in core_). Default 1.0 = no effect
+ // (fragment shader clamps alpha = min(in.color.a, xray_alpha_cap_),
+ // which returns in.color.a when the cap is 1). Alt+X drops it to
+ // 0.3 to translucent the whole scene; pressing again restores 1.0.
+ // When < 1.0, the cull classifier (also in core) routes every
+ // instance into the transparent pass so the blend stage actually
+ // fires (an opaque-pass fragment with capped alpha would still
+ // overwrite the back buffer).
+ float& xray_alpha_cap_;
// Marquee box-select. Armed on LMB press (when no other tool consumes
// the click), becomes active after the cursor moves past