mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Merge branch 'ifcviewer-wgpu' of https://github.com/IfcOpenShell/IfcOpenShell into ifcviewer-wgpu
This commit is contained in:
@@ -436,7 +436,8 @@ void ViewportWindow::encodeOverlaysInMainPass(WGPURenderPassEncoder pass,
|
||||
// — drawn inside the MSAA pass so depth-test correctly hides them
|
||||
// behind closer geometry. (Corner axis / marquee / labels run on the
|
||||
// resolved surface; see encodeOverlaysPostMain.)
|
||||
overlays_.encodeSectionGizmos(pass, frame, section_planes_);
|
||||
// NB: section-plane gizmos now draw from ViewportCore::render via the shared
|
||||
// SectionGizmoRenderer (desktop + web), so they are NOT drawn here.
|
||||
overlays_.encodeHighlightTriangles(pass, frame);
|
||||
overlays_.encodePivot(pass, frame, pivot_indicator_visible_);
|
||||
overlays_.encodeOverlayLines(pass, frame);
|
||||
@@ -558,17 +559,19 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, StreamingSidecar metada
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadMeshChunk /
|
||||
// uploadInstanceChunk / finalizeModel). Streamer pushes chunks; we stage
|
||||
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadStreamedMesh /
|
||||
// uploadStreamedInstance / finalizeModel). Streamer pushes transfer records; we stage
|
||||
// them into a SidecarData-shaped buffer and commit at finalize via the
|
||||
// same chunk planner the sidecar load uses.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// getOrCreateDirectStaging moved to ViewportCore (anon namespace) (#84-q).
|
||||
|
||||
void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { core_.uploadMeshChunk(chunk); }
|
||||
void ViewportWindow::uploadStreamedMesh(const StreamedMesh& mesh) { core_.uploadStreamedMesh(mesh); }
|
||||
|
||||
void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { core_.uploadInstanceChunk(chunk); }
|
||||
void ViewportWindow::uploadStreamedInstance(const StreamedInstance& instance_record) {
|
||||
core_.uploadStreamedInstance(instance_record);
|
||||
}
|
||||
|
||||
void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); }
|
||||
|
||||
@@ -1047,11 +1050,11 @@ bool ViewportWindow::meshLocalToGlobal(uint32_t object_id,
|
||||
double global_out[3]) const {
|
||||
// Find the instance via the per-model object_id_to_instance map.
|
||||
// Use the live map key (`mid`) — see pickMeshLocalAt comment about
|
||||
// stale InstanceCpu::model_id from sidecar writes.
|
||||
// stale InstanceInfo::model_id from sidecar writes.
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(object_id);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
const InstanceInfo& inst = m.instances[it->second];
|
||||
// CoordinateOperation · placement · local — gives the IFC's own
|
||||
// georeferenced world frame (ENH). Excludes FederatedFalseOrigin
|
||||
// and ModelTransformation, matching the GL meshLocalToGlobal
|
||||
@@ -1147,7 +1150,7 @@ void ViewportWindow::invertElementVisibility() {
|
||||
to_hide.reserve(1024);
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
for (const InstanceInfo& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (!visibility_.isHidden(inst.object_id)) {
|
||||
to_hide.push_back(inst.object_id);
|
||||
@@ -1243,7 +1246,7 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
total += v;
|
||||
if (!show_labels) continue;
|
||||
// O(1) instance lookup via object_id_to_instance, then read the
|
||||
// world AABB from the cached InstanceCpu directly — same data
|
||||
// world AABB from the cached InstanceInfo directly — same data
|
||||
// computeObjectAabb's linear scan would have produced for the
|
||||
// first matching instance. For label placement at the AABB
|
||||
// centre this is identical-looking; only the rare multi-
|
||||
@@ -1251,7 +1254,7 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(oid);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
const InstanceInfo& inst = m.instances[it->second];
|
||||
OverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = (inst.world_aabb_min[0] + inst.world_aabb_max[0]) * 0.5f;
|
||||
lbl.world_pos[1] = (inst.world_aabb_min[1] + inst.world_aabb_max[1]) * 0.5f;
|
||||
@@ -1274,93 +1277,12 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
overlays_.setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
// Project a world point to LOGICAL pixel coords (Qt's mouse-event units).
|
||||
// Returns false if behind the camera.
|
||||
static bool projectWorldToLogicalScreen(const Eigen::Matrix4f& vp,
|
||||
const Eigen::Vector3f& world,
|
||||
int win_w, int win_h,
|
||||
Eigen::Vector2f& out) {
|
||||
const Eigen::Vector4f clip = vp * Eigen::Vector4f(world.x(), world.y(), world.z(), 1.0f);
|
||||
if (clip.w() <= 0.0f) return false;
|
||||
const float invw = 1.0f / clip.w();
|
||||
out = Eigen::Vector2f(
|
||||
(clip.x() * invw * 0.5f + 0.5f) * float(win_w),
|
||||
(1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(win_h));
|
||||
return true;
|
||||
}
|
||||
// projectWorldToLogicalScreen moved to SectionGizmoRenderer (its only users,
|
||||
// the section hit-test + drag, now live in ViewportCore).
|
||||
|
||||
int ViewportWindow::hitTestSectionGizmo(int x, int y) const {
|
||||
if (section_planes_.empty()) return -1;
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
if (w <= 0 || h <= 0) return -1;
|
||||
Eigen::Matrix4f view, proj;
|
||||
core_.buildViewProj(view, proj);
|
||||
const Eigen::Matrix4f vp = proj * view;
|
||||
const float grab_px = 12.0f;
|
||||
int best = -1;
|
||||
float best_d2 = grab_px * grab_px;
|
||||
for (int i = 0; i < int(section_planes_.size()); ++i) {
|
||||
const SectionPlane& p = section_planes_[i];
|
||||
Eigen::Vector2f s_origin, s_tip;
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin,
|
||||
w, h, s_origin)) continue;
|
||||
// The gizmo's arrow extends along +n by exactly 1 m in world
|
||||
// space — OverlayRenderer::encodeSectionGizmos uses
|
||||
// half_size = 1.0 to scale a plane-local arrow tip at z = 1.
|
||||
// Mirror that here.
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin + p.n * 1.0f,
|
||||
w, h, s_tip)) continue;
|
||||
const Eigen::Vector2f q{float(x), float(y)};
|
||||
const Eigen::Vector2f ab = s_tip - s_origin;
|
||||
const float ab_len2 = ab.squaredNorm();
|
||||
if (ab_len2 < 1e-3f) continue;
|
||||
float t = (q - s_origin).dot(ab) / ab_len2;
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
const Eigen::Vector2f proj_pt = s_origin + ab * t;
|
||||
const float d2 = (q - proj_pt).squaredNorm();
|
||||
if (d2 < best_d2) { best_d2 = d2; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void ViewportWindow::updateSectionDrag(int x, int y) {
|
||||
if (!section_drag_active_) return;
|
||||
if (section_drag_index_ < 0
|
||||
|| section_drag_index_ >= int(section_planes_.size())) return;
|
||||
SectionPlane& p = section_planes_[section_drag_index_];
|
||||
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
Eigen::Matrix4f view, proj;
|
||||
core_.buildViewProj(view, proj);
|
||||
const Eigen::Matrix4f vp = proj * view;
|
||||
|
||||
// Re-project the press-time origin and origin + n to screen space.
|
||||
// The press-time origin is what `start` should be relative to — so the
|
||||
// plane slides smoothly even as the camera moves (we re-project every
|
||||
// frame to handle mid-drag camera rotation cleanly).
|
||||
Eigen::Vector2f s_origin, s_n;
|
||||
if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_,
|
||||
w, h, s_origin)) return;
|
||||
if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_ + p.n,
|
||||
w, h, s_n)) return;
|
||||
const Eigen::Vector2f screen_axis = s_n - s_origin;
|
||||
const float screen_axis_len2 = screen_axis.squaredNorm();
|
||||
if (screen_axis_len2 < 1e-3f) return; // arrow is edge-on
|
||||
|
||||
// Project pixel delta onto the screen-space axis; convert to metres
|
||||
// via (delta · axis) / |axis|² (axis is 1 m long in world space).
|
||||
const Eigen::Vector2f delta_px(float(x - section_drag_start_mouse_.x()),
|
||||
float(y - section_drag_start_mouse_.y()));
|
||||
const float meters = delta_px.dot(screen_axis)
|
||||
/ screen_axis_len2;
|
||||
|
||||
p.origin = section_drag_start_origin_ + p.n * meters;
|
||||
p.d = -p.n.dot(p.origin);
|
||||
requestUpdate();
|
||||
}
|
||||
// Section-gizmo hit-test + drag-to-move now live in ViewportCore (shared with
|
||||
// web, using SectionGizmoRenderer::hitTest). The mouse handlers call
|
||||
// core_.hitTestSectionGizmo / beginSectionDrag / updateSectionDrag / endSectionDrag.
|
||||
|
||||
// buildHizPipeline moved to ViewportCore (#84-r).
|
||||
|
||||
@@ -1542,21 +1464,32 @@ void ViewportWindow::fpsIntegrate() {
|
||||
|
||||
// chunkScreenAreaPx moved to ViewportCore (#84-h).
|
||||
|
||||
void ViewportWindow::applyNavPreset(const char* name) {
|
||||
// Matches GL AppSettings::NavPreset semantics exactly.
|
||||
// blender — Orbit MMB, Pan Shift+MMB (default)
|
||||
// rhino — Orbit RMB, Pan Shift+RMB
|
||||
// revit — Orbit Shift+MMB, Pan MMB
|
||||
if (name && std::strcmp(name, "rhino") == 0) {
|
||||
orbit_button_ = Qt::RightButton; orbit_mods_ = Qt::NoModifier;
|
||||
pan_button_ = Qt::RightButton; pan_mods_ = Qt::ShiftModifier;
|
||||
} else if (name && std::strcmp(name, "revit") == 0) {
|
||||
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::ShiftModifier;
|
||||
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::NoModifier;
|
||||
} else {
|
||||
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::NoModifier;
|
||||
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::ShiftModifier;
|
||||
static Qt::MouseButton toQtBtn(ViewportCore::MouseBtn b) {
|
||||
switch (b) {
|
||||
case ViewportCore::MouseBtn::Left: return Qt::LeftButton;
|
||||
case ViewportCore::MouseBtn::Middle: return Qt::MiddleButton;
|
||||
case ViewportCore::MouseBtn::Right: return Qt::RightButton;
|
||||
}
|
||||
return Qt::LeftButton;
|
||||
}
|
||||
static Qt::KeyboardModifiers toQtMod(ViewportCore::NavMod m) {
|
||||
switch (m) {
|
||||
case ViewportCore::NavMod::Plain: return Qt::NoModifier;
|
||||
case ViewportCore::NavMod::Shift: return Qt::ShiftModifier;
|
||||
case ViewportCore::NavMod::Ctrl: return Qt::ControlModifier;
|
||||
case ViewportCore::NavMod::Alt: return Qt::AltModifier;
|
||||
}
|
||||
return Qt::NoModifier;
|
||||
}
|
||||
|
||||
void ViewportWindow::applyNavPreset(const char* name) {
|
||||
// The preset table lives in ViewportCore (shared with web). Map its bindings
|
||||
// to the Qt types the mouse handlers compare against.
|
||||
core_.setNavPreset(name);
|
||||
const auto& b = core_.navBindings();
|
||||
orbit_button_ = toQtBtn(b.orbit); orbit_mods_ = toQtMod(b.orbit_mod);
|
||||
pan_button_ = toQtBtn(b.pan); pan_mods_ = toQtMod(b.pan_mod);
|
||||
select_button_ = toQtBtn(b.select); select_mods_ = toQtMod(b.select_mod);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1615,13 +1548,9 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
&& event->button() == Qt::LeftButton
|
||||
&& event->modifiers() == Qt::NoModifier) {
|
||||
const Eigen::Vector2i lp = toV2i(event->position().toPoint());
|
||||
const int hit = hitTestSectionGizmo(lp.x(), lp.y());
|
||||
if (hit >= 0) {
|
||||
section_drag_active_ = true;
|
||||
section_drag_index_ = hit;
|
||||
section_drag_start_mouse_ = lp;
|
||||
section_drag_start_origin_ = section_planes_[hit].origin;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
const int hit = core_.hitTestSectionGizmo(lp.x(), lp.y());
|
||||
if (hit >= 0 && core_.beginSectionDrag(hit, lp.x(), lp.y())) {
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
Log::info().noquote().nospace()
|
||||
<< "[wgpu section] drag start: plane=" << hit;
|
||||
return;
|
||||
@@ -1642,15 +1571,15 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
&& (mods & Qt::KeyboardModifierMask) == pan_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Pan;
|
||||
setPivotIndicatorVisible(true);
|
||||
} else if (event->button() == Qt::LeftButton
|
||||
} else if (event->button() == select_button_
|
||||
&& !section_tool_active_
|
||||
&& tool_mode_ != ToolMode::Area
|
||||
&& tool_mode_ != ToolMode::Length
|
||||
&& nav_drag_kind_ == NavDrag::Inactive) {
|
||||
// Arm marquee box-select. Plain / Shift / Ctrl LMB without a tool
|
||||
// intercepting the click; if the cursor never moves past the
|
||||
// threshold this stays armed-only and the release falls through
|
||||
// to single-pick.
|
||||
// Arm marquee box-select. Plain / Shift / Ctrl on the select button
|
||||
// (Shift/Ctrl = add/remove) without a tool intercepting the click; if
|
||||
// the cursor never moves past the threshold this stays armed-only and
|
||||
// the release falls through to single-pick.
|
||||
box_select_armed_ = true;
|
||||
box_select_active_ = false;
|
||||
box_select_start_pos_ = nav_press_pos_;
|
||||
@@ -1660,16 +1589,15 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
}
|
||||
|
||||
void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
if (section_drag_active_ && event->button() == Qt::LeftButton) {
|
||||
section_drag_active_ = false;
|
||||
section_drag_index_ = -1;
|
||||
if (core_.sectionDragActive() && event->button() == Qt::LeftButton) {
|
||||
core_.endSectionDrag();
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
return;
|
||||
}
|
||||
// Marquee finalisation: only commit when the drag actually became
|
||||
// active (cursor moved past threshold). Press-time mods decide the
|
||||
// set op so a mid-drag Shift release doesn't flip the behaviour.
|
||||
if (box_select_armed_ && event->button() == Qt::LeftButton) {
|
||||
if (box_select_armed_ && event->button() == select_button_) {
|
||||
const bool was_active = box_select_active_;
|
||||
box_select_armed_ = false;
|
||||
box_select_active_ = false;
|
||||
@@ -1711,7 +1639,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
// LMB-click without drag → pick the object under the cursor and
|
||||
// route through the selection state. Shift = add, Ctrl = remove,
|
||||
// no modifier = replace. Empty-space click clears.
|
||||
if (event->button() == Qt::LeftButton && !nav_dragged_) {
|
||||
if (event->button() == select_button_ && !nav_dragged_) {
|
||||
const Eigen::Vector2i pos = toV2i(event->position().toPoint());
|
||||
const int px = int(pos.x() * devicePixelRatio());
|
||||
const int py = int(pos.y() * devicePixelRatio());
|
||||
@@ -1861,9 +1789,9 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
// Section drag intercepts the move handler entirely: the orbit/pan
|
||||
// classification already declined this drag in mousePressEvent, so all
|
||||
// we have to do is slide the plane along its normal.
|
||||
if (section_drag_active_) {
|
||||
if (core_.sectionDragActive()) {
|
||||
const Eigen::Vector2i pos = toV2i(event->position().toPoint());
|
||||
updateSectionDrag(pos.x(), pos.y());
|
||||
core_.updateSectionDrag(pos.x(), pos.y());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user