mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +00:00
ifcviewer: visibility (hide/isolate/show-all) + X-ray — desktop parity on web
Lift the visibility + X-ray ops into ViewportCore so desktop and web share them. The cull already reads visibility_ (hidden objects skipped) and the frame uniform reads xray_alpha_cap_, both per frame, so each op just mutates state + schedules a frame — no GPU buffers to rebuild, no rendering work: - hideSelected (hide the selection, then deselect), isolateSelected (hide every non-selected object in a visible model), showAll (clear the hidden set), toggleXray (flip the alpha cap 1.0<->0.3; cull routes all to the transparent pass), xrayActive(). - Desktop ViewportWindow: the H / Shift+H / Alt+H / Alt+X keys and the menu-action wrappers now call the core; the four inline/duplicated implementations are gone. - Web main_web: same keys (H hide · Shift+H isolate · Alt+H show all · Alt+X x-ray) + toolbar buttons (Hide / Isolate / Show all / X-ray, the last reflecting active state). Verified on web: x-ray toggles and visibly translucent-izes the scene, hide after a pick removes geometry, 0 GPU errors. 113/113 desktop + 6/6 web smoke; desktop app builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ target_link_options(IfcViewerWeb PRIVATE
|
||||
# EMSCRIPTEN_KEEPALIVE alone keeps the symbols in the binary but doesn't
|
||||
# add them to Module. ccall lets shell.html pass a JS string (the ?model
|
||||
# URL) to load_sidecar_from_url_c without manual heap marshalling.
|
||||
"-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c']"
|
||||
"-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c','_hide_selected_c','_isolate_selected_c','_show_all_c','_toggle_xray_c','_xray_is_active_c']"
|
||||
# ccall: shell.html passes the ?model URL string to load_sidecar_from_url_c.
|
||||
# HEAPU8: lets tooling/tests read the wasm heap size (e.g. to verify a large
|
||||
# sidecar streams by range instead of loading whole). Standard, zero-cost.
|
||||
|
||||
@@ -227,6 +227,16 @@ EM_BOOL onKeyDown(int, const EmscriptenKeyboardEvent* e, void* user) {
|
||||
// While flying, WASDQE/Shift are held-movement keys, not hotkeys.
|
||||
if (app->fly_mode) { if (setFlyKey(app, code, true)) return EM_TRUE; return EM_FALSE; }
|
||||
if (e->repeat) return EM_FALSE;
|
||||
const bool alt = e->altKey;
|
||||
// Visibility + X-ray, matching desktop: H hide selected · Shift+H isolate ·
|
||||
// Alt+H show all · Alt+X x-ray.
|
||||
if (!std::strcmp(code, "KeyH")) {
|
||||
if (alt) app->core.showAll();
|
||||
else if (shift) app->core.isolateSelected();
|
||||
else app->core.hideSelected();
|
||||
return EM_TRUE;
|
||||
}
|
||||
if (!std::strcmp(code, "KeyX") && alt) { app->core.toggleXray(); return EM_TRUE; }
|
||||
using SV = ViewportCore::StandardView;
|
||||
if (!std::strcmp(code, "Home")) app->core.viewAll();
|
||||
else if (!std::strcmp(code, "KeyF") && !shift) app->core.frameSelection();
|
||||
@@ -356,6 +366,13 @@ extern "C" EMSCRIPTEN_KEEPALIVE int fly_is_active_c() {
|
||||
return (g_app && g_app->ready && g_app->fly_mode) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Visibility + X-ray, for the toolbar (same ops as the H/Shift+H/Alt+H/Alt+X keys).
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void hide_selected_c() { if (g_app && g_app->ready) g_app->core.hideSelected(); }
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void isolate_selected_c() { if (g_app && g_app->ready) g_app->core.isolateSelected(); }
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void show_all_c() { if (g_app && g_app->ready) g_app->core.showAll(); }
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void toggle_xray_c() { if (g_app && g_app->ready) g_app->core.toggleXray(); }
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int xray_is_active_c() { return (g_app && g_app->ready && g_app->core.xrayActive()) ? 1 : 0; }
|
||||
|
||||
// id: 0 Front, 1 Back, 2 Left, 3 Right, 4 Top, 5 Bottom.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void standard_view_c(int id) {
|
||||
if (!g_app || !g_app->ready || id < 0 || id > 5) return;
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
<button data-view="3" title="Right (Y)">Right</button>
|
||||
<button data-view="4" title="Top (Z)">Top</button>
|
||||
<button data-view="5" title="Bottom (Shift+Z)">Bottom</button>
|
||||
<span class="sep"></span>
|
||||
<button data-act="hide" title="Hide selected (H)">Hide</button>
|
||||
<button data-act="isolate" title="Isolate selected (Shift+H)">Isolate</button>
|
||||
<button data-act="showall" title="Show all (Alt+H)">Show all</button>
|
||||
<button data-act="xray" id="xray-btn" title="X-ray — translucent everything (Alt+X)">X-ray</button>
|
||||
</div>
|
||||
<div id="status">Starting…</div>
|
||||
<script>
|
||||
@@ -162,6 +167,10 @@
|
||||
var fb = document.getElementById('fly-btn');
|
||||
if (fb) fb.classList.toggle('active', !!Module._fly_is_active_c());
|
||||
}
|
||||
if (Module._xray_is_active_c) {
|
||||
var xb = document.getElementById('xray-btn');
|
||||
if (xb) xb.classList.toggle('active', !!Module._xray_is_active_c());
|
||||
}
|
||||
Module._raf_tick_c(Module._app_ptr);
|
||||
}
|
||||
requestAnimationFrame(shellTick);
|
||||
@@ -338,6 +347,10 @@
|
||||
else if (act === 'focus') Module._frame_selection_c();
|
||||
else if (act === 'ortho') { Module._toggle_projection_c(); refreshOrthoLabel(); }
|
||||
else if (act === 'fly') Module._toggle_fly_c();
|
||||
else if (act === 'hide') Module._hide_selected_c();
|
||||
else if (act === 'isolate') Module._isolate_selected_c();
|
||||
else if (act === 'showall') Module._show_all_c();
|
||||
else if (act === 'xray') Module._toggle_xray_c();
|
||||
else if (view !== null) Module._standard_view_c(parseInt(view, 10));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4996,6 +4996,49 @@ void ViewportCore::applyPickToSelection(std::uint32_t object_id, bool add, bool
|
||||
else selection_.replace(object_id);
|
||||
}
|
||||
|
||||
void ViewportCore::hideSelected() {
|
||||
if (selection_.count() == 0) return;
|
||||
for (uint32_t id : selection_.selectionIds()) visibility_.hide(id);
|
||||
const size_t n = selection_.count();
|
||||
selection_.clear(); // hiding deselects, matching the desktop/GL behaviour
|
||||
Log::info().noquote().nospace() << "[wgpu] hid " << n << " selected";
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::isolateSelected() {
|
||||
if (selection_.count() == 0) return;
|
||||
// Hide every object in a VISIBLE model that isn't selected. Model-hidden
|
||||
// objects stay model-hidden (element-level hiding on top is redundant), and
|
||||
// object_id 0 (unpickable) is skipped.
|
||||
const auto& sel_ids = selection_.selectionIds();
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (sel_ids.find(inst.object_id) == sel_ids.end())
|
||||
visibility_.hide(inst.object_id);
|
||||
}
|
||||
}
|
||||
Log::info().noquote().nospace() << "[wgpu] isolated " << selection_.count();
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::showAll() {
|
||||
if (visibility_.hiddenCount() == 0) return;
|
||||
visibility_.clear();
|
||||
Log::info() << "[wgpu] show all";
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::toggleXray() {
|
||||
constexpr float kXrayOnCap = 0.3f;
|
||||
xray_alpha_cap_ = (xray_alpha_cap_ < 1.0f) ? 1.0f : kXrayOnCap;
|
||||
Log::info().noquote().nospace()
|
||||
<< "[wgpu] x-ray " << (xray_alpha_cap_ < 1.0f ? "ON" : "OFF")
|
||||
<< " (cap=" << xray_alpha_cap_ << ")";
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
void ViewportCore::pickObjectAtAsync(int x_pixels, int y_pixels,
|
||||
std::function<void(std::uint32_t)> cb) {
|
||||
|
||||
@@ -624,6 +624,19 @@ public:
|
||||
// Marks selection_ dirty for the next render's flush.
|
||||
void applyPickToSelection(std::uint32_t object_id, bool add, bool remove);
|
||||
|
||||
// Visibility + X-ray, shared by desktop (H / Shift+H / Alt+H / Alt+X) and
|
||||
// web. Hidden objects are skipped by the cull and xray_alpha_cap_ is read
|
||||
// by the frame uniform, both per frame — so each call just mutates state and
|
||||
// schedules a frame; no GPU buffers to rebuild.
|
||||
void hideSelected(); // hide the selected objects, then clear selection
|
||||
void isolateSelected(); // hide everything that is NOT selected
|
||||
void showAll(); // clear the hidden set
|
||||
size_t hiddenCount() const { return visibility_.hiddenCount(); }
|
||||
// Global X-ray: translucent everything. Flips the frame uniform's alpha cap;
|
||||
// the cull classifier routes every instance through the transparent pass.
|
||||
void toggleXray();
|
||||
bool xrayActive() const { return xray_alpha_cap_ < 1.0f; }
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
// Async object pick for the web build: encodes the same pick pass as
|
||||
// pickObjectAt but reads the staging buffer back via a spontaneous map
|
||||
|
||||
@@ -1124,36 +1124,11 @@ void ViewportWindow::setSelectedObjectId(uint32_t id) {
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::hideSelectedElements() {
|
||||
if (selection_.count() == 0) return;
|
||||
for (uint32_t id : selection_.selectionIds()) visibility_.hide(id);
|
||||
selection_.clear();
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::isolateSelectedElements() {
|
||||
if (selection_.count() == 0) return;
|
||||
// Hide every object in a visible model that isn't in the selection.
|
||||
// Model-hidden objects stay model-hidden — element-level hiding on
|
||||
// top of that is redundant and just bloats hidden_ids_.
|
||||
const auto& sel_ids = selection_.selectionIds();
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (sel_ids.find(inst.object_id) == sel_ids.end()) {
|
||||
visibility_.hide(inst.object_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::showAllElements() {
|
||||
if (visibility_.hiddenCount() == 0) return;
|
||||
visibility_.clear();
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
// Visibility ops now live in ViewportCore (shared with web); these stay as thin
|
||||
// Qt-facing wrappers for the menu actions.
|
||||
void ViewportWindow::hideSelectedElements() { core_.hideSelected(); }
|
||||
void ViewportWindow::isolateSelectedElements() { core_.isolateSelected(); }
|
||||
void ViewportWindow::showAllElements() { core_.showAll(); }
|
||||
|
||||
void ViewportWindow::invertElementVisibility() {
|
||||
// Compute the new hidden set: every live object_id in a visible model
|
||||
@@ -1988,53 +1963,13 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
// Shift+H — isolate selected
|
||||
// Alt+H — show all (clear hidden set)
|
||||
// Shift+F — enter fly mode (Esc exits)
|
||||
if (key == Qt::Key_H && mods == Qt::AltModifier) {
|
||||
if (visibility_.hiddenCount() == 0) return;
|
||||
visibility_.clear();
|
||||
Log::info() << "[wgpu] show all";
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
// Alt+X — toggle global X-ray (translucent everything). The frame
|
||||
// uniform `xray_alpha_cap` clamps `fs_main`'s output alpha; the cull
|
||||
// classifier sees `xray_alpha_cap_ < 1` and routes every instance
|
||||
// through the transparent pass so the blend actually fires.
|
||||
// Visibility + X-ray math lives in ViewportCore (shared with web).
|
||||
if (key == Qt::Key_H && mods == Qt::AltModifier) { core_.showAll(); return; }
|
||||
if (key == Qt::Key_X && mods == Qt::AltModifier && !event->isAutoRepeat()) {
|
||||
constexpr float kXrayOnCap = 0.3f;
|
||||
xray_alpha_cap_ = (xray_alpha_cap_ < 1.0f) ? 1.0f : kXrayOnCap;
|
||||
Log::info().noquote().nospace()
|
||||
<< "[wgpu] x-ray "
|
||||
<< (xray_alpha_cap_ < 1.0f ? "ON" : "OFF")
|
||||
<< " (cap=" << xray_alpha_cap_ << ")";
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (key == Qt::Key_H && mods == Qt::ShiftModifier) {
|
||||
if (selection_.count() == 0) return;
|
||||
size_t hidden_now = 0;
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
for (const auto& inst : m.instances) {
|
||||
if (selection_.contains(inst.object_id)) continue;
|
||||
if (!visibility_.isHidden(inst.object_id)) {
|
||||
visibility_.hide(inst.object_id);
|
||||
++hidden_now;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log::info().noquote().nospace() << "[wgpu] isolated " << selection_.count()
|
||||
<< " (hid " << hidden_now << " others)";
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (key == Qt::Key_H && mods == Qt::NoModifier) {
|
||||
if (selection_.count() == 0) return;
|
||||
for (uint32_t id : selection_.selectionIds()) visibility_.hide(id);
|
||||
const size_t n = selection_.count();
|
||||
selection_.clear(); // hiding deselects, matching GL behaviour
|
||||
Log::info().noquote().nospace() << "[wgpu] hid " << n << " selected";
|
||||
requestUpdate();
|
||||
return;
|
||||
core_.toggleXray(); return;
|
||||
}
|
||||
if (key == Qt::Key_H && mods == Qt::ShiftModifier) { core_.isolateSelected(); return; }
|
||||
if (key == Qt::Key_H && mods == Qt::NoModifier) { core_.hideSelected(); return; }
|
||||
if (key == Qt::Key_F && mods == Qt::ShiftModifier && !event->isAutoRepeat()) {
|
||||
enterFpsMode();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user