mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 10:06:47 +00:00
ifcviewer: section-plane cut tool on web — shared gizmo, true-face pick, drag/Del
Full section tool for the web viewport, with the gizmo + interaction shared with desktop from one codebase. - True-face surface pick. pickSurfaceAt had always ray-cast the instance AABB (to skip a depth readback), so cuts sat in front of the real surface. The pick fragment already computes the exact world_pos (it clips sections with it); now it OUTPUTS it to a 3rd pick MRT (RGBA32F) that every pick path renders, and pickSurfaceAt / pickSurfaceAtAsync read it back (decodeMappedPickPosition; ray-AABB kept only as a fallback). The web async pick chains id -> normal -> position spontaneous staging maps. - Web tool: LMB drops a cut at the picked surface (LMB drag still orbits), K toggles, Shift+K clears; oriented to the real MRT surface normal. Exports + a Section / Clear cuts toolbar pair. - Shared gizmo: lifted the section-gizmo renderer (SECTION_WGSL + thick-line AA + quad+arrow VBO + pack + screen-space hit-test) out of the Qt-coupled OverlayRenderer into a Qt-free SectionGizmoRenderer that ViewportCore::render draws for BOTH desktop and web (both already render via render()). One identical gizmo; OverlayRenderer's now-dead section code removed. Fixed 1 m size (matches the desktop constant). - Interaction (shared): hitTestSectionGizmo (SectionGizmoRenderer::hitTest) + beginSectionDrag / updateSectionDrag / endSectionDrag live in ViewportCore. Drag a gizmo arrow to slide the plane along its normal; Del/Backspace removes the most recent cut. Desktop's ViewportWindow dropped its duplicate hit-test / drag math + state and delegates to the core; web wires the same calls. Tests: sectionPlaneCount add/clear/cap (Catch2, 125); web smoke "click a surface cuts geometry, clear restores" exercises the shared gizmo + 3-MRT pick (11/11). Desktop object-pick / marquee unaffected; BonsaiViewer builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -427,7 +427,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);
|
||||
@@ -1038,11 +1039,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
|
||||
@@ -1138,7 +1139,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);
|
||||
@@ -1234,7 +1235,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-
|
||||
@@ -1242,7 +1243,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;
|
||||
@@ -1265,93 +1266,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).
|
||||
|
||||
@@ -1617,13 +1537,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;
|
||||
@@ -1662,9 +1578,8 @@ 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;
|
||||
}
|
||||
@@ -1863,9 +1778,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