mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-25 04:44:37 +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
|
# 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
|
# 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.
|
# 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.
|
# 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
|
# 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.
|
# 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.
|
// 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 (app->fly_mode) { if (setFlyKey(app, code, true)) return EM_TRUE; return EM_FALSE; }
|
||||||
if (e->repeat) 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;
|
using SV = ViewportCore::StandardView;
|
||||||
if (!std::strcmp(code, "Home")) app->core.viewAll();
|
if (!std::strcmp(code, "Home")) app->core.viewAll();
|
||||||
else if (!std::strcmp(code, "KeyF") && !shift) app->core.frameSelection();
|
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;
|
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.
|
// id: 0 Front, 1 Back, 2 Left, 3 Right, 4 Top, 5 Bottom.
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void standard_view_c(int id) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void standard_view_c(int id) {
|
||||||
if (!g_app || !g_app->ready || id < 0 || id > 5) return;
|
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="3" title="Right (Y)">Right</button>
|
||||||
<button data-view="4" title="Top (Z)">Top</button>
|
<button data-view="4" title="Top (Z)">Top</button>
|
||||||
<button data-view="5" title="Bottom (Shift+Z)">Bottom</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>
|
||||||
<div id="status">Starting…</div>
|
<div id="status">Starting…</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -162,6 +167,10 @@
|
|||||||
var fb = document.getElementById('fly-btn');
|
var fb = document.getElementById('fly-btn');
|
||||||
if (fb) fb.classList.toggle('active', !!Module._fly_is_active_c());
|
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);
|
Module._raf_tick_c(Module._app_ptr);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(shellTick);
|
requestAnimationFrame(shellTick);
|
||||||
@@ -338,6 +347,10 @@
|
|||||||
else if (act === 'focus') Module._frame_selection_c();
|
else if (act === 'focus') Module._frame_selection_c();
|
||||||
else if (act === 'ortho') { Module._toggle_projection_c(); refreshOrthoLabel(); }
|
else if (act === 'ortho') { Module._toggle_projection_c(); refreshOrthoLabel(); }
|
||||||
else if (act === 'fly') Module._toggle_fly_c();
|
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));
|
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);
|
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__)
|
#if defined(__EMSCRIPTEN__)
|
||||||
void ViewportCore::pickObjectAtAsync(int x_pixels, int y_pixels,
|
void ViewportCore::pickObjectAtAsync(int x_pixels, int y_pixels,
|
||||||
std::function<void(std::uint32_t)> cb) {
|
std::function<void(std::uint32_t)> cb) {
|
||||||
|
|||||||
@@ -624,6 +624,19 @@ public:
|
|||||||
// Marks selection_ dirty for the next render's flush.
|
// Marks selection_ dirty for the next render's flush.
|
||||||
void applyPickToSelection(std::uint32_t object_id, bool add, bool remove);
|
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__)
|
#if defined(__EMSCRIPTEN__)
|
||||||
// Async object pick for the web build: encodes the same pick pass as
|
// Async object pick for the web build: encodes the same pick pass as
|
||||||
// pickObjectAt but reads the staging buffer back via a spontaneous map
|
// pickObjectAt but reads the staging buffer back via a spontaneous map
|
||||||
|
|||||||
@@ -1124,36 +1124,11 @@ void ViewportWindow::setSelectedObjectId(uint32_t id) {
|
|||||||
if (isExposed()) requestUpdate();
|
if (isExposed()) requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ViewportWindow::hideSelectedElements() {
|
// Visibility ops now live in ViewportCore (shared with web); these stay as thin
|
||||||
if (selection_.count() == 0) return;
|
// Qt-facing wrappers for the menu actions.
|
||||||
for (uint32_t id : selection_.selectionIds()) visibility_.hide(id);
|
void ViewportWindow::hideSelectedElements() { core_.hideSelected(); }
|
||||||
selection_.clear();
|
void ViewportWindow::isolateSelectedElements() { core_.isolateSelected(); }
|
||||||
if (isExposed()) requestUpdate();
|
void ViewportWindow::showAllElements() { core_.showAll(); }
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ViewportWindow::invertElementVisibility() {
|
void ViewportWindow::invertElementVisibility() {
|
||||||
// Compute the new hidden set: every live object_id in a visible model
|
// 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
|
// Shift+H — isolate selected
|
||||||
// Alt+H — show all (clear hidden set)
|
// Alt+H — show all (clear hidden set)
|
||||||
// Shift+F — enter fly mode (Esc exits)
|
// Shift+F — enter fly mode (Esc exits)
|
||||||
if (key == Qt::Key_H && mods == Qt::AltModifier) {
|
// Visibility + X-ray math lives in ViewportCore (shared with web).
|
||||||
if (visibility_.hiddenCount() == 0) return;
|
if (key == Qt::Key_H && mods == Qt::AltModifier) { core_.showAll(); 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.
|
|
||||||
if (key == Qt::Key_X && mods == Qt::AltModifier && !event->isAutoRepeat()) {
|
if (key == Qt::Key_X && mods == Qt::AltModifier && !event->isAutoRepeat()) {
|
||||||
constexpr float kXrayOnCap = 0.3f;
|
core_.toggleXray(); return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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()) {
|
if (key == Qt::Key_F && mods == Qt::ShiftModifier && !event->isAutoRepeat()) {
|
||||||
enterFpsMode();
|
enterFpsMode();
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user