Last round of straight-swap Qt value types in ViewportWindow + its
overlay co-pilot.
setBackgroundColor(const QColor&) → (float r, float g, float b, float a)
QColor background_color_ → Eigen::Vector4f (linear, 0..1)
QPoint {nav_,box_select_,fps_, } → Eigen::Vector2i
{section_drag_start_mouse_}
QSet<int> fps_keys_held_ → std::unordered_set<int>
QElapsedTimer fps_last_tick_, → Stopwatch (new header in
fly_render_clock_, IfcViewerCore — std::chrono-
render_thread_local_ backed, exposes the existing
timers in render() QElapsedTimer .start/.restart/
.elapsed/.nsecsElapsed surface)
Also propagates the QPoint → Eigen::Vector2i change through
OverlayRenderer::encodeMarquee since the marquee corner coords flow
through that interface.
API-level helpers:
toV2i(QPoint) — small inline in ViewportWindow.cpp, isolates
the QMouseEvent→Vector2i conversion at the
five mouse-event handlers
Stopwatch.h — new file, IfcViewerCore. Same call shape as
QElapsedTimer; backed by std::chrono::steady_clock.
QSet method swaps:
.isEmpty() → .empty()
.contains(k) → .count(k) (C++17, no std contains() until C++20)
.remove(k) → .erase(k)
Eigen::Vector2i doesn't have .manhattanLength(); the box-select drag
threshold uses std::abs(diff.x()) + std::abs(diff.y()) inline.
Bonsai side: View.cpp's setBackgroundColor wrapper now decomposes the
QColor into floats at the call site (kept locally so the bonsai UI
keeps its QColor-driven theming).
Closes#81 + the QElapsedTimer half of #83. QTimer
(pivot_indicator_hide_timer_) still uses Qt — it needs the host's
scheduleOnce mechanism that lands with #85.
Builds: desktop / bonsai / web all green. Tests 100/100.
Replace Qt math wrappers with Eigen across ViewportWindow, OverlayRenderer,
Federation, and the bonsai-side viewport modules. Eigen was already the
canonical type for the actually-important matrix work (InstanceCompose,
ModelGpuData, federation matrices); QVector3D/QVector4D/QMatrix4x4 were
leftover from when Qt was the path of least resistance. They offered
nothing over Eigen for our use case beyond a few graphics helpers
(lookAt / perspective / ortho) which were 30 lines to write.
Substitutions:
QMatrix4x4 → Eigen::Matrix4f
QVector2D → Eigen::Vector2f
QVector3D → Eigen::Vector3f
QVector4D → Eigen::Vector4f
API rewrites:
.lengthSquared() → .squaredNorm()
.length() → .norm()
.isNull() → .isZero()
.setToIdentity() → .setIdentity()
.constData() → .data()
.toVector3D() → .head<3>()
.inverted(&ok) → tryInvert4f(M, out)
Q::dotProduct(a,b) → a.dot(b)
Q::crossProduct(a,b) → a.cross(b)
QMat4x4(... row-major) → Eigen::Map<const Matrix4f>(col-major buf)
QMat4x4().lookAt(...) → lookAtRH(eye, target, up)
QMat4x4().perspective(.) → perspectiveYFovGL(fovy, aspect, n, f)
QMat4x4().ortho(...) → orthoGL(l, r, b, t, n, f)
Default-init divergence handled explicitly (QMatrix4x4() = identity,
QVector3D() = zero; Eigen leaves both uninitialized). Public API
(CameraState, HomeView, ViewportWindow::computeObjectAabb, the
addSectionPlaneAtSurface / pickSurfaceAt / raycast signatures) follows
through to Eigen too; bonsai-side View.cpp and Commands.cpp updated to
match.
Camera helpers (lookAtRH, perspectiveYFovGL, orthoGL, tryInvert4f)
extracted to a new CameraMath.h so OverlayRenderer's gizmo MVP and
ViewportWindow's buildViewProj share the same definitions. Federation
drops its <QVector3D> include in favour of <Eigen/Dense> (already had
the latter for the georef matrices).
Builds: desktop IfcViewerMinimal ✓, BonsaiViewer ✓, web IfcViewerWeb ✓.
Tests: 100/100 pass. Closes#78 + #79; opens the door for #80-#83.
The GL backend is gone (task #53). The wgpu/non-wgpu folder split and
the Wgpu* class prefix were both disambiguation artefacts from the
overlap period — now pure dead weight.
## Folder + library merge
* `src/ifcviewer-wgpu/` → folded into `src/ifcviewer/` (git mv tracks
every file as a rename so blame/log history survives).
* `src/ifcviewer-wgpu-minimal/` → `src/ifcviewer-minimal/` (the exe was
already named `IfcViewerMinimal`; this just brings the folder + CMake
target name into line).
* `src/ifcviewer-wgpu/tests/test_wgpu_{selection,visibility}.cpp` →
`src/ifcviewer/tests/test_{selection,visibility}.cpp`, folded into
the existing `add_ifcviewer_unit_test(...)` helper.
* The `IfcViewerWgpu` static library is dissolved — its sources become
part of the unified `IfcViewer` static library, which now bundles
scene/loader + renderer in one target. The pre-merge circular
dependency (IfcViewer linking IfcViewerWgpu just to get the
ViewportWindow.h include path that SceneLoader.h needs) goes away.
* The wgpu-native FetchContent block, the Cocoa/QuartzCore link on
Apple, the OBJCXX-enabled `.mm` source, and the wgpu-native runtime
install all move into `src/ifcviewer/CMakeLists.txt` unchanged.
## Type renames (Wgpu prefix dropped from every Wgpu* identifier)
WgpuAreaMeasurement → AreaMeasurement
WgpuBufferPool → BufferPool
WgpuLengthMeasurement → LengthMeasurement
WgpuMetalSurface → MetalSurface
WgpuModelGpuData → ModelGpuData
WgpuOverlayFrame → OverlayFrame
WgpuOverlayRenderer → OverlayRenderer
WgpuSectionPlane → SectionPlane
WgpuSelectionState → SelectionState
WgpuStreamingLoader → StreamingLoader
WgpuStreamingThread → StreamingThread
WgpuViewportWindow → ViewportWindow
WgpuVisibilityState → VisibilityState
CMake target IfcViewerWgpuMinimal → IfcViewerMinimal (exe name was
already this since wgpu shipped as default).
Deliberately kept: `onWgpuLog` (wgpu-native log callback — names a
binding to an external API, not one of *our* types), and the WGPU*
enum/struct prefixes from wgpu-native's own headers. `WgpuMemProbe`
lives in the separate `src/wgpu-mem-probe/` standalone diagnostic
project and isn't touched.
## Include-path updates
Every `#include "../ifcviewer-wgpu/Wgpu<X>.h"` → `"../ifcviewer/<X>.h"`,
every in-directory `#include "Wgpu<X>.h"` → `"<X>.h"`. Includes from
sibling subdirectories (modules/, etc.) are updated to point at
`../../../ifcviewer/` instead of `../../../ifcviewer-wgpu/`.
## cmake/CMakeLists.txt simplification
The redundant `add_subdirectory(ifcviewer-wgpu)` blocks (one inside
the BUILD_BONSAIVIEWER fan-in, one in the BONSAIVIEWER-less standalone
block) collapse into a single unconditional
`add_subdirectory(../src/ifcviewer ifcviewer)`. The standalone block
keeps only `wgpu-mem-probe` (the diagnostic tool, unrelated to the
viewer lib).
## Verification
* Full build green: `IfcViewer` static lib, `IfcViewerMinimal` exe,
`BonsaiViewer` exe, all four pre-existing ifcviewer unit tests, and
the two new-location tests (`test_selection`, `test_visibility`).
* No stray `Wgpu<X>` identifier remains across `src/ifcviewer/`,
`src/bonsaiviewer/`, `src/ifcviewer-minimal/` (verified by grep).
* Renames tracked by git as `R` entries — `git log --follow` on
ViewportWindow.cpp etc. continues to show history through the move.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Use Qt's system fixed font for viewer overlay text instead of the generic monospace family, avoiding the Windows font-resolution delay seen during measurement overlays.
Generated with the assistance of an AI coding tool.
Update the overlay renderer's dynamic VBO uploads to bind the buffer and use glBufferData/glBufferSubData instead of direct-state glNamedBufferData/glNamedBufferSubData.
This avoids Windows/NVIDIA driver corruption seen with overlay axes, pick markers, HUD rects, and marquee rectangles while keeping the same overlay geometry and draw paths.
Generated with the assistance of an AI coding tool.
SelectionState (new) owns the multi-set, the "active" id (last single-
clicked), and a per-object_id flags SSBO bound at binding=3. Main
shader reads sel_flags[v_object_id] for the in-set tint and a separate
u_active_id uniform for a stronger tint on the active.
Click semantics: plain replaces, Shift/Ctrl toggles. LMB-drag past 5px
boxes the rect through a pick-pass readback — plain replaces, Shift
adds, Ctrl removes; box-select preserves the active. Drag promotes
regardless of start point so a press on geometry doesn't disqualify it.
Sidecar fast-path bulk-loads instances, so noteObjectId is also called
from the apply path — without it the flags buffer was sized to 1 slot
while object_ids were in the 100k+ range and the in-set bit was lost.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Length tool's 1-pt laser is now hybrid:
- On any surface, a coplanar BFS finds the connected face patch
around the click and projects its vertices into the surface
tangent basis to get an exact bounding-box extent. Stops at
the face edge by construction — no overshoot into adjacent
geometry like the previous tangent-raycast did.
- On near-horizontal surfaces (|n.z| > 0.85, i.e. floors and
ceilings) it additionally fires one raycast in +n to the
opposing surface — so a single floor click reports X extent +
Y extent + ceiling height.
- Bars are labelled by their dominant world axis (X/Y/Z) instead
of "vertical/horizontal", which reads cleanly on either kind
of surface.
The 2-pt readout now draws the world-space XYZ stair-step (red ΔX,
green ΔY, blue ΔZ) with each leg labelled, and a dashed
perpendicular line whenever the two picks landed on near-parallel
surfaces — useful for measuring across walls.
To support multiple line styles per frame, OverlayRenderer's
setOverlayLines takes std::vector<LineGroup> instead of a single
inline style; each group has its own color/halo/width and an
optional dash period. The line shader gained v_along_px +
u_dash_period uniforms (screen-space dashes), and both line and
point shaders now use a sharp step() for the inner→stroke
transition with AA only on the outer halo edge — much crisper than
the previous soft band. Default visual style trimmed: 1.5px lines
(0.5px halo), 6px dots (1px halo), opaque black halo.
Also adds ViewportWindow::raycast(origin, dir, RaycastHit&) — CPU
ray traversal of each model's per-instance BVH followed by
Möller-Trumbore against the candidate meshes' triangles (lazily
read back, cached per call). Used by the floor/ceiling laser path
today and reusable for any future raycast-based feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ViewportWindow trades the area_tool_active_ bool for an enum ToolMode
{None, Area, Length}; the existing surfacePickedInTool signal carries
both, the app dispatches on toolMode(). Esc exits any active tool;
Backspace/Delete in length mode emits toolBackspacePressed which the
length tool uses to remove the last point.
LengthMeasurement collects clicked world-space points and adapts the
readout: 2pt → distance + axis-aligned ΔX/ΔY/ΔZ, 3pt → angle at the
middle vertex + triangle area, 4+pt → best-fit-plane PCA + shoelace
when planar (RMS plane distance / bbox diag < 1e-3) else fan
triangulation, with the chosen method labelled in the readout. Per-
segment lengths float at each midpoint.
OverlayRenderer grows three new pipelines to support this:
- point sprite shader: gl_PointCoord-based outlined disc with
fwidth-smoothed inner/stroke bands, a single draw call.
- line shader: CPU-expand each segment to 6 verts carrying both
endpoints + (side, along) corner index; vertex shader computes
the screen-space perpendicular and offsets accordingly. Real
outlined lines independent of the driver's glLineWidth clamp.
- screen-space rect shader: HUD + label backgrounds drawn as raw
GL quads in NDC. QPainter::fillRect on QOpenGLPaintDevice was
silently dropping fills across drivers; bypassing it entirely
via this shader makes backgrounds reliable. Cull-face is also
explicitly disabled here — GL_TRIANGLES respects it but the
line/point primitives don't, so this was the one path needing
the fix.
setOverlayLines / setOverlayPoints take an inner color, an outline
color, and an extra-pixels-per-side stroke amount. Lines + points
draw with GL_ALWAYS so measurement annotations stay visible through
geometry; highlight tris stay depth-aware (GL_LEQUAL) so area
shading still tints the surface in place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New OverlayRenderer module owns every client-supplied overlay primitive
drawn after the main pass: tinted, depth-aware highlight triangles via
its own GL shader, and top-left HUD text via QPainter on a
QOpenGLPaintDevice. Public surface on ViewportWindow is just two
forwarders (setHighlightTriangles, setHudText).
ViewportWindow's MeshLocalPick now exposes the instance's composed
transform so consumers can map mesh-local geometry back to world
space without re-querying. AreaMeasurement uses both: its selection
key is now (object_id, tri) so per-instance highlighting works for
two distinct walls sharing a mesh, and on every pick it rebuilds the
world-space tri list and the HUD readout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>