From f1d97ac5aaeebfb211a995e3c57c7620c7c71e02 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 08:37:27 +1000 Subject: [PATCH 01/11] ifcviewer: preset-driven nav mouse bindings + a "Web" preset (desktop + web) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make orbit/pan/select mouse bindings pure data owned by ViewportCore so both hosts and every preset share one source of truth, and add a "Web" preset. This rounds out the matrix: the desktop gains a web-style scheme and the web inherits all presets, with no per-platform hardcoding. - Core: NavBindings { orbit, pan, select button + modifier } + setNavPreset ("blender" default | "rhino" | "revit" | "web") + navBindings(). Select is preset-driven too (was hardcoded LMB) so "web" moves it to RMB. web = orbit LMB, pan MMB, select RMB (LMB drag orbits with no click/drag ambiguity; RMB click-selects / drag-marquees). NavMod uses "Plain" not "None" (X11 #defines None to 0L). - Desktop ViewportWindow: applyNavPreset sources the core table (mapped to Qt); marquee-arm / single-pick dispatch keys off select_button_. Default stays blender → no behaviour change. - Desktop config: AppSettings::NavPreset gains Web + navPresetName(); the Settings dialog lists it. This also FIXES a pre-existing gap — the preset combo was persisted but never applied (only WGPU_NAV_PRESET env worked). MainWindow now applies the persisted preset at startup (env override still wins) and live on navPresetChanged, so all four presets actually work from the dialog. - Web main_web: classifyPress routes the pressed button through navBindings() (orbit/pan/select), defaulting to the "web" preset; context menu already suppressed so RMB is free. Tests: setNavPreset table (Catch2, 123 total); web smoke select tests use RMB. BonsaiViewer builds; 9/9 web smoke. Co-Authored-By: Claude Opus 4.8 --- src/bonsaiviewer/MainWindow.cpp | 34 +- src/bonsaiviewer/modules/settings/Dialog.cpp | 15 +- src/ifcviewer-web/main_web.cpp | 121 ++++--- src/ifcviewer-web/tests/smoke.spec.mjs | 6 +- src/ifcviewer/AppSettings.cpp | 12 +- src/ifcviewer/AppSettings.h | 5 + src/ifcviewer/ViewportCore.cpp | 354 ++++++++++--------- src/ifcviewer/ViewportCore.h | 28 ++ src/ifcviewer/ViewportWindow.cpp | 53 +-- src/ifcviewer/ViewportWindow.h | 20 +- src/ifcviewer/tests/test_viewport_camera.cpp | 38 ++ 11 files changed, 430 insertions(+), 256 deletions(-) diff --git a/src/bonsaiviewer/MainWindow.cpp b/src/bonsaiviewer/MainWindow.cpp index 5cfb1d0d45..4aee23127c 100644 --- a/src/bonsaiviewer/MainWindow.cpp +++ b/src/bonsaiviewer/MainWindow.cpp @@ -100,10 +100,10 @@ void MainWindow::setupChrome() { QMainWindow::AllowTabbedDocks | QMainWindow::GroupedDragging); - auto bind_shortcut = [this](const QKeySequence& sequence, auto fn) { + auto bind_shortcut = [this](const QKeySequence& sequence, auto handler) { auto* shortcut = new QShortcut(sequence, this); shortcut->setContext(Qt::WindowShortcut); - connect(shortcut, &QShortcut::activated, this, fn); + connect(shortcut, &QShortcut::activated, this, handler); }; bind_shortcut(QKeySequence("Ctrl+Shift+L"), [this]() { modules::viewport::commands::toggleDistance(*viewport_widget_->viewport()); @@ -496,6 +496,20 @@ void MainWindow::setupStatus() { status_perf_label_->setVisible(show); if (!show) status_perf_label_->clear(); }); + + // Nav mouse preset. The WGPU_NAV_PRESET env var is a dev override applied at + // ViewportWindow construction; otherwise apply the persisted Settings choice + // here, and re-apply live whenever the user changes it in the dialog. + if (!std::getenv("WGPU_NAV_PRESET")) { + if (auto* vp = viewport_widget_->viewport()) + vp->applyNavPreset(AppSettings::navPresetName(AppSettings::instance().navPreset())); + } + connect(&AppSettings::instance(), &AppSettings::navPresetChanged, this, + [this](AppSettings::NavPreset preset) { + if (auto* vp = viewport_widget_->viewport()) + vp->applyNavPreset(AppSettings::navPresetName(preset)); + }); + connect(session_state_, &bonsaiviewer::SessionState::statusMessageChanged, this, [this](const QString& mode, const QString& detail) { status_mode_label_->setText(mode); @@ -530,17 +544,17 @@ void MainWindow::setupLoader() { }); connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this, - [this](const ViewportWindow::FrameStats& s) { + [this](const ViewportWindow::FrameStats& stats) { if (!status_perf_label_->isVisible()) return; status_perf_label_->setText( QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws") - .arg(s.fps, 0, 'f', 1) - .arg(s.frame_time_ms, 0, 'f', 1) - .arg(s.visible_objects) - .arg(s.total_objects) - .arg(s.visible_triangles) - .arg(s.total_triangles) - .arg(s.gl_draw_calls)); + .arg(stats.fps, 0, 'f', 1) + .arg(stats.frame_time_ms, 0, 'f', 1) + .arg(stats.visible_objects) + .arg(stats.total_objects) + .arg(stats.visible_triangles) + .arg(stats.total_triangles) + .arg(stats.gl_draw_calls)); }); connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked, this, [this](uint32_t object_id) { diff --git a/src/bonsaiviewer/modules/settings/Dialog.cpp b/src/bonsaiviewer/modules/settings/Dialog.cpp index 2f029d03e5..f93e114531 100644 --- a/src/bonsaiviewer/modules/settings/Dialog.cpp +++ b/src/bonsaiviewer/modules/settings/Dialog.cpp @@ -188,10 +188,11 @@ void SettingsDialog::setupUi() { nav_preset_combo_->addItem("Blender (Orbit MMB, Pan Shift+MMB)"); nav_preset_combo_->addItem("Rhino (Orbit RMB, Pan Shift+RMB)"); nav_preset_combo_->addItem("Revit (Orbit Shift+MMB, Pan MMB)"); + nav_preset_combo_->addItem("Web (Orbit LMB, Pan MMB, Select RMB)"); nav_preset_combo_->setToolTip( - "Mouse-button mapping for orbit and pan. Selection stays on " - "left mouse button for every preset, so click + box-select " - "always work."); + "Mouse-button mapping for orbit, pan, and selection. Selection is " + "on the left mouse button for Blender/Rhino/Revit and on the right " + "for Web; click + box-select use whichever the preset assigns."); form->addRow("Preset", nav_preset_combo_); section->addBodyWidget(body); @@ -390,20 +391,20 @@ QWidget* SettingsDialog::buildConnectorsTab() { body_layout->addWidget(empty); } - for (const auto& m : manifests) { + for (const auto& manifest : manifests) { auto* row = new QWidget(body); auto* row_layout = new QHBoxLayout(row); row_layout->setContentsMargins(0, 0, 0, 0); row_layout->setSpacing(8); - auto* name = new QLabel(m.name, row); + auto* name = new QLabel(manifest.name, row); auto* version = new QLabel( - m.version.isEmpty() ? QString() : QString("v%1").arg(m.version), row); + manifest.version.isEmpty() ? QString() : QString("v%1").arg(manifest.version), row); version->setProperty("textRole", "secondary"); auto* settings_button = new QPushButton("Settings…", row); settings_button->setIcon(components::icons::makeSvgIcon(":/icons/settings.svg")); - const QString connector_id = m.id; + const QString connector_id = manifest.id; connect(settings_button, &QPushButton::clicked, this, [this, connector_id, settings_button]() { if (!session_state_) return; diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp index 4a0d02bde8..8208a9250a 100644 --- a/src/ifcviewer-web/main_web.cpp +++ b/src/ifcviewer-web/main_web.cpp @@ -45,6 +45,9 @@ namespace { // WebViewportHost selector below. constexpr const char* kCanvasSelector = "#viewer-canvas"; +// What a mouse drag drives, decided against the active nav preset's bindings. +enum class NavKind { None, Orbit, Pan, Select }; + struct AppState { WebViewportHost host{ kCanvasSelector }; ViewportCore core{ &host }; @@ -56,15 +59,14 @@ struct AppState { bool ready = false; // ---- Mouse navigation state ---- - // A drag is armed on mousedown and released on mouseup. button is the - // DOM button code (0 left, 1 middle, 2 right). Left orbits; middle or - // right pans — matches common web 3D-viewer bindings and covers both - // three-button mice and trackpad (right-drag) users. - bool nav_active = false; - int nav_button = 0; - // Accumulated |movement| since mousedown, in CSS px. A left release under - // the click threshold (no real drag) is treated as a pick instead of an - // orbit. Captures the down position (canvas-relative CSS px) for the pick. + // A drag is armed on mousedown and released on mouseup. What the press + // drives (orbit / pan / select) is decided against the active nav preset's + // bindings (ViewportCore::navBindings), so any preset works on web too. + bool nav_active = false; + NavKind nav_kind = NavKind::None; + // Accumulated |movement| since mousedown, in CSS px. A select-button release + // under the click threshold (no real drag) is treated as a pick; a drag will + // become a marquee. Captures the down position (canvas-relative CSS px). float nav_drag_px = 0.0f; long down_x = 0; long down_y = 0; @@ -78,7 +80,13 @@ struct AppState { // Distinguishes "lock lost" (Esc/click-out → exit fly) from "lock denied on // entry" (headless / permission) where fly stays keyboard-drivable. bool fly_locked = false; - bool k_w = false, k_a = false, k_s = false, k_d = false, k_q = false, k_e = false, k_shift = false; + bool key_w_pressed = false; + bool key_a_pressed = false; + bool key_s_pressed = false; + bool key_d_pressed = false; + bool key_q_pressed = false; + bool key_e_pressed = false; + bool key_shift_pressed = false; double fly_last_ms = 0.0; }; @@ -102,13 +110,26 @@ int canvasCssHeight() { return (h > 1.0) ? int(h) : 1; } +NavKind classifyPress(const ViewportCore::NavBindings& b, int em_button, + bool shift, bool ctrl, bool alt) { + using MB = ViewportCore::MouseBtn; using M = ViewportCore::NavMod; + const MB btn = (em_button == 0) ? MB::Left : (em_button == 1) ? MB::Middle : MB::Right; + const M mod = shift ? M::Shift : ctrl ? M::Ctrl : alt ? M::Alt : M::Plain; + if (btn == b.orbit && mod == b.orbit_mod) return NavKind::Orbit; + if (btn == b.pan && mod == b.pan_mod) return NavKind::Pan; + if (btn == b.select) return NavKind::Select; // Shift/Ctrl = add/remove + return NavKind::None; +} + EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void* user) { auto* app = static_cast(user); // In fly mode a click exits (matches the desktop app). if (app->fly_mode) { setFlyMode(app, false); return EM_TRUE; } - if (e->button == 0 || e->button == 1 || e->button == 2) { + const NavKind kind = classifyPress(app->core.navBindings(), e->button, + e->shiftKey, e->ctrlKey, e->altKey); + if (kind != NavKind::None) { app->nav_active = true; - app->nav_button = e->button; + app->nav_kind = kind; app->nav_drag_px = 0.0f; app->down_x = e->targetX; // canvas-relative CSS px app->down_y = e->targetY; @@ -129,24 +150,23 @@ EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void* user) { const float dx = float(e->movementX); const float dy = float(e->movementY); app->nav_drag_px += std::abs(dx) + std::abs(dy); - if (app->nav_button == 0) { - app->core.orbitBy(dx, dy); - } else { - app->core.panBy(dx, dy, canvasCssHeight()); - } + if (app->nav_kind == NavKind::Orbit) app->core.orbitBy(dx, dy); + else if (app->nav_kind == NavKind::Pan) app->core.panBy(dx, dy, canvasCssHeight()); + // NavKind::Select drag → marquee box-select (next step). return EM_TRUE; } EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) { auto* app = static_cast(user); - const bool was_active = app->nav_active; - const int button = app->nav_button; + const bool was_active = app->nav_active; + const NavKind kind = app->nav_kind; app->nav_active = false; + app->nav_kind = NavKind::None; - // Left release with no real drag → pick the object under the cursor and - // route it through selection (Shift add, Ctrl remove, plain replace). + // Select-button release with no real drag → pick the object under the cursor + // and route it through selection (Shift add, Ctrl remove, plain replace). // Async readback: the highlight appears a frame after the result lands. - if (was_active && button == 0 && app->ready && + if (was_active && kind == NavKind::Select && app->ready && app->nav_drag_px <= kClickDragThresholdPx) { const double dpr = emscripten_get_device_pixel_ratio(); const int px = int(app->down_x * dpr); @@ -183,14 +203,14 @@ EM_BOOL onWheel(int, const EmscriptenWheelEvent* e, void* user) { // Track a held fly movement key (W/A/S/D/Q/E/Shift). Returns true if `code` was // one. Physical `.code` so it's keymap-independent. bool setFlyKey(AppState* app, const char* code, bool down) { - if (!std::strcmp(code, "KeyW")) app->k_w = down; - else if (!std::strcmp(code, "KeyA")) app->k_a = down; - else if (!std::strcmp(code, "KeyS")) app->k_s = down; - else if (!std::strcmp(code, "KeyD")) app->k_d = down; - else if (!std::strcmp(code, "KeyQ")) app->k_q = down; - else if (!std::strcmp(code, "KeyE")) app->k_e = down; + if (!std::strcmp(code, "KeyW")) app->key_w_pressed = down; + else if (!std::strcmp(code, "KeyA")) app->key_a_pressed = down; + else if (!std::strcmp(code, "KeyS")) app->key_s_pressed = down; + else if (!std::strcmp(code, "KeyD")) app->key_d_pressed = down; + else if (!std::strcmp(code, "KeyQ")) app->key_q_pressed = down; + else if (!std::strcmp(code, "KeyE")) app->key_e_pressed = down; else if (!std::strcmp(code, "ShiftLeft") || !std::strcmp(code, "ShiftRight")) - app->k_shift = down; + app->key_shift_pressed = down; else return false; return true; } @@ -205,7 +225,9 @@ void setFlyMode(AppState* app, bool on) { emscripten_request_pointerlock(kCanvasSelector, EM_TRUE); Log::info() << "[fly] on — WASD/QE move, mouse looks, Shift boosts, wheel = speed, Esc exits"; } else { - app->k_w = app->k_a = app->k_s = app->k_d = app->k_q = app->k_e = app->k_shift = false; + app->key_w_pressed = app->key_a_pressed = app->key_s_pressed = false; + app->key_d_pressed = app->key_q_pressed = app->key_e_pressed = false; + app->key_shift_pressed = false; app->fly_locked = false; emscripten_exit_pointerlock(); Log::info() << "[fly] off"; @@ -312,8 +334,10 @@ extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) { const double now = emscripten_get_now(); const float dt = float((now - app->fly_last_ms) / 1000.0); app->fly_last_ms = now; - app->core.flyMove(app->k_w, app->k_s, app->k_d, app->k_a, - app->k_e, app->k_q, app->k_shift, dt); + app->core.flyMove(app->key_w_pressed, app->key_s_pressed, + app->key_d_pressed, app->key_a_pressed, + app->key_e_pressed, app->key_q_pressed, + app->key_shift_pressed, dt); } if (app->host.consumeFrameRequest()) { @@ -387,11 +411,15 @@ extern "C" EMSCRIPTEN_KEEPALIVE void standard_view_c(int id) { // geometry chunks arrive. extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_chunks_resident_c() { if (!g_app) return 0; - int r = 0, t = 0; g_app->core.streamingProgress(r, t); return r; + int resident_chunks = 0, total_chunks = 0; + g_app->core.streamingProgress(resident_chunks, total_chunks); + return resident_chunks; } extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_chunks_total_c() { if (!g_app) return 0; - int r = 0, t = 0; g_app->core.streamingProgress(r, t); return t; + int resident_chunks = 0, total_chunks = 0; + g_app->core.streamingProgress(resident_chunks, total_chunks); + return total_chunks; } // Per-model progress for the federation loading panel: how many models are in @@ -401,11 +429,15 @@ extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_count_c() { } extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_resident_c(int idx) { if (!g_app) return 0; - int r = 0, t = 0; g_app->core.streamingModelProgress(idx, r, t); return r; + int resident_chunks = 0, total_chunks = 0; + g_app->core.streamingModelProgress(idx, resident_chunks, total_chunks); + return resident_chunks; } extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_total_c(int idx) { if (!g_app) return 0; - int r = 0, t = 0; g_app->core.streamingModelProgress(idx, r, t); return t; + int resident_chunks = 0, total_chunks = 0; + g_app->core.streamingModelProgress(idx, resident_chunks, total_chunks); + return total_chunks; } // Combined byte progress for the loading bar: total geometry, bytes the current @@ -413,23 +445,28 @@ extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_total_c(int idx) { // JS gets exact byte counts well past 2 GB. extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_total_c() { if (!g_app) return 0.0; - std::uint64_t tot = 0, need = 0, load = 0; - g_app->core.streamingByteProgress(tot, need, load); return double(tot); + std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0; + g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes); + return double(total_bytes); } extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_needed_c() { if (!g_app) return 0.0; - std::uint64_t tot = 0, need = 0, load = 0; - g_app->core.streamingByteProgress(tot, need, load); return double(need); + std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0; + g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes); + return double(needed_bytes); } extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_loaded_c() { if (!g_app) return 0.0; - std::uint64_t tot = 0, need = 0, load = 0; - g_app->core.streamingByteProgress(tot, need, load); return double(load); + std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0; + g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes); + return double(loaded_bytes); } int main(int /*argc*/, char** /*argv*/) { Log::info() << "ifcviewer-web: starting"; g_app = new AppState(); + // Default to the web mouse scheme: LMB orbit, MMB pan, RMB select/marquee. + g_app->core.setNavPreset("web"); g_app->core.initWgpuAsyncWeb([](bool ok) { if (!ok) { Log::warn() << "ifcviewer-web: wgpu init failed"; diff --git a/src/ifcviewer-web/tests/smoke.spec.mjs b/src/ifcviewer-web/tests/smoke.spec.mjs index 1664237336..6316c4e264 100644 --- a/src/ifcviewer-web/tests/smoke.spec.mjs +++ b/src/ifcviewer-web/tests/smoke.spec.mjs @@ -261,7 +261,7 @@ test('click selects an object and the highlight renders (async pick)', async ({ const cx = box.x + box.width / 2; const cy = box.y + box.height / 2; const before = await shot(page); - await page.mouse.click(cx, cy); + await page.mouse.click(cx, cy, { button: 'right' }); // Web preset: RMB selects await page.waitForTimeout(600); // async pick result + flush + render const after = await shot(page); expect( @@ -330,8 +330,8 @@ test('hide selected removes geometry after a pick', async ({ page }) => { const box = await page.locator('#viewer-canvas').boundingBox(); const before = await shot(page); - // select whatever is under the centre, then hide it - await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + // select whatever is under the centre (Web preset: RMB selects), then hide it + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: 'right' }); await page.waitForTimeout(400); await page.evaluate(() => window.Module._hide_selected_c()); await page.waitForTimeout(400); diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp index 8b8212cb4a..86d23ea57b 100644 --- a/src/ifcviewer/AppSettings.cpp +++ b/src/ifcviewer/AppSettings.cpp @@ -186,6 +186,16 @@ AppSettings::NavPreset AppSettings::navPreset() const { return nav_preset_; } +const char* AppSettings::navPresetName(NavPreset preset) { + switch (preset) { + case NavPreset::Rhino: return "rhino"; + case NavPreset::Revit: return "revit"; + case NavPreset::Web: return "web"; + case NavPreset::Blender: break; + } + return "blender"; +} + void AppSettings::setNavPreset(NavPreset value) { if (nav_preset_ == value) return; nav_preset_ = value; @@ -220,7 +230,7 @@ void AppSettings::load() { static_cast(NavPreset::Blender)).toInt(); // Clamp to known values so a stale config doesn't drop us into // an undefined preset slot. - if (raw < 0 || raw > static_cast(NavPreset::Revit)) { + if (raw < 0 || raw > static_cast(NavPreset::Web)) { nav_preset_ = NavPreset::Blender; } else { nav_preset_ = static_cast(raw); diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h index 9c38819a6a..fbe177059e 100644 --- a/src/ifcviewer/AppSettings.h +++ b/src/ifcviewer/AppSettings.h @@ -37,13 +37,18 @@ public: // Blender — Orbit MMB, Pan Shift+MMB (current default) // Rhino — Orbit RMB, Pan Shift+RMB // Revit — Orbit Shift+MMB, Pan MMB + // Web — Orbit LMB, Pan MMB, Select RMB enum class NavPreset { Blender = 0, Rhino = 1, Revit = 2, + Web = 3, }; Q_ENUM(NavPreset) + // Preset → the lowercase name ViewportCore::setNavPreset / applyNavPreset take. + static const char* navPresetName(NavPreset preset); + static AppSettings& instance(); QString geometryLibrary() const; diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 636e206d84..240bf494eb 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -461,6 +461,18 @@ void ViewportCore::setStandardView(StandardView view) { } } +void ViewportCore::setNavPreset(const char* name) { + using B = MouseBtn; using M = NavMod; + if (name && std::strcmp(name, "rhino") == 0) + nav_bindings_ = { B::Right, M::Plain, B::Right, M::Shift, B::Left, M::Plain }; + else if (name && std::strcmp(name, "revit") == 0) + nav_bindings_ = { B::Middle, M::Shift, B::Middle, M::Plain, B::Left, M::Plain }; + else if (name && std::strcmp(name, "web") == 0) + nav_bindings_ = { B::Left, M::Plain, B::Middle, M::Plain, B::Right, M::Plain }; + else // blender (default) + nav_bindings_ = { B::Middle, M::Plain, B::Middle, M::Shift, B::Left, M::Plain }; +} + bool ViewportCore::frameSelection() { if (selection_.count() == 0) return false; float lo[3] = { std::numeric_limits::infinity(), @@ -2901,13 +2913,13 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, models_gpu_.erase(it); } - ModelGpuData m; - m.vertex_bytes = 0; // accumulated from chunks below (v16 has no section) - m.index_count = 0; - m.mesh_count = std::uint32_t(metadata.meta.meshes.size()); - m.instance_count = std::uint32_t(metadata.meta.instances.size()); - m.streaming_file_path = metadata.file_path; - m.geometry_section_offset = metadata.geometry_section_offset; + ModelGpuData model_gpu_data; + model_gpu_data.vertex_bytes = 0; // accumulated from chunks below (v16 has no section) + model_gpu_data.index_count = 0; + model_gpu_data.mesh_count = std::uint32_t(metadata.meta.meshes.size()); + model_gpu_data.instance_count = std::uint32_t(metadata.meta.instances.size()); + model_gpu_data.streaming_file_path = metadata.file_path; + model_gpu_data.geometry_section_offset = metadata.geometry_section_offset; // ---- Spatial chunk plan ---------------------------------------------- // A sidecar carries a baked chunk TOC (v14): each chunk is a contiguous @@ -2918,10 +2930,10 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, // would scatter the chunks. In-memory direct loads (finalizeModel) carry // no TOC, so they fall back to deriving the same Morton + greedy plan. const std::size_t n_meshes = metadata.meta.meshes.size(); - m.mesh_chunk_idx.assign(n_meshes, 0); - m.mesh_chunk_local_base_vertex.assign(n_meshes, 0); - m.mesh_chunk_local_ebo_first_u32.assign(n_meshes, 0); - m.mesh_chunk_local_lod1_first_u32.assign(n_meshes, 0); + model_gpu_data.mesh_chunk_idx.assign(n_meshes, 0); + model_gpu_data.mesh_chunk_local_base_vertex.assign(n_meshes, 0); + model_gpu_data.mesh_chunk_local_ebo_first_u32.assign(n_meshes, 0); + model_gpu_data.mesh_chunk_local_lod1_first_u32.assign(n_meshes, 0); std::vector> chunk_mesh_ids; std::vector instance_to_chunk; @@ -2930,14 +2942,14 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, if (!metadata.meta.chunks.empty()) { // Baked TOC: chunk ci is meshes [first_mesh, first_mesh + mesh_count). chunk_mesh_ids.reserve(metadata.meta.chunks.size()); - for (const auto& ch : metadata.meta.chunks) { - std::vector ids; - ids.reserve(ch.mesh_count); - for (std::uint32_t k = 0; k < ch.mesh_count; ++k) { - const std::uint32_t mi = ch.first_mesh + k; - if (mi < n_meshes) ids.push_back(mi); + for (const auto& sidecar_chunk : metadata.meta.chunks) { + std::vector mesh_ids; + mesh_ids.reserve(sidecar_chunk.mesh_count); + for (std::uint32_t k = 0; k < sidecar_chunk.mesh_count; ++k) { + const std::uint32_t mesh_index = sidecar_chunk.first_mesh + k; + if (mesh_index < n_meshes) mesh_ids.push_back(mesh_index); } - chunk_mesh_ids.push_back(std::move(ids)); + chunk_mesh_ids.push_back(std::move(mesh_ids)); } } else { // No TOC (direct load): derive the plan from mesh centroids. @@ -2972,25 +2984,27 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, { std::vector mesh_to_chunk(n_meshes, 0); - for (std::size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { - for (std::uint32_t mi : chunk_mesh_ids[ci]) mesh_to_chunk[mi] = std::uint32_t(ci); + for (std::size_t chunk_index = 0; chunk_index < chunk_mesh_ids.size(); ++chunk_index) { + for (std::uint32_t mesh_index : chunk_mesh_ids[chunk_index]) { + mesh_to_chunk[mesh_index] = std::uint32_t(chunk_index); + } } for (std::size_t i = 0; i < metadata.meta.instances.size(); ++i) { - const std::uint32_t mi = metadata.meta.instances[i].mesh_id; - if (mi < n_meshes) instance_to_chunk[i] = mesh_to_chunk[mi]; + const std::uint32_t mesh_index = metadata.meta.instances[i].mesh_id; + if (mesh_index < n_meshes) instance_to_chunk[i] = mesh_to_chunk[mesh_index]; } } std::vector chunk_instance_count(chunk_mesh_ids.size(), 0); for (std::size_t i = 0; i < instance_to_chunk.size(); ++i) { - const std::uint32_t ci = instance_to_chunk[i]; - if (ci < chunk_instance_count.size()) ++chunk_instance_count[ci]; + const std::uint32_t chunk_index = instance_to_chunk[i]; + if (chunk_index < chunk_instance_count.size()) ++chunk_instance_count[chunk_index]; } // ---- Allocate per-chunk state. NO pool slices yet (chunks are // non-resident); the per-frame loader brings them in as cull marks // them visible. - m.chunks.resize(chunk_mesh_ids.size()); + model_gpu_data.chunks.resize(chunk_mesh_ids.size()); struct MeshLocal { std::uint32_t base_vertex; std::uint32_t ebo_first; @@ -2998,46 +3012,51 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, }; std::vector> chunk_mesh_offsets(chunk_mesh_ids.size()); - for (std::size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { - ModelGpuData::Chunk& c = m.chunks[ci]; - c.mesh_ids = std::move(chunk_mesh_ids[ci]); - c.is_resident = false; + for (std::size_t chunk_index = 0; chunk_index < chunk_mesh_ids.size(); ++chunk_index) { + ModelGpuData::Chunk& chunk = model_gpu_data.chunks[chunk_index]; + chunk.mesh_ids = std::move(chunk_mesh_ids[chunk_index]); + chunk.is_resident = false; - std::uint32_t chunk_local_v = 0; - std::uint32_t chunk_local_i = 0; - for (std::uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = metadata.meta.meshes[mi]; - m.mesh_chunk_idx[mi] = std::uint32_t(ci); - m.mesh_chunk_local_base_vertex[mi] = chunk_local_v; - m.mesh_chunk_local_ebo_first_u32[mi] = chunk_local_i; - chunk_mesh_offsets[ci][mi] = MeshLocal{chunk_local_v, chunk_local_i, 0}; - chunk_local_v += mesh.vertex_count; - chunk_local_i += mesh.index_count; + std::uint32_t chunk_local_vertex_count = 0; + std::uint32_t chunk_local_index_count = 0; + for (std::uint32_t mesh_index : chunk.mesh_ids) { + const MeshInfo& mesh = metadata.meta.meshes[mesh_index]; + model_gpu_data.mesh_chunk_idx[mesh_index] = std::uint32_t(chunk_index); + model_gpu_data.mesh_chunk_local_base_vertex[mesh_index] = chunk_local_vertex_count; + model_gpu_data.mesh_chunk_local_ebo_first_u32[mesh_index] = chunk_local_index_count; + chunk_mesh_offsets[chunk_index][mesh_index] = + MeshLocal{chunk_local_vertex_count, chunk_local_index_count, 0}; + chunk_local_vertex_count += mesh.vertex_count; + chunk_local_index_count += mesh.index_count; } std::uint32_t chunk_local_lod1 = 0; - for (std::uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = metadata.meta.meshes[mi]; + for (std::uint32_t mesh_index : chunk.mesh_ids) { + const MeshInfo& mesh = metadata.meta.meshes[mesh_index]; if (mesh.lod1_index_count == 0) continue; - m.mesh_chunk_local_lod1_first_u32[mi] = chunk_local_i + chunk_local_lod1; - chunk_mesh_offsets[ci][mi].lod1_first = chunk_local_i + chunk_local_lod1; + model_gpu_data.mesh_chunk_local_lod1_first_u32[mesh_index] = + chunk_local_index_count + chunk_local_lod1; + chunk_mesh_offsets[chunk_index][mesh_index].lod1_first = + chunk_local_index_count + chunk_local_lod1; chunk_local_lod1 += mesh.lod1_index_count; } - c.vertex_count = chunk_local_v; - c.vertex_byte_size = std::uint64_t(chunk_local_v) * INSTANCED_VERTEX_STRIDE_BYTES; - c.index_count = chunk_local_i + chunk_local_lod1; - c.lod1_index_count = chunk_local_lod1; + chunk.vertex_count = chunk_local_vertex_count; + chunk.vertex_byte_size = std::uint64_t(chunk_local_vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + chunk.index_count = chunk_local_index_count + chunk_local_lod1; + chunk.lod1_index_count = chunk_local_lod1; // v16: compressed-blob locators from the baked TOC (streaming path). - if (ci < metadata.meta.chunks.size()) { - const SidecarChunk& sc = metadata.meta.chunks[ci]; - c.v_comp_off = sc.v_comp_off; c.v_comp_size = sc.v_comp_size; - c.i_comp_off = sc.i_comp_off; c.i_comp_size = sc.i_comp_size; + if (chunk_index < metadata.meta.chunks.size()) { + const SidecarChunk& sidecar_chunk = metadata.meta.chunks[chunk_index]; + chunk.v_comp_off = sidecar_chunk.v_comp_off; + chunk.v_comp_size = sidecar_chunk.v_comp_size; + chunk.i_comp_off = sidecar_chunk.i_comp_off; + chunk.i_comp_size = sidecar_chunk.i_comp_size; } - m.vertex_bytes += c.vertex_byte_size; - m.index_count += std::uint32_t(c.index_count); + model_gpu_data.vertex_bytes += chunk.vertex_byte_size; + model_gpu_data.index_count += std::uint32_t(chunk.index_count); // Small per-chunk buffers, allocated upfront so cull can write into // them. visible_draws_buffer cap = chunk's instance count. - const std::size_t chunk_inst = std::max(chunk_instance_count[ci], 1); + const std::size_t chunk_inst = std::max(chunk_instance_count[chunk_index], 1); const std::size_t draws_bytes = chunk_inst * sizeof(ModelGpuData::VisibleDrawGpu); const std::size_t ps_bytes = (chunk_inst + 1) * sizeof(std::uint32_t); @@ -3045,27 +3064,27 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, vd_desc.size = std::max(draws_bytes, 16); vd_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; vd_desc.label = svFromCStr("model.chunk.visible_draws"); - c.visible_draws_buffer = wgpuDeviceCreateBuffer(device_, &vd_desc); - c.visible_draws_capacity = chunk_inst; - m.vram_bytes_ssbo += vd_desc.size; + chunk.visible_draws_buffer = wgpuDeviceCreateBuffer(device_, &vd_desc); + chunk.visible_draws_capacity = chunk_inst; + model_gpu_data.vram_bytes_ssbo += vd_desc.size; WGPUBufferDescriptor ps_desc = {}; ps_desc.size = std::max(ps_bytes, 16); ps_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; ps_desc.label = svFromCStr("model.chunk.prefix_sums"); - c.prefix_sums_buffer = wgpuDeviceCreateBuffer(device_, &ps_desc); - c.prefix_sums_capacity = chunk_inst + 1; - m.vram_bytes_ssbo += ps_desc.size; + chunk.prefix_sums_buffer = wgpuDeviceCreateBuffer(device_, &ps_desc); + chunk.prefix_sums_capacity = chunk_inst + 1; + model_gpu_data.vram_bytes_ssbo += ps_desc.size; WGPUBufferDescriptor mu_desc = {}; mu_desc.size = 16; mu_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; mu_desc.label = svFromCStr("model.chunk.uniform"); - c.per_chunk_uniform = wgpuDeviceCreateBuffer(device_, &mu_desc); - m.vram_bytes_ssbo += 16; + chunk.per_chunk_uniform = wgpuDeviceCreateBuffer(device_, &mu_desc); + model_gpu_data.vram_bytes_ssbo += 16; - c.visible_draws_scratch.reserve(chunk_inst); - c.prefix_sums_scratch.reserve(chunk_inst + 1); + chunk.visible_draws_scratch.reserve(chunk_inst); + chunk.prefix_sums_scratch.reserve(chunk_inst + 1); } // Index section is NOT loaded upfront. Each chunk's index slice is @@ -3074,115 +3093,116 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id, // MeshGpu storage (per-mesh quant basis). std::vector mesh_gpu; mesh_gpu.reserve(metadata.meta.meshes.size()); - for (const auto& mi : metadata.meta.meshes) { - MeshGpu mg = {}; - mg.aabb_min[0] = mi.local_aabb_min[0]; - mg.aabb_min[1] = mi.local_aabb_min[1]; - mg.aabb_min[2] = mi.local_aabb_min[2]; - mg.aabb_max[0] = mi.local_aabb_max[0]; - mg.aabb_max[1] = mi.local_aabb_max[1]; - mg.aabb_max[2] = mi.local_aabb_max[2]; - mesh_gpu.push_back(mg); + for (const auto& mesh_info : metadata.meta.meshes) { + MeshGpu mesh_gpu_record = {}; + mesh_gpu_record.aabb_min[0] = mesh_info.local_aabb_min[0]; + mesh_gpu_record.aabb_min[1] = mesh_info.local_aabb_min[1]; + mesh_gpu_record.aabb_min[2] = mesh_info.local_aabb_min[2]; + mesh_gpu_record.aabb_max[0] = mesh_info.local_aabb_max[0]; + mesh_gpu_record.aabb_max[1] = mesh_info.local_aabb_max[1]; + mesh_gpu_record.aabb_max[2] = mesh_info.local_aabb_max[2]; + mesh_gpu.push_back(mesh_gpu_record); } const std::size_t mesh_storage_bytes = mesh_gpu.size() * sizeof(MeshGpu); - m.mesh_storage = createBufferWithData( + model_gpu_data.mesh_storage = createBufferWithData( device_, queue_, mesh_gpu.data(), mesh_storage_bytes, WGPUBufferUsage_Storage, "model.mesh_storage"); - m.vram_bytes_ssbo += mesh_storage_bytes; + model_gpu_data.vram_bytes_ssbo += mesh_storage_bytes; // InstanceGpu storage. Rebase object_ids globally. const std::uint32_t object_id_base = next_object_id_; std::uint32_t max_local_id = 0; std::vector inst_gpu; inst_gpu.reserve(metadata.meta.instances.size()); - for (auto& ic : metadata.meta.instances) { - if (ic.object_id > max_local_id) max_local_id = ic.object_id; - ic.object_id = object_id_base + ic.object_id; - InstanceGpu ig = {}; - std::memcpy(ig.transform, ic.transform, sizeof(ig.transform)); - ig.object_id = ic.object_id; - ig.color_override_rgba8 = ic.color_override_rgba8; - ig.mesh_id = ic.mesh_id; - inst_gpu.push_back(ig); + for (auto& instance_cpu : metadata.meta.instances) { + if (instance_cpu.object_id > max_local_id) max_local_id = instance_cpu.object_id; + instance_cpu.object_id = object_id_base + instance_cpu.object_id; + InstanceGpu instance_gpu = {}; + std::memcpy(instance_gpu.transform, instance_cpu.transform, sizeof(instance_gpu.transform)); + instance_gpu.object_id = instance_cpu.object_id; + instance_gpu.color_override_rgba8 = instance_cpu.color_override_rgba8; + instance_gpu.mesh_id = instance_cpu.mesh_id; + inst_gpu.push_back(instance_gpu); } next_object_id_ = object_id_base + max_local_id + 1; - m.object_id_base = object_id_base; // deferred elements rebase to match + model_gpu_data.object_id_base = object_id_base; // deferred elements rebase to match const std::size_t inst_storage_bytes = inst_gpu.size() * sizeof(InstanceGpu); - m.instance_storage = createBufferWithData( + model_gpu_data.instance_storage = createBufferWithData( device_, queue_, inst_gpu.data(), inst_storage_bytes, WGPUBufferUsage_Storage, "model.instance_storage"); - m.vram_bytes_ssbo += inst_storage_bytes; + model_gpu_data.vram_bytes_ssbo += inst_storage_bytes; // Hand off CPU mirrors. - m.meshes = std::move(metadata.meta.meshes); - m.instances = std::move(metadata.meta.instances); + model_gpu_data.meshes = std::move(metadata.meta.meshes); + model_gpu_data.instances = std::move(metadata.meta.instances); // Streaming defers per-mesh vertex data until the owning chunk is // loaded. Both volumes + Area-tool CPU shadow fill in per-chunk // inside applyStreamedChunk as the bytes arrive. - m.mesh_local_volumes.assign(m.meshes.size(), 0.0); - m.mesh_triangles_cache.assign(m.meshes.size(), ModelGpuData::MeshTriangles{}); - m.mesh_has_alpha.assign(m.meshes.size(), std::uint8_t(0)); + model_gpu_data.mesh_local_volumes.assign(model_gpu_data.meshes.size(), 0.0); + model_gpu_data.mesh_triangles_cache.assign(model_gpu_data.meshes.size(), ModelGpuData::MeshTriangles{}); + model_gpu_data.mesh_has_alpha.assign(model_gpu_data.meshes.size(), std::uint8_t(0)); // object_id → instance index lookup. Volume tool reads it on every // selection mutation; per-pick latency stays O(K) instead of O(K*N). - m.object_id_to_instance.clear(); - m.object_id_to_instance.reserve(m.instances.size()); - for (std::uint32_t i = 0; i < std::uint32_t(m.instances.size()); ++i) { - m.object_id_to_instance.emplace(m.instances[i].object_id, i); + model_gpu_data.object_id_to_instance.clear(); + model_gpu_data.object_id_to_instance.reserve(model_gpu_data.instances.size()); + for (std::uint32_t i = 0; i < std::uint32_t(model_gpu_data.instances.size()); ++i) { + model_gpu_data.object_id_to_instance.emplace(model_gpu_data.instances[i].object_id, i); } // Per-chunk world AABBs + instance-id lists from instance_to_chunk. - for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) { - m.chunks[ci].instance_ids.reserve(m.instances.size() / m.chunks.size() + 4); + for (std::size_t chunk_index = 0; chunk_index < model_gpu_data.chunks.size(); ++chunk_index) { + model_gpu_data.chunks[chunk_index].instance_ids.reserve( + model_gpu_data.instances.size() / model_gpu_data.chunks.size() + 4); } - for (std::uint32_t inst_idx = 0; inst_idx < std::uint32_t(m.instances.size()); ++inst_idx) { - const auto& inst = m.instances[inst_idx]; - const std::uint32_t ci = instance_to_chunk[inst_idx]; - if (ci >= m.chunks.size()) continue; - auto& c = m.chunks[ci]; + for (std::uint32_t inst_idx = 0; inst_idx < std::uint32_t(model_gpu_data.instances.size()); ++inst_idx) { + const auto& inst = model_gpu_data.instances[inst_idx]; + const std::uint32_t chunk_index = instance_to_chunk[inst_idx]; + if (chunk_index >= model_gpu_data.chunks.size()) continue; + auto& chunk = model_gpu_data.chunks[chunk_index]; for (int a = 0; a < 3; ++a) { - c.aabb_min[a] = std::min(c.aabb_min[a], inst.world_aabb_min[a]); - c.aabb_max[a] = std::max(c.aabb_max[a], inst.world_aabb_max[a]); + chunk.aabb_min[a] = std::min(chunk.aabb_min[a], inst.world_aabb_min[a]); + chunk.aabb_max[a] = std::max(chunk.aabb_max[a], inst.world_aabb_max[a]); } - c.instance_ids.push_back(inst_idx); + chunk.instance_ids.push_back(inst_idx); } // Populate per-instance arrays from the per-chunk per-mesh offsets // computed during chunk construction. { - const std::size_t n_inst = m.instances.size(); - m.instance_chunk_idx.assign(n_inst, 0); - m.instance_base_vertex.assign(n_inst, 0); - m.instance_ebo_first_u32.assign(n_inst, 0); - m.instance_lod1_first_u32.assign(n_inst, 0); + const std::size_t n_inst = model_gpu_data.instances.size(); + model_gpu_data.instance_chunk_idx.assign(n_inst, 0); + model_gpu_data.instance_base_vertex.assign(n_inst, 0); + model_gpu_data.instance_ebo_first_u32.assign(n_inst, 0); + model_gpu_data.instance_lod1_first_u32.assign(n_inst, 0); for (std::size_t i = 0; i < n_inst; ++i) { - const std::uint32_t ci = instance_to_chunk[i]; - const std::uint32_t mi = m.instances[i].mesh_id; - if (ci >= chunk_mesh_offsets.size()) continue; - auto it_off = chunk_mesh_offsets[ci].find(mi); - if (it_off == chunk_mesh_offsets[ci].end()) continue; - m.instance_chunk_idx[i] = ci; - m.instance_base_vertex[i] = it_off->second.base_vertex; - m.instance_ebo_first_u32[i] = it_off->second.ebo_first; - m.instance_lod1_first_u32[i] = it_off->second.lod1_first; + const std::uint32_t chunk_index = instance_to_chunk[i]; + const std::uint32_t mesh_index = model_gpu_data.instances[i].mesh_id; + if (chunk_index >= chunk_mesh_offsets.size()) continue; + auto it_off = chunk_mesh_offsets[chunk_index].find(mesh_index); + if (it_off == chunk_mesh_offsets[chunk_index].end()) continue; + model_gpu_data.instance_chunk_idx[i] = chunk_index; + model_gpu_data.instance_base_vertex[i] = it_off->second.base_vertex; + model_gpu_data.instance_ebo_first_u32[i] = it_off->second.ebo_first; + model_gpu_data.instance_lod1_first_u32[i] = it_off->second.lod1_first; } } - auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m)); - ModelGpuData& mref = inserted->second; + auto [inserted, _] = models_gpu_.emplace(model_id, std::move(model_gpu_data)); + ModelGpuData& inserted_model = inserted->second; Log::info() << "[wgpu stream] applyCachedModel mid=" << model_id - << " verts=" << mref.vertex_bytes << "B (deferred)" - << " idx=" << mref.index_count - << " meshes=" << mref.mesh_count - << " instances=" << mref.instance_count - << " chunks=" << mref.chunks.size(); + << " verts=" << inserted_model.vertex_bytes << "B (deferred)" + << " idx=" << inserted_model.index_count + << " meshes=" << inserted_model.mesh_count + << " instances=" << inserted_model.instance_count + << " chunks=" << inserted_model.chunks.size(); if (!initial_view_applied_) { viewAll(); @@ -3208,10 +3228,10 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) { -std::numeric_limits::infinity(), -std::numeric_limits::infinity() }; for (std::size_t i = 0; i < n_verts; ++i) { - const float* v = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; + const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; for (int a = 0; a < 3; ++a) { - if (v[a] < bmin[a]) bmin[a] = v[a]; - if (v[a] > bmax[a]) bmax[a] = v[a]; + if (vertex[a] < bmin[a]) bmin[a] = vertex[a]; + if (vertex[a] > bmax[a]) bmax[a] = vertex[a]; } } float extent_recip[3]; @@ -3256,20 +3276,20 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) { void ViewportCore::uploadInstanceChunk(const InstanceChunk& chunk) { SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); - InstanceCpu inst{}; - inst.mesh_id = chunk.local_mesh_id; - inst.object_id = chunk.object_id; - inst.color_override_rgba8 = chunk.color_override_rgba8; - inst.model_id = chunk.model_id; - std::memcpy(inst.placement_transformation, chunk.transform, - sizeof(inst.placement_transformation)); + InstanceCpu instance{}; + instance.mesh_id = chunk.local_mesh_id; + instance.object_id = chunk.object_id; + instance.color_override_rgba8 = chunk.color_override_rgba8; + instance.model_id = chunk.model_id; + std::memcpy(instance.placement_transformation, chunk.transform, + sizeof(instance.placement_transformation)); for (int i = 0; i < 16; ++i) { - inst.transform[i] = float(chunk.transform[i]); + instance.transform[i] = float(chunk.transform[i]); } - std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); - std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); + std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min)); + std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max)); - s.instances.push_back(inst); + s.instances.push_back(instance); } std::uint32_t ViewportCore::loadSidecarFromPath(const std::string& path) { @@ -3746,16 +3766,16 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) { } std::unique_ptr staging_ptr = std::move(it->second); pending_direct_loads_.erase(it); - SidecarData& s = *staging_ptr; + SidecarData& sidecar_data = *staging_ptr; if (!device_ || !queue_) { Log::warn() << "[wgpu direct] finalizeModel without an initialised device"; return; } - if (s.meshes.empty() || s.instances.empty()) { + if (sidecar_data.meshes.empty() || sidecar_data.instances.empty()) { Log::info() << "[wgpu direct] finalizeModel(" << model_id - << "): empty staging (meshes=" << s.meshes.size() - << " instances=" << s.instances.size() << ")"; + << "): empty staging (meshes=" << sidecar_data.meshes.size() + << " instances=" << sidecar_data.instances.size() << ")"; return; } @@ -3765,7 +3785,7 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) { // that to skip these chunks (they're already resident after the // applyStreamedChunk loop below). StreamingSidecar metadata; - metadata.meta = std::move(s); + metadata.meta = std::move(sidecar_data); // Direct load: geometry is already in memory (uploaded below), streamed // from nothing — leave file_path empty so the streaming worker skips it. metadata.geometry_section_offset = 0; @@ -3783,38 +3803,40 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) { << "): applyCachedModel produced no model entry"; return; } - ModelGpuData& m = model_it->second; + ModelGpuData& model_gpu_data = model_it->second; // Gather each chunk's vertex + index bytes from the staged buffers. std::size_t chunks_uploaded = 0; - for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) { - auto& c = m.chunks[ci]; - if (c.mesh_ids.empty()) continue; + for (std::size_t chunk_index = 0; chunk_index < model_gpu_data.chunks.size(); ++chunk_index) { + auto& chunk = model_gpu_data.chunks[chunk_index]; + if (chunk.mesh_ids.empty()) continue; - std::vector vbytes(c.vertex_byte_size); - std::vector idx; - idx.reserve(c.index_count); + std::vector vbytes(chunk.vertex_byte_size); + std::vector indices; + indices.reserve(chunk.index_count); - for (std::uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = m.meshes[mi]; - const std::size_t vsz = std::size_t(mesh.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; - if (vsz > 0) { - const std::size_t dst_off = std::size_t(m.mesh_chunk_local_base_vertex[mi]) + for (std::uint32_t mesh_index : chunk.mesh_ids) { + const MeshInfo& mesh = model_gpu_data.meshes[mesh_index]; + const std::size_t vertex_byte_count = + std::size_t(mesh.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + if (vertex_byte_count > 0) { + const std::size_t destination_vertex_offset = + std::size_t(model_gpu_data.mesh_chunk_local_base_vertex[mesh_index]) * INSTANCED_VERTEX_STRIDE_BYTES; - std::memcpy(vbytes.data() + dst_off, - raw_vertices.data() + mesh.vbo_byte_offset, vsz); + std::memcpy(vbytes.data() + destination_vertex_offset, + raw_vertices.data() + mesh.vbo_byte_offset, vertex_byte_count); } if (mesh.index_count > 0) { const std::uint32_t* src = raw_indices.data() + (mesh.ebo_byte_offset / sizeof(std::uint32_t)); - idx.insert(idx.end(), src, src + mesh.index_count); + indices.insert(indices.end(), src, src + mesh.index_count); } } - if (!applyStreamedChunk(m, ci, vbytes, idx)) { + if (!applyStreamedChunk(model_gpu_data, chunk_index, vbytes, indices)) { Log::warn() << "[wgpu direct] finalizeModel(" << model_id - << "): applyStreamedChunk failed on chunk " << ci + << "): applyStreamedChunk failed on chunk " << chunk_index << " (pool OOM?)"; continue; } @@ -3823,9 +3845,9 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) { Log::info() << "[wgpu direct] finalizeModel mid=" << model_id - << " meshes=" << m.meshes.size() - << " instances=" << m.instances.size() - << " chunks=" << chunks_uploaded << "/" << m.chunks.size() + << " meshes=" << model_gpu_data.meshes.size() + << " instances=" << model_gpu_data.instances.size() + << " chunks=" << chunks_uploaded << "/" << model_gpu_data.chunks.size() << " verts=" << raw_vertices.size() << "B" << " idx=" << raw_indices.size(); } diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 95304248f4..4de8b188b3 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -192,6 +192,30 @@ public: enum class StandardView { Front, Back, Left, Right, Top, Bottom }; void setStandardView(StandardView view); + // ---- Navigation mouse bindings (shared, preset-driven) ------------------ + // + // Which mouse button (+ modifier) orbits / pans / selects. Owned by the core + // as pure data so BOTH hosts and ALL presets share one source of truth — the + // desktop maps these to Qt::MouseButton, the web to DOM button codes. Select + // is preset-driven too (not hardcoded to LMB) so a "web" preset can move it + // to RMB. Marquee box-select uses the same button as select (drag vs click). + enum class MouseBtn { Left, Middle, Right }; + // Plain (not "None": X11 #defines None to 0L, which would corrupt the token). + enum class NavMod { Plain, Shift, Ctrl, Alt }; + struct NavBindings { + MouseBtn orbit; NavMod orbit_mod; + MouseBtn pan; NavMod pan_mod; + MouseBtn select; NavMod select_mod; + }; + // name: "blender" (default) | "rhino" | "revit" | "web". Unknown → blender. + // blender orbit MMB, pan Shift+MMB, select LMB + // rhino orbit RMB, pan Shift+RMB, select LMB + // revit orbit Shift+MMB, pan MMB, select LMB + // web orbit LMB, pan MMB, select RMB (LMB stays free to + // orbit-drag; RMB click-selects / drag-marquees, no ambiguity) + void setNavPreset(const char* name); + const NavBindings& navBindings() const { return nav_bindings_; } + // Frame the current selection: union the selected objects' world AABBs and // fit the camera to them (same 1.30 padding as the desktop "F" hotkey). // No-op with an empty selection or no resolvable AABBs; returns whether it @@ -1151,6 +1175,10 @@ private: // Fly-camera move speed (m/s), wheel-adjustable via flyAdjustSpeed. Shared // by desktop + web fly mode; the mode flag itself lives in each host. float fly_move_speed_ = 5.0f; + // Nav mouse bindings; default matches the historical "blender" preset. + NavBindings nav_bindings_ = { MouseBtn::Middle, NavMod::Plain, + MouseBtn::Middle, NavMod::Shift, + MouseBtn::Left, NavMod::Plain }; // Perspective by default; toggleProjection (P key) flips this. When // true, buildViewProj uses an orthographic matrix sized by // camera_distance_ × tan(fov/2) so toggling looks like a smooth diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index cc201fdfaa..6f799568bd 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1533,21 +1533,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); } // ----------------------------------------------------------------------------- @@ -1633,15 +1644,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,7 +1671,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) { // 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; @@ -1702,7 +1713,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()); diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index ceefe60c35..497c2ed926 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -239,12 +239,15 @@ public: private: // Re-aim the orbit camera so the bounding sphere of [mn, mx] fits. void frameAabb(const float mn[3], const float mx[3], float padding); - // Resolve nav_preset_ env var to orbit/pan bindings. - void applyNavPreset(const char* name); // chunkScreenAreaPx moved to ViewportCore (#84-h). public: + // Apply a nav mouse preset by name ("blender"|"rhino"|"revit"|"web"). + // Sources the shared binding table from ViewportCore; called from init + // (env / persisted setting) and live from the Settings dialog. + void applyNavPreset(const char* name); + // Queue a one-shot framebuffer capture: the next rendered frame is // copied back to host memory and saved to `path` as PNG. If @@ -800,10 +803,15 @@ private: // so the click-vs-drag distinction at mouseReleaseEvent's pick path keeps // working. Set at init from WGPU_NAV_PRESET=blender|rhino|revit (default // blender, matching GL's AppSettings::NavPreset::Blender default). - Qt::MouseButton orbit_button_ = Qt::MiddleButton; - Qt::KeyboardModifiers orbit_mods_ = Qt::NoModifier; - Qt::MouseButton pan_button_ = Qt::MiddleButton; - Qt::KeyboardModifiers pan_mods_ = Qt::ShiftModifier; + // Mirror of ViewportCore's preset bindings, mapped to Qt types by + // applyNavPreset (the core owns the preset table; these are the Qt-side + // cache the mouse handlers compare against). + Qt::MouseButton orbit_button_ = Qt::MiddleButton; + Qt::KeyboardModifiers orbit_mods_ = Qt::NoModifier; + Qt::MouseButton pan_button_ = Qt::MiddleButton; + Qt::KeyboardModifiers pan_mods_ = Qt::ShiftModifier; + Qt::MouseButton select_button_ = Qt::LeftButton; + Qt::KeyboardModifiers select_mods_ = Qt::NoModifier; // Set by mousePressEvent based on which binding matched; consumed by // mouseMoveEvent so mid-drag modifier changes don't switch axes. enum class NavDrag : uint8_t { Inactive, Orbit, Pan }; diff --git a/src/ifcviewer/tests/test_viewport_camera.cpp b/src/ifcviewer/tests/test_viewport_camera.cpp index 47f28a6035..cca86eed37 100644 --- a/src/ifcviewer/tests/test_viewport_camera.cpp +++ b/src/ifcviewer/tests/test_viewport_camera.cpp @@ -143,6 +143,44 @@ TEST_CASE("toggleXray flips the active state", "[camera][xray]") { REQUIRE_FALSE(core.xrayActive()); } +TEST_CASE("setNavPreset maps names to the shared button bindings", "[camera][nav]") { + MockHost host; ViewportCore core(&host); + using B = ViewportCore::MouseBtn; using M = ViewportCore::NavMod; + + // Default is blender: orbit MMB, pan Shift+MMB, select LMB. + { + const auto& b = core.navBindings(); + REQUIRE(b.orbit == B::Middle); REQUIRE(b.orbit_mod == M::Plain); + REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Shift); + REQUIRE(b.select == B::Left); REQUIRE(b.select_mod == M::Plain); + } + SECTION("web: orbit LMB, pan MMB, select RMB") { + core.setNavPreset("web"); + const auto& b = core.navBindings(); + REQUIRE(b.orbit == B::Left); REQUIRE(b.orbit_mod == M::Plain); + REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Plain); + REQUIRE(b.select == B::Right); REQUIRE(b.select_mod == M::Plain); + } + SECTION("rhino: orbit RMB, pan Shift+RMB") { + core.setNavPreset("rhino"); + const auto& b = core.navBindings(); + REQUIRE(b.orbit == B::Right); REQUIRE(b.pan == B::Right); + REQUIRE(b.pan_mod == M::Shift); REQUIRE(b.select == B::Left); + } + SECTION("revit: orbit Shift+MMB, pan MMB") { + core.setNavPreset("revit"); + const auto& b = core.navBindings(); + REQUIRE(b.orbit == B::Middle); REQUIRE(b.orbit_mod == M::Shift); + REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Plain); + } + SECTION("unknown name falls back to blender") { + core.setNavPreset("web"); + core.setNavPreset("nonsense"); + const auto& b = core.navBindings(); + REQUIRE(b.orbit == B::Middle); REQUIRE(b.select == B::Left); + } +} + TEST_CASE("hideSelected hides the selection; showAll restores", "[camera][visibility]") { MockHost host; ViewportCore core(&host); REQUIRE(core.hiddenCount() == 0); From 8dfe00cdf87f2105446f387bdcd395819c0b8727 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 08:38:57 +1000 Subject: [PATCH 02/11] Improve viewer variable names Rename short local variables and parameters in the viewer loading, sidecar, and BonsaiViewer command paths to make their responsibilities clearer.\n\nGenerated with the assistance of an AI coding tool. --- src/bonsaiviewer/ElementRegistry.cpp | 38 +-- src/bonsaiviewer/ElementRegistry.h | 4 +- src/bonsaiviewer/SessionState.cpp | 16 +- .../modules/connectors/PickerDialog.cpp | 6 +- .../modules/connectors/Process.cpp | 4 +- .../modules/connectors/Registry.cpp | 26 +- src/bonsaiviewer/modules/models/Commands.cpp | 222 +++++++-------- src/bonsaiviewer/modules/models/Commands.h | 32 +-- .../modules/models/FederationItemModel.cpp | 4 +- src/bonsaiviewer/modules/models/Panel.cpp | 5 +- .../modules/models/SettingsDialog.cpp | 46 ++-- .../modules/models/SettingsView.cpp | 26 +- src/bonsaiviewer/modules/project/Commands.cpp | 252 +++++++++--------- src/bonsaiviewer/modules/project/Commands.h | 20 +- .../modules/viewport/Commands.cpp | 56 ++-- src/bonsaiviewer/modules/viewport/Commands.h | 26 +- src/bonsaiviewer/modules/viewport/View.cpp | 72 ++--- src/bonsaiviewer/modules/viewport/View.h | 8 +- src/ifcviewer-minimal/main.cpp | 7 +- src/ifcviewer/BufferPool.cpp | 105 ++++---- src/ifcviewer/GeometryStreamer.cpp | 88 +++--- src/ifcviewer/InstanceCompose.cpp | 18 +- src/ifcviewer/SidecarBuilder.cpp | 28 +- src/ifcviewer/SidecarCache.cpp | 241 ++++++++++------- src/ifcviewer/SidecarLayout.cpp | 100 +++---- src/ifcviewer/StreamingLoader.cpp | 22 +- src/ifcviewer/StreamingThread.cpp | 40 +-- 27 files changed, 785 insertions(+), 727 deletions(-) diff --git a/src/bonsaiviewer/ElementRegistry.cpp b/src/bonsaiviewer/ElementRegistry.cpp index d633e70f17..33ec5c8cdd 100644 --- a/src/bonsaiviewer/ElementRegistry.cpp +++ b/src/bonsaiviewer/ElementRegistry.cpp @@ -88,37 +88,37 @@ std::optional ElementRegistry::findEntity(uint32_t object_id) con } } -void ElementRegistry::onSidecarElementsReady(uint32_t /*mid*/, +void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/, std::vector elements, std::string string_table) { - auto str = [&](uint32_t offset, uint32_t length) -> QString { + auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString { if (length == 0 || offset + length > string_table.size()) return {}; return QString::fromStdString(string_table.substr(offset, length)); }; - for (const auto& pe : elements) { + for (const auto& packed_element : elements) { BasicElementInfo info; - info.object_id = pe.object_id; - info.model_id = pe.model_id; - info.ifc_id = pe.ifc_id; - info.parent_id = pe.parent_id; - info.guid = str(pe.guid_offset, pe.guid_length); - info.name = str(pe.name_offset, pe.name_length); - info.type = str(pe.type_offset, pe.type_length); + info.object_id = packed_element.object_id; + info.model_id = packed_element.model_id; + info.ifc_id = packed_element.ifc_id; + info.parent_id = packed_element.parent_id; + info.guid = string_from_table(packed_element.guid_offset, packed_element.guid_length); + info.name = string_from_table(packed_element.name_offset, packed_element.name_length); + info.type = string_from_table(packed_element.type_offset, packed_element.type_length); elements_[info.object_id] = info; } } -void ElementRegistry::onStreamedElementsReady(uint32_t /*mid*/, std::vector elements) { - for (const auto& e : elements) { +void ElementRegistry::onStreamedElementsReady(uint32_t /*model_id*/, std::vector elements) { + for (const auto& element : elements) { BasicElementInfo info; - info.object_id = e.object_id; - info.model_id = e.model_id; - info.ifc_id = e.ifc_id; - info.parent_id = e.parent_id; - info.guid = QString::fromStdString(e.guid); - info.name = QString::fromStdString(e.name); - info.type = QString::fromStdString(e.type); + info.object_id = element.object_id; + info.model_id = element.model_id; + info.ifc_id = element.ifc_id; + info.parent_id = element.parent_id; + info.guid = QString::fromStdString(element.guid); + info.name = QString::fromStdString(element.name); + info.type = QString::fromStdString(element.type); elements_[info.object_id] = info; } } diff --git a/src/bonsaiviewer/ElementRegistry.h b/src/bonsaiviewer/ElementRegistry.h index b804919f08..469e4b5996 100644 --- a/src/bonsaiviewer/ElementRegistry.h +++ b/src/bonsaiviewer/ElementRegistry.h @@ -58,10 +58,10 @@ public: std::optional findEntity(uint32_t object_id) const; private: - void onSidecarElementsReady(uint32_t mid, + void onSidecarElementsReady(uint32_t model_id, std::vector elements, std::string string_table); - void onStreamedElementsReady(uint32_t mid, std::vector elements); + void onStreamedElementsReady(uint32_t model_id, std::vector elements); SceneLoader* loader_ = nullptr; std::unordered_map elements_; diff --git a/src/bonsaiviewer/SessionState.cpp b/src/bonsaiviewer/SessionState.cpp index 91189e9680..4a5563e05d 100644 --- a/src/bonsaiviewer/SessionState.cpp +++ b/src/bonsaiviewer/SessionState.cpp @@ -64,25 +64,25 @@ void SessionState::createLoader(ViewportWindow* viewport) { }); connect(loader_, &SceneLoader::progressChanged, this, &SessionState::setProgress); connect(loader_, &SceneLoader::loadedFromSidecar, this, - [this, format_elapsed](uint32_t mid, qint64 elapsed_ms) { + [this, format_elapsed](uint32_t model_id, qint64 elapsed_ms) { setStatusMessage("Loaded", QString("%1 from cache in %2") - .arg(loader_->displayName(mid)) + .arg(loader_->displayName(model_id)) .arg(format_elapsed(elapsed_ms))); endProgress(); - emit modelGeometryReady(mid); + emit modelGeometryReady(model_id); }); connect(loader_, &SceneLoader::loadedFromStream, this, - [this, format_elapsed](uint32_t mid, qint64 elapsed_ms) { + [this, format_elapsed](uint32_t model_id, qint64 elapsed_ms) { setStatusMessage("Loaded", QString("%1 streamed in %2") - .arg(loader_->displayName(mid)) + .arg(loader_->displayName(model_id)) .arg(format_elapsed(elapsed_ms))); endProgress(); - emit modelGeometryReady(mid); + emit modelGeometryReady(model_id); }); - connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t mid) { - setStatusMessage("Cancelled", loader_->displayName(mid)); + connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t model_id) { + setStatusMessage("Cancelled", loader_->displayName(model_id)); endProgress(); }); connect(loader_, &SceneLoader::loadError, this, diff --git a/src/bonsaiviewer/modules/connectors/PickerDialog.cpp b/src/bonsaiviewer/modules/connectors/PickerDialog.cpp index 5da7dc0ec2..4cc11c4eef 100644 --- a/src/bonsaiviewer/modules/connectors/PickerDialog.cpp +++ b/src/bonsaiviewer/modules/connectors/PickerDialog.cpp @@ -60,9 +60,9 @@ ConnectorPickerDialog::ConnectorPickerDialog(const std::vectorsetSpacing(components::style::metrics::padding); QList buttons; - for (const auto& m : manifests) { - auto* button = components::buttons::makeButton(m.name, ":/icons/cloud-square.svg", choices); - const QString id = m.id; + for (const auto& manifest : manifests) { + auto* button = components::buttons::makeButton(manifest.name, ":/icons/cloud-square.svg", choices); + const QString id = manifest.id; connect(button, &QToolButton::clicked, this, [this, id]() { selected_id_ = id; accept(); diff --git a/src/bonsaiviewer/modules/connectors/Process.cpp b/src/bonsaiviewer/modules/connectors/Process.cpp index 1f27222391..b85e7d54f5 100644 --- a/src/bonsaiviewer/modules/connectors/Process.cpp +++ b/src/bonsaiviewer/modules/connectors/Process.cpp @@ -189,8 +189,8 @@ void ConnectorProcess::dispatchLine(const QByteArray& line) { void ConnectorProcess::failPendingAndClear(int code, const QString& message) { QHash snapshot; snapshot.swap(pending_); - for (const auto& p : snapshot) { - if (p.on_error) p.on_error(code, message); + for (const auto& pending_request : snapshot) { + if (pending_request.on_error) pending_request.on_error(code, message); } } diff --git a/src/bonsaiviewer/modules/connectors/Registry.cpp b/src/bonsaiviewer/modules/connectors/Registry.cpp index 1ec2c6ad86..1a244d8c1b 100644 --- a/src/bonsaiviewer/modules/connectors/Registry.cpp +++ b/src/bonsaiviewer/modules/connectors/Registry.cpp @@ -48,14 +48,14 @@ void ConnectorRegistry::refresh() { // exec changed under us. Surviving entries keep their running process. for (auto it = processes_.begin(); it != processes_.end();) { const QString id = it.key(); - ConnectorProcess* p = it.value(); - const ConnectorManifest* now = manifestFor(id); - const bool stale = !now || !p || - p->manifest().exec_path != now->exec_path; + ConnectorProcess* process = it.value(); + const ConnectorManifest* current_manifest = manifestFor(id); + const bool stale = !current_manifest || !process || + process->manifest().exec_path != current_manifest->exec_path; if (stale) { - if (p) { - p->shutdown(); - p->deleteLater(); + if (process) { + process->shutdown(); + process->deleteLater(); } it = processes_.erase(it); } else { @@ -65,8 +65,8 @@ void ConnectorRegistry::refresh() { } const ConnectorManifest* ConnectorRegistry::manifestFor(const QString& id) const { - for (const auto& m : manifests_) { - if (m.id == id) return &m; + for (const auto& manifest : manifests_) { + if (manifest.id == id) return &manifest; } return nullptr; } @@ -90,7 +90,7 @@ ConnectorProcess* ConnectorRegistry::get(const QString& id) { processes_.insert(id, proc); connect(proc, &ConnectorProcess::crashed, this, [this, id](const QString& message) { qWarning() << "ifcviewer connectors:" << message; - if (auto* p = processes_.take(id)) p->deleteLater(); + if (auto* process = processes_.take(id)) process->deleteLater(); }); return proc; } @@ -99,9 +99,9 @@ void ConnectorRegistry::shutdownAll() { const auto procs = processes_; processes_.clear(); for (auto it = procs.begin(); it != procs.end(); ++it) { - if (auto* p = it.value()) { - p->shutdown(); - p->deleteLater(); + if (auto* process = it.value()) { + process->shutdown(); + process->deleteLater(); } } } diff --git a/src/bonsaiviewer/modules/models/Commands.cpp b/src/bonsaiviewer/modules/models/Commands.cpp index 3736d68293..4aadb3ca5a 100644 --- a/src/bonsaiviewer/modules/models/Commands.cpp +++ b/src/bonsaiviewer/modules/models/Commands.cpp @@ -91,24 +91,24 @@ QString formatElapsed(qint64 ms) { } // namespace -void toggleVisibility(SessionState& s, ItemKind kind, const QString& id) { - Federation* fed = s.federation(); +void toggleVisibility(SessionState& session, ItemKind kind, const QString& id) { + Federation* federation = session.federation(); if (kind == ItemKind::Group) { - const Federation::Group* group = fed->findGroupById(id); + const Federation::Group* group = federation->findGroupById(id); if (!group) return; - fed->setGroupVisible(id, !group->visible); - s.notifyVisibilityChanged(); - s.setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown"); + federation->setGroupVisible(id, !group->visible); + session.notifyVisibilityChanged(); + session.setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown"); } else { - const Federation::Model* model = fed->findById(id); + const Federation::Model* model = federation->findById(id); if (!model) return; - fed->setModelVisible(id, !model->visible); - s.notifyVisibilityChanged(); - s.setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown"); + federation->setModelVisible(id, !model->visible); + session.notifyVisibilityChanged(); + session.setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown"); } } -void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id) { +void addGroup(SessionState& session, QWidget& host, const QString& parent_group_id) { bool ok = false; const QString name = QInputDialog::getText( &host, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok); @@ -116,13 +116,13 @@ void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id) { const QString trimmed = name.trimmed(); if (trimmed.isEmpty()) return; - s.federation()->addGroup(trimmed, parent_group_id); - s.notifyFederationChanged(); - s.setStatusMessage("Models", "Group added"); + session.federation()->addGroup(trimmed, parent_group_id); + session.notifyFederationChanged(); + session.setStatusMessage("Models", "Group added"); } -void renameGroup(SessionState& s, QWidget& host, const QString& group_id) { - const Federation::Group* group = s.federation()->findGroupById(group_id); +void renameGroup(SessionState& session, QWidget& host, const QString& group_id) { + const Federation::Group* group = session.federation()->findGroupById(group_id); if (!group) return; bool ok = false; @@ -132,27 +132,27 @@ void renameGroup(SessionState& s, QWidget& host, const QString& group_id) { const QString trimmed = name.trimmed(); if (trimmed.isEmpty()) return; - s.federation()->setGroupName(group_id, trimmed); - s.notifyFederationChanged(); - s.setStatusMessage("Models", "Group renamed"); + session.federation()->setGroupName(group_id, trimmed); + session.notifyFederationChanged(); + session.setStatusMessage("Models", "Group renamed"); } -void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id) { - s.federation()->setGroupParent(id, parent_group_id); - s.notifyFederationChanged(); - s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root" : "Group moved"); +void moveGroup(SessionState& session, const QString& id, const QString& parent_group_id) { + session.federation()->setGroupParent(id, parent_group_id); + session.notifyFederationChanged(); + session.setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root" : "Group moved"); } -void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id) { +void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id) { for (const auto& id : ids) { - s.federation()->setModelGroup(id, parent_group_id); + session.federation()->setModelGroup(id, parent_group_id); } - s.notifyFederationChanged(); - s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root" : "Model(s) moved"); + session.notifyFederationChanged(); + session.setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root" : "Model(s) moved"); } -void removeGroup(SessionState& s, QWidget& host, const QString& group_id) { - const Federation::Group* group = s.federation()->findGroupById(group_id); +void removeGroup(SessionState& session, QWidget& host, const QString& group_id) { + const Federation::Group* group = session.federation()->findGroupById(group_id); if (!group) return; const auto choice = QMessageBox::question( @@ -161,13 +161,13 @@ void removeGroup(SessionState& s, QWidget& host, const QString& group_id) { QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (choice != QMessageBox::Yes) return; - s.federation()->removeGroup(group_id); - s.notifyFederationChanged(); - s.setStatusMessage("Models", "Group removed"); + session.federation()->removeGroup(group_id); + session.notifyFederationChanged(); + session.setStatusMessage("Models", "Group removed"); } -void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id) { - const Federation::Model* model = s.federation()->findById(fed_id); +void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& fed_id) { + const Federation::Model* model = session.federation()->findById(fed_id); const QString label = model ? model->display_name : fed_id; const auto choice = QMessageBox::question( &host, "Remove Model", @@ -175,41 +175,41 @@ void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QStri QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (choice != QMessageBox::Yes) return; - const uint32_t mid = s.modelIdForFedId(fed_id); - if (mid == 0) { - s.federation()->removeModel(fed_id); - s.notifyFederationChanged(); - s.setStatusMessage("Models", "Model removed"); + const uint32_t model_id = session.modelIdForFedId(fed_id); + if (model_id == 0) { + session.federation()->removeModel(fed_id); + session.notifyFederationChanged(); + session.setStatusMessage("Models", "Model removed"); return; } - if (s.loader()->isLoadingModel(mid)) return; + if (session.loader()->isLoadingModel(model_id)) return; - vp.setSelectedObjectId(0); - s.setSelectedObjectId(0); - s.federation()->removeModel(fed_id); - vp.removeModel(mid); - s.loader()->removeModel(mid); - s.elementRegistry()->removeModel(mid); - s.removeModelMappingByFedId(fed_id); - s.notifySelectionChanged(); - s.notifyModelsChanged(); - s.setStatusMessage("Models", "Model removed"); + viewport.setSelectedObjectId(0); + session.setSelectedObjectId(0); + session.federation()->removeModel(fed_id); + viewport.removeModel(model_id); + session.loader()->removeModel(model_id); + session.elementRegistry()->removeModel(model_id); + session.removeModelMappingByFedId(fed_id); + session.notifySelectionChanged(); + session.notifyModelsChanged(); + session.setStatusMessage("Models", "Model removed"); } namespace detail { -void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) { +void loadModels(SessionState& session, const QStringList& paths, const QStringList& fed_ids) { if (paths.isEmpty()) return; - const auto ids = s.loader()->addFiles(paths); - for (int i = 0; i < paths.size() && i < static_cast(ids.size()) && i < fed_ids.size(); ++i) { - s.setModelMapping(fed_ids[i], ids[i]); + const auto model_ids = session.loader()->addFiles(paths); + for (int i = 0; i < paths.size() && i < static_cast(model_ids.size()) && i < fed_ids.size(); ++i) { + session.setModelMapping(fed_ids[i], model_ids[i]); } } } // namespace detail -void addModel(SessionState& s, QWidget& host) { +void addModel(SessionState& session, QWidget& host) { AddModelDialog dialog(&host); if (dialog.exec() != QDialog::Accepted) return; @@ -253,13 +253,13 @@ void addModel(SessionState& s, QWidget& host) { break; } case SourceMode::CloudModel: - addModelFromCloud(s, host); + addModelFromCloud(session, host); return; case SourceMode::ConvertToDatabase: - convertIfcToDatabase(s, host); + convertIfcToDatabase(session, host); return; case SourceMode::ExportGeometryDatabase: - exportGeometryDatabase(s, host); + exportGeometryDatabase(session, host); return; case SourceMode::None: return; @@ -270,24 +270,24 @@ void addModel(SessionState& s, QWidget& host) { // origin via ViewportView. Checked here (before federation->addModel) // because federation->addModel doesn't yet populate SessionState's // model mapping; modelIds() reflects pre-add state at this point. - if (s.modelIds().isEmpty()) { + if (session.modelIds().isEmpty()) { armFederatedFalseOriginGuess(); } QStringList accepted_paths; QStringList accepted_fed_ids; for (const auto& path : paths) { - const QString fed_id = s.federation()->addModel(path); + const QString fed_id = session.federation()->addModel(path); if (fed_id.isEmpty()) continue; accepted_paths << path; accepted_fed_ids << fed_id; } - detail::loadModels(s, accepted_paths, accepted_fed_ids); - s.notifyModelsChanged(); + detail::loadModels(session, accepted_paths, accepted_fed_ids); + session.notifyModelsChanged(); } -void addModelFromCloud(SessionState& s, QWidget& host) { - auto* registry = s.connectorRegistry(); +void addModelFromCloud(SessionState& session, QWidget& host) { + auto* registry = session.connectorRegistry(); const auto& manifests = registry->available(); if (manifests.empty()) { QMessageBox::information(&host, "Add From Cloud", @@ -310,9 +310,9 @@ void addModelFromCloud(SessionState& s, QWidget& host) { return; } - s.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id)); + session.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); proc->call("pull_models_interactive", QJsonValue(), [sguard, connector_id](const QJsonValue& result) { @@ -327,9 +327,9 @@ void addModelFromCloud(SessionState& s, QWidget& host) { QStringList paths; QStringList fed_ids; int added = 0; - for (const QJsonValue& v : arr) { - if (v.isNull() || !v.isObject()) continue; - const QJsonObject entry = v.toObject(); + for (const QJsonValue& value : arr) { + if (value.isNull() || !value.isObject()) continue; + const QJsonObject entry = value.toObject(); const QString display_name = entry.value("display_name").toString(); const QString path = entry.value("path").toString(); if (path.isEmpty()) continue; @@ -368,35 +368,35 @@ void addModelFromCloud(SessionState& s, QWidget& host) { namespace { // Shared "local path on disk" lookup for the right-click cloud commands: -// the loader keeps the path keyed by mid (set when a file or pull_models +// the loader keeps the path keyed by model_id (set when a file or pull_models // path was queued). Both local-sourced and resolved cloud-sourced models // have one; only un-resolved cloud models (where pull_models hasn't // returned yet) won't. -QString localPathForModel(SessionState& s, const QString& fed_id) { - const uint32_t mid = s.modelIdForFedId(fed_id); - if (mid == 0 || !s.loader()) return {}; - return s.loader()->filePath(mid); +QString localPathForModel(SessionState& session, const QString& fed_id) { + const uint32_t model_id = session.modelIdForFedId(fed_id); + if (model_id == 0 || !session.loader()) return {}; + return session.loader()->filePath(model_id); } } // namespace -void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id) { - auto* fed = s.federation(); - const Federation::Model* model = fed->findById(fed_id); +void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_id) { + auto* federation = session.federation(); + const Federation::Model* model = federation->findById(fed_id); if (!model) return; if (model->source_connector == "local") { QMessageBox::information(&host, "Save Model To Cloud", "This model has no cloud target. Use \"Save As To Cloud\" first."); return; } - const QString local_path = localPathForModel(s, fed_id); + const QString local_path = localPathForModel(session, fed_id); if (local_path.isEmpty()) { QMessageBox::warning(&host, "Save Model To Cloud", "Cannot find a local copy of this model to push."); return; } const QString connector_id = model->source_connector; - auto* registry = s.connectorRegistry(); + auto* registry = session.connectorRegistry(); auto* proc = registry->get(connector_id); if (!proc) { QMessageBox::warning(&host, "Save Model To Cloud", @@ -411,10 +411,10 @@ void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id) { params["path"] = local_path; params["source"] = source; - s.setStatusMessage("Cloud", + session.setStatusMessage("Cloud", QString("Saving %1 to %2...").arg(model->display_name, connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); proc->call("push_model", params, [sguard, fed_id, connector_id](const QJsonValue& result) { if (!sguard) return; @@ -438,17 +438,17 @@ void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id) { }); } -void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id) { - const Federation::Model* model = s.federation()->findById(fed_id); +void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed_id) { + const Federation::Model* model = session.federation()->findById(fed_id); if (!model) return; - const QString local_path = localPathForModel(s, fed_id); + const QString local_path = localPathForModel(session, fed_id); if (local_path.isEmpty()) { QMessageBox::warning(&host, "Save Model As To Cloud", "Cannot find a local copy of this model to push."); return; } - auto* registry = s.connectorRegistry(); + auto* registry = session.connectorRegistry(); const auto& manifests = registry->available(); if (manifests.empty()) { QMessageBox::information(&host, "Save Model As To Cloud", @@ -475,10 +475,10 @@ void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id) { QJsonObject params; params["path"] = local_path; - s.setStatusMessage("Cloud", + session.setStatusMessage("Cloud", QString("Pushing %1 to %2...").arg(model->display_name, connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); proc->call("push_model_interactive", params, [sguard, fed_id, connector_id](const QJsonValue& result) { if (!sguard) return; @@ -507,7 +507,7 @@ void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id) { }); } -void convertIfcToDatabase(SessionState& s, QWidget& host) { +void convertIfcToDatabase(SessionState& session, QWidget& host) { QFileDialog input_dialog(&host, "Select IFC File to Convert"); input_dialog.setFileMode(QFileDialog::ExistingFile); input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); @@ -545,10 +545,10 @@ void convertIfcToDatabase(SessionState& s, QWidget& host) { } } - s.beginProgress(QString("Converting %1 to %2…") + session.beginProgress(QString("Converting %1 to %2…") .arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); - s.setStatusMessage("Converting", + session.setStatusMessage("Converting", QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); auto timer = std::make_shared(); @@ -583,20 +583,20 @@ void convertIfcToDatabase(SessionState& s, QWidget& host) { }); QObject::connect(thread, &QThread::finished, &host, - [&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() { + [&session, host_ptr = &host, thread, timer, error_message, input_path, output_path]() { const qint64 elapsed = timer->elapsed(); - s.endProgress(); + session.endProgress(); thread->deleteLater(); if (!error_message->isEmpty()) { - s.setStatusMessage("Error", *error_message); + session.setStatusMessage("Error", *error_message); QMessageBox::warning(host_ptr, "Convert IFC to Database", QString("Conversion failed:\n%1").arg(*error_message)); return; } - s.setStatusMessage( + session.setStatusMessage( "Converted", QString("%1 → %2 in %3") .arg(QFileInfo(input_path).fileName(), @@ -609,7 +609,7 @@ void convertIfcToDatabase(SessionState& s, QWidget& host) { thread->start(); } -void exportGeometryDatabase(SessionState& s, QWidget& host) { +void exportGeometryDatabase(SessionState& session, QWidget& host) { QFileDialog input_dialog(&host, "Select IFC File to Export"); input_dialog.setFileMode(QFileDialog::ExistingFile); input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); @@ -637,10 +637,10 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { output_path += ".rdbview"; } - s.beginProgress(QString("Exporting %1 to %2…") + session.beginProgress(QString("Exporting %1 to %2…") .arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); - s.setStatusMessage("Exporting", + session.setStatusMessage("Exporting", QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); auto timer = std::make_shared(); @@ -695,13 +695,13 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { // Write to a sibling `.tmp` then rename so a partial file never // appears at the destination (matters for cloud-sync folders). - const QString tmp_zip = output_path + ".tmp"; - QFile::remove(tmp_zip); + const QString temporary_zip = output_path + ".tmp"; + QFile::remove(temporary_zip); { - QZipWriter writer(tmp_zip); + QZipWriter writer(temporary_zip); if (writer.status() != QZipWriter::NoError) { throw ifcopenshell::exception( - ("Failed to open " + tmp_zip + " for writing").toStdString()); + ("Failed to open " + temporary_zip + " for writing").toStdString()); } writer.setCompressionPolicy(QZipWriter::AutoCompress); @@ -731,15 +731,15 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { writer.close(); if (writer.status() != QZipWriter::NoError) { throw ifcopenshell::exception( - ("Failed to finalize " + tmp_zip).toStdString()); + ("Failed to finalize " + temporary_zip).toStdString()); } } QFile::remove(output_path); - if (!QFile::rename(tmp_zip, output_path)) { - QFile::remove(tmp_zip); + if (!QFile::rename(temporary_zip, output_path)) { + QFile::remove(temporary_zip); throw ifcopenshell::exception( - ("Failed to move " + tmp_zip + " to " + output_path).toStdString()); + ("Failed to move " + temporary_zip + " to " + output_path).toStdString()); } } catch (const std::exception& e) { *error_message = QString::fromUtf8(e.what()); @@ -751,20 +751,20 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { }); QObject::connect(thread, &QThread::finished, &host, - [&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() { + [&session, host_ptr = &host, thread, timer, error_message, input_path, output_path]() { const qint64 elapsed = timer->elapsed(); - s.endProgress(); + session.endProgress(); thread->deleteLater(); if (!error_message->isEmpty()) { - s.setStatusMessage("Error", *error_message); + session.setStatusMessage("Error", *error_message); QMessageBox::warning(host_ptr, "Export Geometry Database", QString("Export failed:\n%1").arg(*error_message)); return; } - s.setStatusMessage( + session.setStatusMessage( "Exported", QString("%1 → %2 in %3") .arg(QFileInfo(input_path).fileName(), @@ -777,8 +777,8 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { thread->start(); } -void openSettings(SessionState& s, QWidget& host) { - SettingsDialog dialog(&s, &host); +void openSettings(SessionState& session, QWidget& host) { + SettingsDialog dialog(&session, &host); dialog.exec(); } diff --git a/src/bonsaiviewer/modules/models/Commands.h b/src/bonsaiviewer/modules/models/Commands.h index 702e70eb82..9130508b04 100644 --- a/src/bonsaiviewer/modules/models/Commands.h +++ b/src/bonsaiviewer/modules/models/Commands.h @@ -52,35 +52,35 @@ namespace bonsaiviewer::modules::models::commands { // User-facing commands. Each one is responsible for emitting any notify() // signals exactly once, at the end of its execution. -void toggleVisibility(SessionState& s, ItemKind kind, const QString& id); -void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id); -void renameGroup(SessionState& s, QWidget& host, const QString& group_id); -void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id); -void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id); -void removeGroup(SessionState& s, QWidget& host, const QString& group_id); -void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id); -void addModel(SessionState& s, QWidget& host); +void toggleVisibility(SessionState& session, ItemKind kind, const QString& id); +void addGroup(SessionState& session, QWidget& host, const QString& parent_group_id); +void renameGroup(SessionState& session, QWidget& host, const QString& group_id); +void moveGroup(SessionState& session, const QString& id, const QString& parent_group_id); +void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id); +void removeGroup(SessionState& session, QWidget& host, const QString& group_id); +void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& fed_id); +void addModel(SessionState& session, QWidget& host); // Connector picker → pull_models_interactive → addCloudModel + load. // Reachable from AddModelDialog's CloudModel button; the underlying call // is async, so addModelFromCloud returns immediately after kicking it off. -void addModelFromCloud(SessionState& s, QWidget& host); +void addModelFromCloud(SessionState& session, QWidget& host); // push_model: push a cloud-sourced model back to its existing target. // Only valid when model.source_connector != "local". Async. -void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id); +void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_id); // push_model_interactive: pick a connector and push to a fresh cloud // target. Valid for any model (local or already cloud-sourced). Async. -void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id); -void convertIfcToDatabase(SessionState& s, QWidget& host); -void exportGeometryDatabase(SessionState& s, QWidget& host); -void openSettings(SessionState& s, QWidget& host); +void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed_id); +void convertIfcToDatabase(SessionState& session, QWidget& host); +void exportGeometryDatabase(SessionState& session, QWidget& host); +void openSettings(SessionState& session, QWidget& host); // Internal building blocks shared by commands here and by ProjectController. // These NEVER call notify*() — the caller is responsible for emitting once // at the end of its execution. namespace detail { -// Queues already-federated models on the loader and maps their fed-ids to mids. -void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids); +// Queues already-federated models on the loader and maps their federation-ids to mids. +void loadModels(SessionState& session, const QStringList& paths, const QStringList& fed_ids); } // namespace detail diff --git a/src/bonsaiviewer/modules/models/FederationItemModel.cpp b/src/bonsaiviewer/modules/models/FederationItemModel.cpp index fa7efca01f..8c206e24a3 100644 --- a/src/bonsaiviewer/modules/models/FederationItemModel.cpp +++ b/src/bonsaiviewer/modules/models/FederationItemModel.cpp @@ -164,8 +164,8 @@ void FederationItemModel::refreshSubtreeVisibility(QStandardItem* root) { const auto kind = static_cast(item->data(KindRole).toInt()); bool visible = true; if (kind == ItemKind::Group) { - const Federation::Group* g = federation_->findGroupById(id); - visible = g && g->visible; + const Federation::Group* group = federation_->findGroupById(id); + visible = group && group->visible; } else { visible = federation_->isModelEffectivelyVisible(id); } diff --git a/src/bonsaiviewer/modules/models/Panel.cpp b/src/bonsaiviewer/modules/models/Panel.cpp index ebda8d2a93..8567e6cca3 100644 --- a/src/bonsaiviewer/modules/models/Panel.cpp +++ b/src/bonsaiviewer/modules/models/Panel.cpp @@ -197,8 +197,9 @@ private: if (!target_index.isValid()) return true; if (kindOf(target_index) != ItemKind::Group) return false; if (group_id == target_group_id) return false; - for (QModelIndex cur = target_index; cur.isValid(); cur = cur.parent()) { - if (idOf(cur) == group_id) return false; + for (QModelIndex ancestor_index = target_index; ancestor_index.isValid(); + ancestor_index = ancestor_index.parent()) { + if (idOf(ancestor_index) == group_id) return false; } return true; } diff --git a/src/bonsaiviewer/modules/models/SettingsDialog.cpp b/src/bonsaiviewer/modules/models/SettingsDialog.cpp index 9f544ac9a3..a864aa674d 100644 --- a/src/bonsaiviewer/modules/models/SettingsDialog.cpp +++ b/src/bonsaiviewer/modules/models/SettingsDialog.cpp @@ -166,10 +166,10 @@ void SettingsDialog::setupUi() { federation_unit_form->setHorizontalSpacing(16); federation_unit_form->setVerticalSpacing(10); unit_combo_ = new QComboBox(federation_unit_body); - for (const auto& uc : kUnitChoices) { + for (const auto& unit_choice : kUnitChoices) { QStringList data; - data << QString::fromUtf8(uc.prefix) << QString::fromUtf8(uc.name); - unit_combo_->addItem(uc.label, data); + data << QString::fromUtf8(unit_choice.prefix) << QString::fromUtf8(unit_choice.name); + unit_combo_->addItem(unit_choice.label, data); } federation_unit_form->addRow("Unit", unit_combo_); federation_unit_section->addBodyWidget(unit_hint); @@ -341,13 +341,13 @@ void SettingsDialog::setupUi() { void SettingsDialog::syncFromFederation() { if (!federation_) return; - const auto& cfg = federation_->config(); + const auto& config = federation_->config(); int idx = -1; for (int i = 0; i < unit_combo_->count(); ++i) { const QStringList data = unit_combo_->itemData(i).toStringList(); if (data.size() == 2 && - data[0].toStdString() == cfg.unit_prefix && - data[1].toStdString() == cfg.unit_name) { + data[0].toStdString() == config.unit_prefix && + data[1].toStdString() == config.unit_name) { idx = i; break; } @@ -370,7 +370,7 @@ void SettingsDialog::populateModelTable() { int row = 0; for (const auto& model : federation_->models()) { - const auto& xf = model.model_transformation; + const auto& transformation = model.model_transformation; model_table_->insertRow(row); auto* model_item = new QTableWidgetItem(model.display_name.isEmpty() ? model.id : model.display_name); @@ -383,13 +383,13 @@ void SettingsDialog::populateModelTable() { widgets.frame = new QComboBox(model_table_); widgets.frame->addItem("Local", static_cast(AFrame::ModelLocal)); widgets.frame->addItem("Global", static_cast(AFrame::ModelGlobal)); - widgets.frame->setCurrentIndex(xf.a_frame == AFrame::ModelGlobal ? 1 : 0); + widgets.frame->setCurrentIndex(transformation.a_frame == AFrame::ModelGlobal ? 1 : 0); model_table_->setCellWidget(row, 1, widgets.frame); - widgets.from_point = new QTableWidgetItem(formatVector3(xf.a)); - widgets.to_point = new QTableWidgetItem(formatVector3(xf.b)); - widgets.rotate = new QTableWidgetItem(formatVector3(xf.rxyz_deg)); - widgets.pivot = new QTableWidgetItem(formatVector3(xf.pivot)); + widgets.from_point = new QTableWidgetItem(formatVector3(transformation.a)); + widgets.to_point = new QTableWidgetItem(formatVector3(transformation.b)); + widgets.rotate = new QTableWidgetItem(formatVector3(transformation.rxyz_deg)); + widgets.pivot = new QTableWidgetItem(formatVector3(transformation.pivot)); model_table_->setItem(row, 2, widgets.from_point); model_table_->setItem(row, 3, widgets.to_point); model_table_->setItem(row, 4, widgets.rotate); @@ -454,12 +454,12 @@ void SettingsDialog::updateSelectedModelGeoref() { void SettingsDialog::onAccepted() { if (federation_) { const QStringList data = unit_combo_->currentData().toStringList(); - FederationConfig cfg; + FederationConfig config; if (data.size() == 2) { - cfg.unit_prefix = data[0].toStdString(); - cfg.unit_name = data[1].toStdString(); + config.unit_prefix = data[0].toStdString(); + config.unit_name = data[1].toStdString(); } - federation_->setConfig(cfg); + federation_->setConfig(config); FederatedFalseOrigin origin; origin.xyz = Eigen::Vector3d(parseNumber(xyz_x_), parseNumber(xyz_y_), parseNumber(xyz_z_)); @@ -467,13 +467,13 @@ void SettingsDialog::onAccepted() { federation_->setFederatedFalseOrigin(origin); for (const auto& row : model_rows_) { - ModelTransformation xf; - xf.a_frame = static_cast(row.frame->currentData().toInt()); - xf.a = parseVector3(row.from_point->text()); - xf.b = parseVector3(row.to_point->text()); - xf.rxyz_deg = parseVector3(row.rotate->text()); - xf.pivot = parseVector3(row.pivot->text()); - federation_->setModelTransformation(row.fed_id, xf); + ModelTransformation transformation; + transformation.a_frame = static_cast(row.frame->currentData().toInt()); + transformation.a = parseVector3(row.from_point->text()); + transformation.b = parseVector3(row.to_point->text()); + transformation.rxyz_deg = parseVector3(row.rotate->text()); + transformation.pivot = parseVector3(row.pivot->text()); + federation_->setModelTransformation(row.fed_id, transformation); } if (session_state_) { session_state_->notifyFederationChanged(); diff --git a/src/bonsaiviewer/modules/models/SettingsView.cpp b/src/bonsaiviewer/modules/models/SettingsView.cpp index 31ef07f973..bfe9f2196a 100644 --- a/src/bonsaiviewer/modules/models/SettingsView.cpp +++ b/src/bonsaiviewer/modules/models/SettingsView.cpp @@ -39,12 +39,16 @@ QString formatNumber(double value) { QString formatAngleDms(double degrees) { const double absolute = std::fabs(degrees); - const int d = static_cast(absolute); - const double minutes_total = (absolute - static_cast(d)) * 60.0; - const int m = static_cast(minutes_total); - const double s = (minutes_total - static_cast(m)) * 60.0; + const int degree_part = static_cast(absolute); + const double minutes_total = (absolute - static_cast(degree_part)) * 60.0; + const int minute_part = static_cast(minutes_total); + const double second_part = (minutes_total - static_cast(minute_part)) * 60.0; const QString sign = degrees < 0.0 ? "-" : ""; - return QString("%1%2° %3' %4\"").arg(sign).arg(d).arg(m, 2, 10, QChar('0')).arg(formatNumber(s)); + return QString("%1%2° %3' %4\"") + .arg(sign) + .arg(degree_part) + .arg(minute_part, 2, 10, QChar('0')) + .arg(formatNumber(second_part)); } SelectedModelGeorefState unknownState(const QString& georef, const QString& type) { @@ -74,8 +78,8 @@ QString formatCachedUnitScale(double meters_per_unit) { std::string enumString(const attribute_value& av) { if (av.isNull()) return {}; if (av.type() != ifcopenshell::Argument_ENUMERATION) return {}; - enumeration_reference er = av; - return std::string(er.value() ? er.value() : ""); + enumeration_reference enumeration = av; + return std::string(enumeration.value() ? enumeration.value() : ""); } QString formatNamedUnit(const express::Base& unit) { @@ -224,18 +228,18 @@ void SettingsView::refresh(const QString& fed_id) const { return; } - const uint32_t mid = session_state_->modelIdForFedId(fed_id); - if (mid == 0) { + const uint32_t model_id = session_state_->modelIdForFedId(fed_id); + if (model_id == 0) { widget_->renderSelectedModelGeoref(unknownState("Not loaded", "No live model")); return; } - if (auto* ifc_file = loader->ifcFile(mid)) { + if (auto* ifc_file = loader->ifcFile(model_id)) { widget_->renderSelectedModelGeoref(stateFromLiveFile(ifc_file)); return; } - const ModelGeoref* georef = loader->modelGeoref(mid); + const ModelGeoref* georef = loader->modelGeoref(model_id); if (!georef) { widget_->renderSelectedModelGeoref(unknownState("Not available yet", "No data source")); return; diff --git a/src/bonsaiviewer/modules/project/Commands.cpp b/src/bonsaiviewer/modules/project/Commands.cpp index f0e626c7f2..7761fdb474 100644 --- a/src/bonsaiviewer/modules/project/Commands.cpp +++ b/src/bonsaiviewer/modules/project/Commands.cpp @@ -53,28 +53,28 @@ namespace { // Pure helper — clears the loaded scene without emitting any signals. The // caller (newProject / openProject) emits projectReset / projectOpened once // the whole flow finishes. -void clearScene(SessionState& s, ViewportWindow& vp) { - vp.setSelectedObjectId(0); - s.setSelectedObjectId(0); - for (uint32_t mid : s.modelIds()) { - vp.removeModel(mid); - s.loader()->removeModel(mid); +void clearScene(SessionState& session, ViewportWindow& viewport) { + viewport.setSelectedObjectId(0); + session.setSelectedObjectId(0); + for (uint32_t model_id : session.modelIds()) { + viewport.removeModel(model_id); + session.loader()->removeModel(model_id); } - s.clearModelMappings(); - s.elementRegistry()->clear(); + session.clearModelMappings(); + session.elementRegistry()->clear(); } // Returns false if the user cancelled (i.e. don't proceed with the destructive // op). Handles the Save → Discard → Cancel branch including a follow-on save. -bool confirmDiscardIfDirty(SessionState& s, QWidget& host) { - if (!s.federation()->isDirty()) return true; +bool confirmDiscardIfDirty(SessionState& session, QWidget& host) { + if (!session.federation()->isDirty()) return true; const auto result = QMessageBox::question( &host, "Unsaved Project", "The current project has unsaved changes. Save before continuing?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); if (result == QMessageBox::Cancel) return false; - if (result == QMessageBox::Save) return saveProject(s, host); + if (result == QMessageBox::Save) return saveProject(session, host); return true; } @@ -88,18 +88,18 @@ bool confirmDiscardIfDirty(SessionState& s, QWidget& host) { // - if no scene entry exists yet (initial open), queue a load. // Per spec, connector errors are not surfaced to the user; the connector // has already shown its own UI. -void resolveCloudModels(SessionState& s, ViewportWindow& vp) { - auto* fed = s.federation(); +void resolveCloudModels(SessionState& session, ViewportWindow& viewport) { + auto* federation = session.federation(); QHash connector_to_fed_ids; - for (const auto& m : fed->models()) { - if (m.source_connector == "local") continue; - connector_to_fed_ids[m.source_connector].push_back(m.id); + for (const auto& model : federation->models()) { + if (model.source_connector == "local") continue; + connector_to_fed_ids[model.source_connector].push_back(model.id); } if (connector_to_fed_ids.isEmpty()) return; - auto* registry = s.connectorRegistry(); - QPointer sguard(&s); - QPointer vguard(&vp); + auto* registry = session.connectorRegistry(); + QPointer sguard(&session); + QPointer vguard(&viewport); for (auto it = connector_to_fed_ids.constBegin(); it != connector_to_fed_ids.constEnd(); ++it) { @@ -115,13 +115,13 @@ void resolveCloudModels(SessionState& s, ViewportWindow& vp) { QJsonArray params; for (const QString& fed_id : fed_ids) { - const Federation::Model* m = fed->findById(fed_id); - if (!m) continue; - QJsonObject source = m->source_data; - source["connector"] = m->source_connector; + const Federation::Model* model = federation->findById(fed_id); + if (!model) continue; + QJsonObject source = model->source_data; + source["connector"] = model->source_connector; QJsonObject entry; - entry["display_name"] = m->display_name; - entry["id"] = m->id; + entry["display_name"] = model->display_name; + entry["id"] = model->id; entry["source"] = source; params.append(entry); } @@ -187,7 +187,7 @@ void resolveCloudModels(SessionState& s, ViewportWindow& vp) { it != connector_to_fed_ids.constEnd(); ++it) { total += it.value().size(); } - s.setStatusMessage("Cloud", QString("Resolving %1 model(s)...").arg(total)); + session.setStatusMessage("Cloud", QString("Resolving %1 model(s)...").arg(total)); } // Byte-equality check for "is this .ifcfed the same as what we have loaded?" @@ -203,29 +203,29 @@ bool isIfcfedUnchanged(const QString& current_path, const QString& candidate_pat return a.readAll() == b.readAll(); } -bool openProjectAt(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) { - SceneLoader* loader = s.loader(); +bool openProjectAt(SessionState& session, QWidget& host, ViewportWindow& viewport, const QString& path) { + SceneLoader* loader = session.loader(); if (loader && loader->isLoading()) { QMessageBox::information( &host, "Open Project", "Wait until the current model load finishes before opening another project."); return false; } - if (!confirmDiscardIfDirty(s, host)) return false; + if (!confirmDiscardIfDirty(session, host)) return false; QStringList warnings; QString err; - if (!s.federation()->load(path, &warnings, &err)) { + if (!session.federation()->load(path, &warnings, &err)) { QMessageBox::warning(&host, "Open Project", QString("Could not open project:\n%1").arg(err)); return false; } - clearScene(s, vp); + clearScene(session, viewport); QStringList paths; QStringList fed_ids; - for (const auto& model : s.federation()->models()) { + for (const auto& model : session.federation()->models()) { if (model.source_connector != "local") continue; if (!QFileInfo::exists(model.source_path)) { warnings << QString("Source not found, kept in project: %1").arg(model.source_path); @@ -234,58 +234,58 @@ bool openProjectAt(SessionState& s, QWidget& host, ViewportWindow& vp, const QSt paths << model.source_path; fed_ids << model.id; } - modules::models::commands::detail::loadModels(s, paths, fed_ids); + modules::models::commands::detail::loadModels(session, paths, fed_ids); if (!warnings.isEmpty()) { QMessageBox::warning(&host, "Open Project", "Project opened with warnings:\n\n" + warnings.join("\n")); } - s.federation()->markClean(); - if (s.federation()->hasHomeView()) { - const auto& hv = s.federation()->homeView(); - vp.setCamera(hv.target.x(), hv.target.y(), hv.target.z(), - hv.distance, hv.yaw, hv.pitch); + session.federation()->markClean(); + if (session.federation()->hasHomeView()) { + const auto& home_view = session.federation()->homeView(); + viewport.setCamera(home_view.target.x(), home_view.target.y(), home_view.target.z(), + home_view.distance, home_view.yaw, home_view.pitch); } - s.setStatusMessage("Project", QFileInfo(path).fileName()); - s.notifyProjectOpened(path); + session.setStatusMessage("Project", QFileInfo(path).fileName()); + session.notifyProjectOpened(path); - resolveCloudModels(s, vp); + resolveCloudModels(session, viewport); return true; } -bool saveProjectTo(SessionState& s, QWidget& host, const QString& path) { +bool saveProjectTo(SessionState& session, QWidget& host, const QString& path) { QString err; - if (!s.federation()->save(path, &err)) { + if (!session.federation()->save(path, &err)) { QMessageBox::warning(&host, "Save Project", QString("Could not save project:\n%1").arg(err)); return false; } - s.setStatusMessage("Project", QFileInfo(path).fileName()); - s.notifyProjectSaved(path); + session.setStatusMessage("Project", QFileInfo(path).fileName()); + session.notifyProjectSaved(path); return true; } } // namespace -bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp) { - SceneLoader* loader = s.loader(); +bool newProject(SessionState& session, QWidget& host, ViewportWindow& viewport) { + SceneLoader* loader = session.loader(); if (loader && loader->isLoading()) { QMessageBox::information( &host, "New Project", "Wait until the current model load finishes before creating a new project."); return false; } - if (!confirmDiscardIfDirty(s, host)) return false; + if (!confirmDiscardIfDirty(session, host)) return false; - clearScene(s, vp); - s.federation()->clear(); - s.setStatusMessage("Project", "Untitled"); - s.notifyProjectReset(); + clearScene(session, viewport); + session.federation()->clear(); + session.setStatusMessage("Project", "Untitled"); + session.notifyProjectReset(); return true; } -bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) { +bool openProject(SessionState& session, QWidget& host, ViewportWindow& viewport) { QFileDialog file_dialog(&host, "Open Project"); file_dialog.setFileMode(QFileDialog::ExistingFile); file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)"); @@ -294,25 +294,25 @@ bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) { const QString path = file_dialog.selectedFiles().value(0); if (path.isEmpty()) return false; - return openProjectAt(s, host, vp, path); + return openProjectAt(session, host, viewport, path); } -bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) { +bool openProjectPath(SessionState& session, QWidget& host, ViewportWindow& viewport, const QString& path) { if (path.isEmpty()) return false; - return openProjectAt(s, host, vp, path); + return openProjectAt(session, host, viewport, path); } -bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { - SceneLoader* loader = s.loader(); +bool openCloudProject(SessionState& session, QWidget& host, ViewportWindow& viewport) { + SceneLoader* loader = session.loader(); if (loader && loader->isLoading()) { QMessageBox::information( &host, "Open from Cloud", "Wait until the current model load finishes before opening another project."); return false; } - if (!confirmDiscardIfDirty(s, host)) return false; + if (!confirmDiscardIfDirty(session, host)) return false; - auto* registry = s.connectorRegistry(); + auto* registry = session.connectorRegistry(); const auto& manifests = registry->available(); if (manifests.empty()) { QMessageBox::information(&host, "Open from Cloud", @@ -336,12 +336,12 @@ bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { return false; } - s.beginProgress(QString("Opening project from %1...").arg(connector_id)); - s.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id)); + session.beginProgress(QString("Opening project from %1...").arg(connector_id)); + session.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); QPointer hguard(&host); - QPointer vguard(&vp); + QPointer vguard(&viewport); proc->call("pull_ifcfed_interactive", QJsonValue(), [sguard, hguard, vguard, connector_id](const QJsonValue& result) { @@ -371,16 +371,16 @@ bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { return true; } -bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { - auto* fed = s.federation(); +bool syncCloudProject(SessionState& session, QWidget& host, ViewportWindow& viewport) { + auto* federation = session.federation(); // Per spec, sync has two independent phases — refreshing the .ifcfed // (requires manifest) and refreshing cloud models (requires any // non-local source). Either is sufficient. - const bool has_manifest = fed->hasManifest(); + const bool has_manifest = federation->hasManifest(); bool has_cloud_models = false; - for (const auto& m : fed->models()) { - if (m.source_connector != "local") { has_cloud_models = true; break; } + for (const auto& model : federation->models()) { + if (model.source_connector != "local") { has_cloud_models = true; break; } } if (!has_manifest && !has_cloud_models) { QMessageBox::information(&host, "Sync From Cloud", @@ -388,7 +388,7 @@ bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { return false; } - SceneLoader* loader = s.loader(); + SceneLoader* loader = session.loader(); if (loader && loader->isLoading()) { QMessageBox::information(&host, "Sync From Cloud", "Wait until the current model load finishes before syncing."); @@ -399,23 +399,23 @@ bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { // skipped). Just refresh cloud-sourced models against the .ifcfed // already on disk. Federation state is preserved, so no dirty prompt. if (!has_manifest) { - s.setStatusMessage("Cloud", "Refreshing cloud models..."); - resolveCloudModels(s, vp); + session.setStatusMessage("Cloud", "Refreshing cloud models..."); + resolveCloudModels(session, viewport); return true; } // Manifest path: the .ifcfed itself may be replaced. Confirm dirty — // even though we'll attempt to preserve the session if the returned // .ifcfed is byte-equal, that's not known until after the round-trip. - if (!confirmDiscardIfDirty(s, host)) return false; + if (!confirmDiscardIfDirty(session, host)) return false; - const QString connector_id = fed->manifestConnectorId(); + const QString connector_id = federation->manifestConnectorId(); if (connector_id.isEmpty()) { QMessageBox::warning(&host, "Sync From Cloud", "The project's manifest does not name a connector."); return false; } - auto* registry = s.connectorRegistry(); + auto* registry = session.connectorRegistry(); auto* proc = registry->get(connector_id); if (!proc) { QMessageBox::warning(&host, "Sync From Cloud", @@ -424,15 +424,15 @@ bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { return false; } - s.beginProgress(QString("Syncing from %1...").arg(connector_id)); - s.setStatusMessage("Cloud", QString("Syncing from %1...").arg(connector_id)); + session.beginProgress(QString("Syncing from %1...").arg(connector_id)); + session.setStatusMessage("Cloud", QString("Syncing from %1...").arg(connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); QPointer hguard(&host); - QPointer vguard(&vp); - const QString current_path = fed->filePath(); + QPointer vguard(&viewport); + const QString current_path = federation->filePath(); - proc->call("pull_ifcfed", fed->manifest(), + proc->call("pull_ifcfed", federation->manifest(), [sguard, hguard, vguard, connector_id, current_path](const QJsonValue& result) { if (!sguard) return; sguard->endProgress(); @@ -475,13 +475,13 @@ bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) { return true; } -bool saveProject(SessionState& s, QWidget& host) { - if (s.federation()->filePath().isEmpty()) return saveProjectAs(s, host); - return saveProjectTo(s, host, s.federation()->filePath()); +bool saveProject(SessionState& session, QWidget& host) { + if (session.federation()->filePath().isEmpty()) return saveProjectAs(session, host); + return saveProjectTo(session, host, session.federation()->filePath()); } -bool saveProjectAs(SessionState& s, QWidget& host) { - QString suggested = s.federation()->filePath(); +bool saveProjectAs(SessionState& session, QWidget& host) { + QString suggested = session.federation()->filePath(); if (suggested.isEmpty()) suggested = "project.ifcfed"; QFileDialog file_dialog(&host, "Save Project As", suggested); @@ -494,7 +494,7 @@ bool saveProjectAs(SessionState& s, QWidget& host) { QString path = file_dialog.selectedFiles().value(0); if (path.isEmpty()) return false; if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed"; - return saveProjectTo(s, host, path); + return saveProjectTo(session, host, path); } namespace { @@ -507,7 +507,7 @@ struct TempProjectFile { QString path; }; -TempProjectFile writeProjectToTemp(SessionState& s, QWidget& host, const QString& op_title) { +TempProjectFile writeProjectToTemp(SessionState& session, QWidget& host, const QString& op_title) { TempProjectFile out; out.dir = std::make_shared(); if (!out.dir->isValid()) { @@ -517,12 +517,12 @@ TempProjectFile writeProjectToTemp(SessionState& s, QWidget& host, const QString out.dir.reset(); return out; } - const QString name = s.federation()->filePath().isEmpty() + const QString name = session.federation()->filePath().isEmpty() ? "project.ifcfed" - : QFileInfo(s.federation()->filePath()).fileName(); + : QFileInfo(session.federation()->filePath()).fileName(); const QString tmp_path = QDir(out.dir->path()).filePath(name); QString err; - if (!s.federation()->writeCopyTo(tmp_path, &err)) { + if (!session.federation()->writeCopyTo(tmp_path, &err)) { QMessageBox::warning(&host, op_title, QString("Failed to write temporary project:\n%1").arg(err)); out.dir.reset(); @@ -534,12 +534,12 @@ TempProjectFile writeProjectToTemp(SessionState& s, QWidget& host, const QString // Shared continuation for push_ifcfed[_interactive]: on success, repoint // Federation to the returned path and notify; on error, log + status. -void onPushIfcfedResult(SessionState& s, +void onPushIfcfedResult(SessionState& session, QWidget& host, const QString& op_title, const QString& connector_id, const QJsonValue& result) { - s.endProgress(); + session.endProgress(); const QString new_path = result.toObject().value("path").toString(); if (new_path.isEmpty()) { QMessageBox::warning(&host, op_title, @@ -547,24 +547,24 @@ void onPushIfcfedResult(SessionState& s, return; } QStringList warnings; - s.federation()->repointTo(new_path, &warnings); - s.setStatusMessage("Cloud", + session.federation()->repointTo(new_path, &warnings); + session.setStatusMessage("Cloud", QString("Saved to %1 via %2") .arg(QFileInfo(new_path).fileName(), connector_id)); - s.notifyProjectSaved(new_path); + session.notifyProjectSaved(new_path); } } // namespace -bool saveCloudProject(SessionState& s, QWidget& host) { - auto* fed = s.federation(); - if (!fed->hasManifest()) { +bool saveCloudProject(SessionState& session, QWidget& host) { + auto* federation = session.federation(); + if (!federation->hasManifest()) { QMessageBox::information(&host, "Save To Cloud", "This project has no cloud target. Use \"Save As To Cloud\" first."); return false; } - const QString connector_id = fed->manifestConnectorId(); - auto* registry = s.connectorRegistry(); + const QString connector_id = federation->manifestConnectorId(); + auto* registry = session.connectorRegistry(); auto* proc = registry->get(connector_id); if (!proc) { QMessageBox::warning(&host, "Save To Cloud", @@ -573,26 +573,26 @@ bool saveCloudProject(SessionState& s, QWidget& host) { return false; } - auto tmp = writeProjectToTemp(s, host, "Save To Cloud"); - if (!tmp.dir) return false; + auto temporary_project = writeProjectToTemp(session, host, "Save To Cloud"); + if (!temporary_project.dir) return false; QJsonObject params; - params["path"] = tmp.path; - params["manifest"] = fed->manifest(); + params["path"] = temporary_project.path; + params["manifest"] = federation->manifest(); - s.beginProgress(QString("Saving to %1...").arg(connector_id)); - s.setStatusMessage("Cloud", QString("Saving to %1...").arg(connector_id)); + session.beginProgress(QString("Saving to %1...").arg(connector_id)); + session.setStatusMessage("Cloud", QString("Saving to %1...").arg(connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); QPointer hguard(&host); proc->call("push_ifcfed", params, - [sguard, hguard, connector_id, tmp_keepalive = tmp.dir](const QJsonValue& result) { + [sguard, hguard, connector_id, tmp_keepalive = temporary_project.dir](const QJsonValue& result) { (void)tmp_keepalive; if (!sguard || !hguard) return; onPushIfcfedResult(*sguard, *hguard, "Save To Cloud", connector_id, result); }, - [sguard, connector_id, tmp_keepalive = tmp.dir](int code, const QString& message) { + [sguard, connector_id, tmp_keepalive = temporary_project.dir](int code, const QString& message) { (void)tmp_keepalive; qWarning() << "push_ifcfed to" << connector_id << "failed:" << code << message; @@ -605,8 +605,8 @@ bool saveCloudProject(SessionState& s, QWidget& host) { return true; } -bool saveAsCloudProject(SessionState& s, QWidget& host) { - auto* registry = s.connectorRegistry(); +bool saveAsCloudProject(SessionState& session, QWidget& host) { + auto* registry = session.connectorRegistry(); const auto& manifests = registry->available(); if (manifests.empty()) { QMessageBox::information(&host, "Save As To Cloud", @@ -629,25 +629,25 @@ bool saveAsCloudProject(SessionState& s, QWidget& host) { return false; } - auto tmp = writeProjectToTemp(s, host, "Save As To Cloud"); - if (!tmp.dir) return false; + auto temporary_project = writeProjectToTemp(session, host, "Save As To Cloud"); + if (!temporary_project.dir) return false; QJsonObject params; - params["path"] = tmp.path; + params["path"] = temporary_project.path; - s.beginProgress(QString("Pushing to %1...").arg(connector_id)); - s.setStatusMessage("Cloud", QString("Pushing to %1...").arg(connector_id)); + session.beginProgress(QString("Pushing to %1...").arg(connector_id)); + session.setStatusMessage("Cloud", QString("Pushing to %1...").arg(connector_id)); - QPointer sguard(&s); + QPointer sguard(&session); QPointer hguard(&host); proc->call("push_ifcfed_interactive", params, - [sguard, hguard, connector_id, tmp_keepalive = tmp.dir](const QJsonValue& result) { + [sguard, hguard, connector_id, tmp_keepalive = temporary_project.dir](const QJsonValue& result) { (void)tmp_keepalive; if (!sguard || !hguard) return; onPushIfcfedResult(*sguard, *hguard, "Save As To Cloud", connector_id, result); }, - [sguard, connector_id, tmp_keepalive = tmp.dir](int code, const QString& message) { + [sguard, connector_id, tmp_keepalive = temporary_project.dir](int code, const QString& message) { (void)tmp_keepalive; qWarning() << "push_ifcfed_interactive to" << connector_id << "failed:" << code << message; @@ -660,14 +660,14 @@ bool saveAsCloudProject(SessionState& s, QWidget& host) { return true; } -bool saveProjectDialog(SessionState& s, QWidget& host) { - SaveProjectDialog dialog(s.federation()->hasManifest(), &host); +bool saveProjectDialog(SessionState& session, QWidget& host) { + SaveProjectDialog dialog(session.federation()->hasManifest(), &host); if (dialog.exec() != QDialog::Accepted) return false; switch (dialog.selectedTarget()) { - case SaveTarget::Local: return saveProject(s, host); - case SaveTarget::LocalAs: return saveProjectAs(s, host); - case SaveTarget::Cloud: return saveCloudProject(s, host); - case SaveTarget::CloudAs: return saveAsCloudProject(s, host); + case SaveTarget::Local: return saveProject(session, host); + case SaveTarget::LocalAs: return saveProjectAs(session, host); + case SaveTarget::Cloud: return saveCloudProject(session, host); + case SaveTarget::CloudAs: return saveAsCloudProject(session, host); case SaveTarget::None: return false; } return false; diff --git a/src/bonsaiviewer/modules/project/Commands.h b/src/bonsaiviewer/modules/project/Commands.h index 12dccdc057..386076c116 100644 --- a/src/bonsaiviewer/modules/project/Commands.h +++ b/src/bonsaiviewer/modules/project/Commands.h @@ -32,35 +32,35 @@ namespace bonsaiviewer::modules::project::commands { // User-facing commands. Each owns its own dialogs and confirmations; each // emits exactly one notify() at the end (projectReset / projectOpened / // projectSaved) so views refresh once per command. -bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp); -bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp); +bool newProject(SessionState& session, QWidget& host, ViewportWindow& viewport); +bool openProject(SessionState& session, QWidget& host, ViewportWindow& viewport); // Open a specific .ifcfed by path, bypassing the file dialog. Used by the // "Open Recent" menu. Same dirty-check / load / cloud-resolve flow as // openProject; returns false if the load failed or was cancelled. -bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path); +bool openProjectPath(SessionState& session, QWidget& host, ViewportWindow& viewport, const QString& path); // Pick a connector, then call pull_ifcfed_interactive and open the resulting // .ifcfed as a fresh project. Non-local models in the loaded federation are // resolved asynchronously via pull_models. -bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp); +bool openCloudProject(SessionState& session, QWidget& host, ViewportWindow& viewport); // pull_ifcfed using the current project's .ifcfed.manifest. Re-downloads // the .ifcfed from the same cloud target it came from (typically without // user interaction), then opens it like a fresh project — discarding any // local edits after the usual dirty-check prompt. -bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp); -bool saveProject(SessionState& s, QWidget& host); -bool saveProjectAs(SessionState& s, QWidget& host); +bool syncCloudProject(SessionState& session, QWidget& host, ViewportWindow& viewport); +bool saveProject(SessionState& session, QWidget& host); +bool saveProjectAs(SessionState& session, QWidget& host); // Push the current federation to the cloud target named in its manifest // (push_ifcfed). No user prompt for destination. Caller is responsible for // gating this on Federation::hasManifest. -bool saveCloudProject(SessionState& s, QWidget& host); +bool saveCloudProject(SessionState& session, QWidget& host); // Pick a connector and push the current federation to a fresh cloud target // (push_ifcfed_interactive). The connector returns a new path + manifest; // Federation repoints to that location. -bool saveAsCloudProject(SessionState& s, QWidget& host); +bool saveAsCloudProject(SessionState& session, QWidget& host); // Show the four-way Save dialog (Local / Save As Local / To Cloud / Save // As To Cloud) and dispatch to one of the above. This is what the "Save // Project" ribbon button is wired to. -bool saveProjectDialog(SessionState& s, QWidget& host); +bool saveProjectDialog(SessionState& session, QWidget& host); } // namespace bonsaiviewer::modules::project::commands diff --git a/src/bonsaiviewer/modules/viewport/Commands.cpp b/src/bonsaiviewer/modules/viewport/Commands.cpp index fd8fa30dd5..a6166d07b0 100644 --- a/src/bonsaiviewer/modules/viewport/Commands.cpp +++ b/src/bonsaiviewer/modules/viewport/Commands.cpp @@ -26,8 +26,8 @@ namespace bonsaiviewer::modules::viewport::commands { -void setHome(SessionState& session, ViewportWindow& vp) { - auto camera = vp.cameraState(); +void setHome(SessionState& session, ViewportWindow& viewport) { + auto camera = viewport.cameraState(); Federation::HomeView home_view; home_view.target = camera.target; home_view.distance = camera.distance; @@ -37,66 +37,66 @@ void setHome(SessionState& session, ViewportWindow& vp) { session.setStatusMessage("Camera", "Home view updated"); } -void goHome(SessionState& session, ViewportWindow& vp) { +void goHome(SessionState& session, ViewportWindow& viewport) { Federation* federation = session.federation(); if (!federation->hasHomeView()) { session.setStatusMessage("Camera", "No home view set for this project"); return; } const auto& home_view = federation->homeView(); - vp.setCamera( + viewport.setCamera( home_view.target.x(), home_view.target.y(), home_view.target.z(), home_view.distance, home_view.yaw, home_view.pitch); session.setStatusMessage("Camera", "Home view restored"); } -void viewSelected(ViewportWindow& vp) { - vp.focusOnSelectedObject(); +void viewSelected(ViewportWindow& viewport) { + viewport.focusOnSelectedObject(); } -void fly(SessionState& session, ViewportWindow& vp) { - vp.requestActivate(); - vp.enterFpsMode(); +void fly(SessionState& session, ViewportWindow& viewport) { + viewport.requestActivate(); + viewport.enterFpsMode(); session.setStatusMessage("Mode", "Fly mode active"); } -void toggleSection(SessionState& session, ViewportWindow& vp) { - vp.toggleSectionTool(); +void toggleSection(SessionState& session, ViewportWindow& viewport) { + viewport.toggleSectionTool(); session.setStatusMessage("Section", - vp.sectionToolActive() ? "Section tool active" : "Section tool off"); + viewport.sectionToolActive() ? "Section tool active" : "Section tool off"); } -void clearSection(SessionState& session, ViewportWindow& vp) { - vp.clearSectionPlanes(); +void clearSection(SessionState& session, ViewportWindow& viewport) { + viewport.clearSectionPlanes(); session.setStatusMessage("Section", "Section planes cleared"); } -void toggleDistance(ViewportWindow& vp) { - vp.toggleLengthTool(); +void toggleDistance(ViewportWindow& viewport) { + viewport.toggleLengthTool(); } -void toggleArea(ViewportWindow& vp) { - vp.toggleAreaTool(); +void toggleArea(ViewportWindow& viewport) { + viewport.toggleAreaTool(); } -void toggleVolume(ViewportWindow& vp) { - vp.toggleVolumeTool(); +void toggleVolume(ViewportWindow& viewport) { + viewport.toggleVolumeTool(); } -void hideSelected(ViewportWindow& vp) { - vp.hideSelectedElements(); +void hideSelected(ViewportWindow& viewport) { + viewport.hideSelectedElements(); } -void isolateSelected(ViewportWindow& vp) { - vp.isolateSelectedElements(); +void isolateSelected(ViewportWindow& viewport) { + viewport.isolateSelectedElements(); } -void showAll(ViewportWindow& vp) { - vp.showAllElements(); +void showAll(ViewportWindow& viewport) { + viewport.showAllElements(); } -void invertVisibility(ViewportWindow& vp) { - vp.invertElementVisibility(); +void invertVisibility(ViewportWindow& viewport) { + viewport.invertElementVisibility(); } } // namespace bonsaiviewer::modules::viewport::commands diff --git a/src/bonsaiviewer/modules/viewport/Commands.h b/src/bonsaiviewer/modules/viewport/Commands.h index 0233873faa..dea82a32ee 100644 --- a/src/bonsaiviewer/modules/viewport/Commands.h +++ b/src/bonsaiviewer/modules/viewport/Commands.h @@ -26,22 +26,22 @@ namespace bonsaiviewer { class SessionState; } namespace bonsaiviewer::modules::viewport::commands { -void setHome(SessionState& session, ViewportWindow& vp); -void goHome(SessionState& session, ViewportWindow& vp); -void viewSelected(ViewportWindow& vp); +void setHome(SessionState& session, ViewportWindow& viewport); +void goHome(SessionState& session, ViewportWindow& viewport); +void viewSelected(ViewportWindow& viewport); -void fly(SessionState& session, ViewportWindow& vp); -void toggleSection(SessionState& session, ViewportWindow& vp); -void clearSection(SessionState& session, ViewportWindow& vp); +void fly(SessionState& session, ViewportWindow& viewport); +void toggleSection(SessionState& session, ViewportWindow& viewport); +void clearSection(SessionState& session, ViewportWindow& viewport); -void toggleDistance(ViewportWindow& vp); -void toggleArea(ViewportWindow& vp); -void toggleVolume(ViewportWindow& vp); +void toggleDistance(ViewportWindow& viewport); +void toggleArea(ViewportWindow& viewport); +void toggleVolume(ViewportWindow& viewport); -void hideSelected(ViewportWindow& vp); -void isolateSelected(ViewportWindow& vp); -void showAll(ViewportWindow& vp); -void invertVisibility(ViewportWindow& vp); +void hideSelected(ViewportWindow& viewport); +void isolateSelected(ViewportWindow& viewport); +void showAll(ViewportWindow& viewport); +void invertVisibility(ViewportWindow& viewport); } // namespace bonsaiviewer::modules::viewport::commands diff --git a/src/bonsaiviewer/modules/viewport/View.cpp b/src/bonsaiviewer/modules/viewport/View.cpp index cf9140bf31..1f49941f71 100644 --- a/src/bonsaiviewer/modules/viewport/View.cpp +++ b/src/bonsaiviewer/modules/viewport/View.cpp @@ -67,9 +67,9 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state, // first geometry-ready consumes the arm). refresh() stays terminal — // any federation mutation from the guess propagates through // SessionState's federatedFalseOriginChanged relay. - connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t mid) { + connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t model_id) { if (modules::models::consumeFederatedFalseOriginGuess()) { - guessFederatedFalseOriginFromFirstModel(mid); + guessFederatedFalseOriginFromFirstModel(model_id); } refresh(); }); @@ -136,34 +136,34 @@ void ViewportView::refresh() { viewport_->setFederatedFalseOrigin( composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config())); - for (uint32_t mid : session_state_->modelIds()) { - applyCoordinateOperation(mid); - applyModelVisibility(mid); + for (uint32_t model_id : session_state_->modelIds()) { + applyCoordinateOperation(model_id); + applyModelVisibility(model_id); } } -void ViewportView::applyCoordinateOperation(uint32_t mid) { +void ViewportView::applyCoordinateOperation(uint32_t model_id) { SceneLoader* loader = session_state_->loader(); Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity(); - if (const ModelGeoref* georef = loader->modelGeoref(mid)) { + if (const ModelGeoref* georef = loader->modelGeoref(model_id)) { if (georef->has_coordinate_operation) { matrix = georef->coordinate_operation_meters; } } - viewport_->setModelCoordinateOperation(mid, matrix); - applyModelTransformation(mid); + viewport_->setModelCoordinateOperation(model_id, matrix); + applyModelTransformation(model_id); } -void ViewportView::applyModelTransformation(uint32_t mid) { +void ViewportView::applyModelTransformation(uint32_t model_id) { Federation* federation = session_state_->federation(); SceneLoader* loader = session_state_->loader(); Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity(); - const QString fed_id = session_state_->fedIdForModelId(mid); + const QString fed_id = session_state_->fedIdForModelId(model_id); if (!fed_id.isEmpty()) { if (const Federation::Model* model = federation->findById(fed_id)) { ModelUnits units; Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity(); - if (const ModelGeoref* georef = loader->modelGeoref(mid)) { + if (const ModelGeoref* georef = loader->modelGeoref(model_id)) { units = georef->units; if (georef->has_coordinate_operation) { coordinate_operation = georef->coordinate_operation_meters; @@ -173,18 +173,18 @@ void ViewportView::applyModelTransformation(uint32_t mid) { model->model_transformation, federation->config(), units, coordinate_operation); } } - viewport_->setModelTransformation(mid, matrix); + viewport_->setModelTransformation(model_id, matrix); } -void ViewportView::applyModelVisibility(uint32_t mid) { +void ViewportView::applyModelVisibility(uint32_t model_id) { Federation* federation = session_state_->federation(); - const QString fed_id = session_state_->fedIdForModelId(mid); + const QString fed_id = session_state_->fedIdForModelId(model_id); if (fed_id.isEmpty()) return; if (federation->isModelEffectivelyVisible(fed_id)) { - viewport_->showModel(mid); + viewport_->showModel(model_id); } else { - viewport_->hideModel(mid); + viewport_->hideModel(model_id); } } @@ -209,7 +209,7 @@ void ViewportView::applyModelVisibility(uint32_t mid) { // mutation here propagates through SessionState's federation relay // (federatedFalseOriginChanged → notifyFederationChanged) without // re-entering this function. -void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t mid) { +void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t model_id) { Federation* federation = session_state_->federation(); if (!federation->filePath().isEmpty()) return; @@ -218,10 +218,10 @@ void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t mid) { if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return; Eigen::Vector3d first_geometry_point_m; - if (!viewport_->firstGeometryPointWorldM(mid, first_geometry_point_m)) return; + if (!viewport_->firstGeometryPointWorldM(model_id, first_geometry_point_m)) return; SceneLoader* loader = session_state_->loader(); - const ModelGeoref* georef = loader->modelGeoref(mid); + const ModelGeoref* georef = loader->modelGeoref(model_id); if (georef == nullptr) return; federation->setFederatedFalseOrigin(::guessFederatedFalseOrigin( @@ -236,42 +236,42 @@ void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t mid) { // (0,0,0) — the federated false origin in render space — capped at // 100 m so a model with crazy-coord geometry can't pull the camera // back into nothing. - viewport_->frameOnFederatedOrigin(mid, 100.0f); + viewport_->frameOnFederatedOrigin(model_id, 100.0f); } void ViewportView::updateVolumeReadout() { if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return; - const auto& sel = viewport_->selection().selectionIds(); - if (sel.empty()) { + const auto& selection_ids = viewport_->selection().selectionIds(); + if (selection_ids.empty()) { viewport_->setHudText(std::string()); viewport_->setOverlayLabels({}); return; } - std::vector ids(sel.begin(), sel.end()); - const auto per_obj = volumesPerObject(*viewport_, ids); + std::vector object_ids(selection_ids.begin(), selection_ids.end()); + const auto volumes_by_object = volumesPerObject(*viewport_, object_ids); double total = 0.0; std::vector labels; - labels.reserve(per_obj.size()); - for (const auto& [oid, v] : per_obj) { - total += v; + labels.reserve(volumes_by_object.size()); + for (const auto& [object_id, volume] : volumes_by_object) { + total += volume; Eigen::Vector3f mn, mx; - if (!viewport_->computeObjectAabb(oid, mn, mx)) continue; + if (!viewport_->computeObjectAabb(object_id, mn, mx)) continue; OverlayRenderer::Label lbl; - const Eigen::Vector3f c = (mn + mx) * 0.5f; - lbl.world_pos[0] = c.x(); - lbl.world_pos[1] = c.y(); - lbl.world_pos[2] = c.z(); - lbl.text = QString::number(v, 'f', 4) + " m³"; + const Eigen::Vector3f center = (mn + mx) * 0.5f; + lbl.world_pos[0] = center.x(); + lbl.world_pos[1] = center.y(); + lbl.world_pos[2] = center.z(); + lbl.text = QString::number(volume, 'f', 4) + " m³"; labels.push_back(std::move(lbl)); } viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)") .arg(total, 0, 'f', 4) - .arg(per_obj.size()) - .arg(per_obj.size() == 1 ? "" : "s").toStdString()); + .arg(volumes_by_object.size()) + .arg(volumes_by_object.size() == 1 ? "" : "s").toStdString()); viewport_->setOverlayLabels(labels); } diff --git a/src/bonsaiviewer/modules/viewport/View.h b/src/bonsaiviewer/modules/viewport/View.h index 56e356afc4..196b4b4cff 100644 --- a/src/bonsaiviewer/modules/viewport/View.h +++ b/src/bonsaiviewer/modules/viewport/View.h @@ -50,10 +50,10 @@ public: private: void refresh(); - void applyCoordinateOperation(uint32_t mid); - void applyModelTransformation(uint32_t mid); - void applyModelVisibility(uint32_t mid); - void guessFederatedFalseOriginFromFirstModel(uint32_t mid); + void applyCoordinateOperation(uint32_t model_id); + void applyModelTransformation(uint32_t model_id); + void applyModelVisibility(uint32_t model_id); + void guessFederatedFalseOriginFromFirstModel(uint32_t model_id); void updateVolumeReadout(); bonsaiviewer::SessionState* session_state_ = nullptr; diff --git a/src/ifcviewer-minimal/main.cpp b/src/ifcviewer-minimal/main.cpp index bb059b8798..42551be469 100644 --- a/src/ifcviewer-minimal/main.cpp +++ b/src/ifcviewer-minimal/main.cpp @@ -82,10 +82,11 @@ int main(int argc, char* argv[]) { const QStringList parts = parser.value("camera").split(','); if (parts.size() == 6) { bool ok = true; - float v[6]; - for (int i = 0; i < 6 && ok; ++i) v[i] = parts[i].toFloat(&ok); + float camera_values[6]; + for (int i = 0; i < 6 && ok; ++i) camera_values[i] = parts[i].toFloat(&ok); if (ok) { - viewport->setCamera(v[0], v[1], v[2], v[3], v[4], v[5]); + viewport->setCamera(camera_values[0], camera_values[1], camera_values[2], + camera_values[3], camera_values[4], camera_values[5]); } else { Log::warn() << "--camera: failed to parse " << parser.value("camera"); diff --git a/src/ifcviewer/BufferPool.cpp b/src/ifcviewer/BufferPool.cpp index 2dfde93b3e..bdf533587b 100644 --- a/src/ifcviewer/BufferPool.cpp +++ b/src/ifcviewer/BufferPool.cpp @@ -41,8 +41,8 @@ void BufferPool::configure(WGPUInstance instance, WGPUDevice device, } void BufferPool::destroy() { - for (auto& sp : sub_pools_) { - if (sp.buffer) wgpuBufferRelease(sp.buffer); + for (auto& sub_pool : sub_pools_) { + if (sub_pool.buffer) wgpuBufferRelease(sub_pool.buffer); } sub_pools_.clear(); device_ = nullptr; @@ -140,7 +140,7 @@ bool BufferPool::addSubBuffer() { WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc); struct PopResult { bool done = false; bool error = false; }; - auto pop = [&](PopResult& pr) { + auto pop = [&](PopResult& pop_result) { WGPUPopErrorScopeCallbackInfo pcb = {}; pcb.mode = WGPUCallbackMode_AllowProcessEvents; pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, @@ -149,9 +149,9 @@ bool BufferPool::addSubBuffer() { p->done = true; p->error = (type != WGPUErrorType_NoError); }; - pcb.userdata1 = ≺ + pcb.userdata1 = &pop_result; wgpuDevicePopErrorScope(device_, pcb); - while (!pr.done) wgpuInstanceProcessEvents(instance_); + while (!pop_result.done) wgpuInstanceProcessEvents(instance_); }; PopResult oom_pop, validation_pop; pop(oom_pop); @@ -205,11 +205,12 @@ void BufferPool::resolveProvisionalGrowth(bool failed) { (unsigned long long)(total_capacity_bytes() / (1024 * 1024)), sub_pools_.size()); } else { - sub_pools_[i].provisional = false; + SubPool& sub_pool = sub_pools_[i]; + sub_pool.provisional = false; std::fprintf(stderr, "[wgpu pool] added sub-buffer %zu (%llu MB); pool total now %llu MB\n", i, - (unsigned long long)(sub_pools_[i].capacity / (1024 * 1024)), + (unsigned long long)(sub_pool.capacity / (1024 * 1024)), (unsigned long long)(total_capacity_bytes() / (1024 * 1024))); } return; @@ -225,34 +226,34 @@ BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) { // adding another sub-buffer and retry once. for (int attempt = 0; attempt < 2; ++attempt) { for (size_t sp_idx = 0; sp_idx < sub_pools_.size(); ++sp_idx) { - SubPool& sp = sub_pools_[sp_idx]; + SubPool& sub_pool = sub_pools_[sp_idx]; // Web: never allocate out of a sub-buffer still awaiting OOM // validation — its handle may be a Dawn error buffer. - if (sp.provisional) continue; - for (size_t i = 0; i < sp.free_ranges.size(); ++i) { - const FreeRange& r = sp.free_ranges[i]; - const uint64_t aligned = (r.offset + (align - 1)) & ~(align - 1); - const uint64_t pad = aligned - r.offset; - if (pad >= r.size) continue; - if (size > r.size - pad) continue; + if (sub_pool.provisional) continue; + for (size_t i = 0; i < sub_pool.free_ranges.size(); ++i) { + const FreeRange& free_range = sub_pool.free_ranges[i]; + const uint64_t aligned = (free_range.offset + (align - 1)) & ~(align - 1); + const uint64_t alignment_padding = aligned - free_range.offset; + if (alignment_padding >= free_range.size) continue; + if (size > free_range.size - alignment_padding) continue; const uint64_t post_off = aligned + size; - const uint64_t post_size = (r.offset + r.size) - post_off; + const uint64_t post_size = (free_range.offset + free_range.size) - post_off; - if (pad == 0 && post_size == 0) { - sp.free_ranges.erase(sp.free_ranges.begin() + i); - } else if (pad == 0) { - sp.free_ranges[i] = {post_off, post_size}; + if (alignment_padding == 0 && post_size == 0) { + sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i); + } else if (alignment_padding == 0) { + sub_pool.free_ranges[i] = {post_off, post_size}; } else if (post_size == 0) { - sp.free_ranges[i] = {r.offset, pad}; + sub_pool.free_ranges[i] = {free_range.offset, alignment_padding}; } else { - sp.free_ranges[i] = {r.offset, pad}; - sp.free_ranges.insert(sp.free_ranges.begin() + i + 1, + sub_pool.free_ranges[i] = {free_range.offset, alignment_padding}; + sub_pool.free_ranges.insert(sub_pool.free_ranges.begin() + i + 1, {post_off, post_size}); } - sp.used += size; - out.buffer = sp.buffer; + sub_pool.used += size; + out.buffer = sub_pool.buffer; out.offset = aligned; out.size = size; out.sub_idx = int(sp_idx); @@ -270,50 +271,56 @@ BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) { void BufferPool::free(const Slice& s) { if (!s.valid()) return; if (s.sub_idx < 0 || size_t(s.sub_idx) >= sub_pools_.size()) return; - SubPool& sp = sub_pools_[size_t(s.sub_idx)]; - assert(s.offset + s.size <= sp.capacity); + SubPool& sub_pool = sub_pools_[size_t(s.sub_idx)]; + assert(s.offset + s.size <= sub_pool.capacity); size_t i = 0; - while (i < sp.free_ranges.size() && sp.free_ranges[i].offset < s.offset) ++i; - sp.free_ranges.insert(sp.free_ranges.begin() + i, {s.offset, s.size}); - sp.used -= s.size; + while (i < sub_pool.free_ranges.size() && sub_pool.free_ranges[i].offset < s.offset) ++i; + sub_pool.free_ranges.insert(sub_pool.free_ranges.begin() + i, {s.offset, s.size}); + sub_pool.used -= s.size; - if (i + 1 < sp.free_ranges.size() - && sp.free_ranges[i].offset + sp.free_ranges[i].size == sp.free_ranges[i + 1].offset) { - sp.free_ranges[i].size += sp.free_ranges[i + 1].size; - sp.free_ranges.erase(sp.free_ranges.begin() + i + 1); + if (i + 1 < sub_pool.free_ranges.size() + && sub_pool.free_ranges[i].offset + sub_pool.free_ranges[i].size + == sub_pool.free_ranges[i + 1].offset) { + sub_pool.free_ranges[i].size += sub_pool.free_ranges[i + 1].size; + sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i + 1); } if (i > 0 - && sp.free_ranges[i - 1].offset + sp.free_ranges[i - 1].size == sp.free_ranges[i].offset) { - sp.free_ranges[i - 1].size += sp.free_ranges[i].size; - sp.free_ranges.erase(sp.free_ranges.begin() + i); + && sub_pool.free_ranges[i - 1].offset + sub_pool.free_ranges[i - 1].size + == sub_pool.free_ranges[i].offset) { + sub_pool.free_ranges[i - 1].size += sub_pool.free_ranges[i].size; + sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i); } } uint64_t BufferPool::total_capacity_bytes() const { - uint64_t s = 0; + uint64_t total_capacity = 0; // Skip provisional sub-pools (web, awaiting OOM validation) — their // capacity isn't usable yet, so counting it would mislead the // evictor's "is there room?" heuristics. - for (const auto& sp : sub_pools_) if (!sp.provisional) s += sp.capacity; - return s; + for (const auto& sub_pool : sub_pools_) { + if (!sub_pool.provisional) total_capacity += sub_pool.capacity; + } + return total_capacity; } uint64_t BufferPool::total_used_bytes() const { - uint64_t s = 0; - for (const auto& sp : sub_pools_) if (!sp.provisional) s += sp.used; - return s; + uint64_t total_used = 0; + for (const auto& sub_pool : sub_pools_) { + if (!sub_pool.provisional) total_used += sub_pool.used; + } + return total_used; } uint64_t BufferPool::largest_free_run_bytes() const { - uint64_t m = 0; - for (const auto& sp : sub_pools_) { - if (sp.provisional) continue; - for (const auto& r : sp.free_ranges) { - if (r.size > m) m = r.size; + uint64_t largest_free_run = 0; + for (const auto& sub_pool : sub_pools_) { + if (sub_pool.provisional) continue; + for (const auto& free_range : sub_pool.free_ranges) { + if (free_range.size > largest_free_run) largest_free_run = free_range.size; } } - return m; + return largest_free_run; } void BufferPool::addSubBufferForTesting(WGPUBuffer fake_buffer, uint64_t capacity) { diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 1acd88023d..ff08aad348 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -42,29 +42,29 @@ struct MaterialInfo { }; static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) { - MaterialInfo m; - if (!style) return m; + MaterialInfo material; + if (!style) return material; const auto& color = style->get_color(); if (color) { - m.r = static_cast(color.r()); - m.g = static_cast(color.g()); - m.b = static_cast(color.b()); + material.r = static_cast(color.r()); + material.g = static_cast(color.g()); + material.b = static_cast(color.b()); } if (!std::isnan(style->transparency)) { - m.a = 1.0f - static_cast(style->transparency); + material.a = 1.0f - static_cast(style->transparency); } - return m; + return material; } -static inline uint32_t packRGBA8(const MaterialInfo& m) { - auto to_byte = [](float v) -> uint32_t { - float c = std::clamp(v, 0.0f, 1.0f); - return static_cast(c * 255.0f + 0.5f); +static inline uint32_t packRGBA8(const MaterialInfo& material) { + auto to_byte = [](float channel_value) -> uint32_t { + float clamped_value = std::clamp(channel_value, 0.0f, 1.0f); + return static_cast(clamped_value * 255.0f + 0.5f); }; - uint32_t r = to_byte(m.r); - uint32_t g = to_byte(m.g); - uint32_t b = to_byte(m.b); - uint32_t a = to_byte(m.a); + uint32_t r = to_byte(material.r); + uint32_t g = to_byte(material.g); + uint32_t b = to_byte(material.b); + uint32_t a = to_byte(material.a); // Little-endian byte layout [r,g,b,a] for GL_UNSIGNED_BYTE * 4 normalized. return r | (g << 8) | (b << 16) | (a << 24); } @@ -188,12 +188,12 @@ static MeshChunk buildMeshChunk(uint32_t model_id, chunk.indices.reserve(faces.size()); // Track local AABB as we emit vertices. - float amin[3] = { std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max() }; - float amax[3] = { -std::numeric_limits::max(), - -std::numeric_limits::max(), - -std::numeric_limits::max() }; + float local_aabb_min[3] = { std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max() }; + float local_aabb_max[3] = { -std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max() }; auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t { const uint64_t key = make_key(orig_idx, mat_id); @@ -211,9 +211,12 @@ static MeshChunk buildMeshChunk(uint32_t model_id, chunk.vertices.push_back(px); chunk.vertices.push_back(py); chunk.vertices.push_back(pz); - if (px < amin[0]) amin[0] = px; if (px > amax[0]) amax[0] = px; - if (py < amin[1]) amin[1] = py; if (py > amax[1]) amax[1] = py; - if (pz < amin[2]) amin[2] = pz; if (pz > amax[2]) amax[2] = pz; + if (px < local_aabb_min[0]) local_aabb_min[0] = px; + if (px > local_aabb_max[0]) local_aabb_max[0] = px; + if (py < local_aabb_min[1]) local_aabb_min[1] = py; + if (py > local_aabb_max[1]) local_aabb_max[1] = py; + if (pz < local_aabb_min[2]) local_aabb_min[2] = pz; + if (pz > local_aabb_max[2]) local_aabb_max[2] = pz; if (orig_idx * 3 + 2 < normals.size()) { chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 0])); @@ -246,11 +249,11 @@ static MeshChunk buildMeshChunk(uint32_t model_id, } if (chunk.vertices.empty()) { - for (int a = 0; a < 3; ++a) amin[a] = amax[a] = 0.0f; + for (int a = 0; a < 3; ++a) local_aabb_min[a] = local_aabb_max[a] = 0.0f; } for (int a = 0; a < 3; ++a) { - chunk.local_aabb_min[a] = amin[a]; - chunk.local_aabb_max[a] = amax[a]; + chunk.local_aabb_min[a] = local_aabb_min[a]; + chunk.local_aabb_max[a] = local_aabb_max[a]; } return chunk; } @@ -527,7 +530,6 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what())); return false; } - if (!iterator->initialize()) { // No geometry survived this context for the remaining ids. // Subsequent contexts will pick them up; nothing to emit. @@ -604,15 +606,15 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { MeshChunk mesh_chunk = buildMeshChunk(model_id_, local_mesh_id, tri_elem, offset); - MeshAabb ma; + MeshAabb mesh_aabb; for (int a = 0; a < 3; ++a) { - ma.lmin[a] = mesh_chunk.local_aabb_min[a]; - ma.lmax[a] = mesh_chunk.local_aabb_max[a]; - ma.offset[a] = offset[a]; + mesh_aabb.lmin[a] = mesh_chunk.local_aabb_min[a]; + mesh_aabb.lmax[a] = mesh_chunk.local_aabb_max[a]; + mesh_aabb.offset[a] = offset[a]; } - ma.has_offset = (offset.squaredNorm() > 0.0); + mesh_aabb.has_offset = (offset.squaredNorm() > 0.0); if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1); - mesh_aabbs[local_mesh_id] = ma; + mesh_aabbs[local_mesh_id] = mesh_aabb; if (!mesh_chunk.indices.empty()) { emit meshReady(std::move(mesh_chunk)); } @@ -626,11 +628,11 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { Eigen::Matrix4d mat_d = tri_elem->transformation().data()->ccomponents(); if (mesh_aabbs[local_mesh_id].has_offset) { - const Eigen::Vector3d off( + const Eigen::Vector3d mesh_rebase_offset( mesh_aabbs[local_mesh_id].offset[0], mesh_aabbs[local_mesh_id].offset[1], mesh_aabbs[local_mesh_id].offset[2]); - mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * off; + mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * mesh_rebase_offset; } InstanceChunk inst; @@ -642,25 +644,25 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { inst.transform[i] = mat_d.data()[i]; } - const MeshAabb& ma = mesh_aabbs[local_mesh_id]; + const MeshAabb& mesh_aabb = mesh_aabbs[local_mesh_id]; float mat_f[16]; for (int i = 0; i < 16; ++i) { mat_f[i] = static_cast(inst.transform[i]); } - worldAabbFromLocal(ma.lmin, ma.lmax, mat_f, + worldAabbFromLocal(mesh_aabb.lmin, mesh_aabb.lmax, mat_f, inst.world_aabb_min, inst.world_aabb_max); emit instanceReady(std::move(inst)); total_shapes++; yielded_count++; - const int p = total_count > 0 + const int progress_percent = total_count > 0 ? static_cast((100 * yielded_count) / total_count) : 100; - if (p != last_emitted_progress) { - last_emitted_progress = p; - progress_ = p; - emit progressChanged(p); + if (progress_percent != last_emitted_progress) { + last_emitted_progress = progress_percent; + progress_ = progress_percent; + emit progressChanged(progress_percent); } } while (iterator->next()); diff --git a/src/ifcviewer/InstanceCompose.cpp b/src/ifcviewer/InstanceCompose.cpp index 13e6069007..bd6ce53af8 100644 --- a/src/ifcviewer/InstanceCompose.cpp +++ b/src/ifcviewer/InstanceCompose.cpp @@ -79,16 +79,16 @@ bool findInstanceInModels( const std::unordered_map& models, InstanceLookup& out) { if (object_id == 0) return false; - for (const auto& [mid, m] : models) { - auto it = m.object_id_to_instance.find(object_id); - if (it == m.object_id_to_instance.end()) continue; - const uint32_t inst_idx = it->second; - if (inst_idx >= m.instances.size()) continue; - const InstanceCpu& inst = m.instances[inst_idx]; - out.model_id = mid; - out.mesh_id = inst.mesh_id; + for (const auto& [model_id, model_data] : models) { + auto it = model_data.object_id_to_instance.find(object_id); + if (it == model_data.object_id_to_instance.end()) continue; + const uint32_t instance_index = it->second; + if (instance_index >= model_data.instances.size()) continue; + const InstanceCpu& instance = model_data.instances[instance_index]; + out.model_id = model_id; + out.mesh_id = instance.mesh_id; std::memcpy(out.placement_transformation, - inst.placement_transformation, + instance.placement_transformation, sizeof(out.placement_transformation)); return true; } diff --git a/src/ifcviewer/SidecarBuilder.cpp b/src/ifcviewer/SidecarBuilder.cpp index ebafab97bb..c9b19d32bf 100644 --- a/src/ifcviewer/SidecarBuilder.cpp +++ b/src/ifcviewer/SidecarBuilder.cpp @@ -53,10 +53,10 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) { -std::numeric_limits::infinity(), -std::numeric_limits::infinity() }; for (size_t i = 0; i < n_verts; ++i) { - const float* v = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; + const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; for (int a = 0; a < 3; ++a) { - if (v[a] < bmin[a]) bmin[a] = v[a]; - if (v[a] > bmax[a]) bmax[a] = v[a]; + if (vertex[a] < bmin[a]) bmin[a] = vertex[a]; + if (vertex[a] > bmax[a]) bmax[a] = vertex[a]; } } float extent_recip[3]; @@ -99,25 +99,25 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) { } void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { - InstanceCpu inst; - inst.mesh_id = chunk.local_mesh_id; - inst.object_id = chunk.object_id; - inst.color_override_rgba8 = chunk.color_override_rgba8; - inst.model_id = chunk.model_id; + InstanceCpu instance; + instance.mesh_id = chunk.local_mesh_id; + instance.object_id = chunk.object_id; + instance.color_override_rgba8 = chunk.color_override_rgba8; + instance.model_id = chunk.model_id; // The streamer's chunk.transform is the double-precision // placement_transformation. The cached float transform/world_aabb is only // an identity-stage baseline; applyCachedModel recomposes from placement // against the consumer's stage matrices at load time. - std::memcpy(inst.placement_transformation, chunk.transform, - sizeof(inst.placement_transformation)); + std::memcpy(instance.placement_transformation, chunk.transform, + sizeof(instance.placement_transformation)); for (int i = 0; i < 16; ++i) { - inst.transform[i] = static_cast(chunk.transform[i]); + instance.transform[i] = static_cast(chunk.transform[i]); } - std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); - std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); + std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min)); + std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max)); - sidecar_data_.instances.push_back(inst); + sidecar_data_.instances.push_back(instance); } SidecarData SidecarBuilder::finalize(const ModelGeoref& georef, diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index 666097bdc3..3ff1a98b24 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -59,50 +59,61 @@ static constexpr int kSidecarZstdLevel = 19; // --- In-memory serialisation (a block is built in RAM, then compressed) ------ template -static void appendVec(std::vector& b, const std::vector& v) { - std::uint32_t n = static_cast(v.size()); - const auto* np = reinterpret_cast(&n); - b.insert(b.end(), np, np + 4); - if (n > 0) { - const auto* p = reinterpret_cast(v.data()); - b.insert(b.end(), p, p + std::size_t(sizeof(T)) * n); +static void appendVec(std::vector& buffer, const std::vector& values) { + std::uint32_t count = static_cast(values.size()); + const auto* count_bytes = reinterpret_cast(&count); + buffer.insert(buffer.end(), count_bytes, count_bytes + 4); + if (count > 0) { + const auto* value_bytes = reinterpret_cast(values.data()); + buffer.insert(buffer.end(), value_bytes, value_bytes + std::size_t(sizeof(T)) * count); } } -static void appendBytes(std::vector& b, const void* p, std::size_t n) { - const auto* c = static_cast(p); - b.insert(b.end(), c, c + n); +static void appendBytes(std::vector& buffer, const void* data, std::size_t byte_count) { + const auto* bytes = static_cast(data); + buffer.insert(buffer.end(), bytes, bytes + byte_count); } // Pull one chunk's geometry out of the whole-model vertex/index arrays into the // chunk-LOCAL layout applyStreamedChunk expects: vertices of its meshes in chunk // order, then indices as LOD0 (per mesh) followed by LOD1 (per mesh). -static void extractChunkGeometry(const SidecarData& d, const SidecarChunk& c, +static void extractChunkGeometry(const SidecarData& sidecar_data, const SidecarChunk& sidecar_chunk, std::vector& vbytes, std::vector& ibytes) { vbytes.clear(); ibytes.clear(); - const std::uint32_t end = c.first_mesh + c.mesh_count; - for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) { - const MeshInfo& m = d.meshes[mi]; - const std::size_t voff = m.vbo_byte_offset; - const std::size_t vn = std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; - if (voff + vn <= d.vertices.size()) - vbytes.insert(vbytes.end(), d.vertices.begin() + voff, - d.vertices.begin() + voff + vn); + const std::uint32_t end = sidecar_chunk.first_mesh + sidecar_chunk.mesh_count; + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < sidecar_data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index]; + const std::size_t vertex_offset = mesh_info.vbo_byte_offset; + const std::size_t vertex_byte_count = + std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + if (vertex_offset + vertex_byte_count <= sidecar_data.vertices.size()) + vbytes.insert(vbytes.end(), sidecar_data.vertices.begin() + vertex_offset, + sidecar_data.vertices.begin() + vertex_offset + vertex_byte_count); } auto appendIdx = [&](std::size_t first_u32, std::size_t count) { - if (first_u32 + count > d.indices.size()) return; - const auto* p = reinterpret_cast(d.indices.data() + first_u32); - ibytes.insert(ibytes.end(), p, p + count * sizeof(std::uint32_t)); + if (first_u32 + count > sidecar_data.indices.size()) return; + const auto* index_bytes = + reinterpret_cast(sidecar_data.indices.data() + first_u32); + ibytes.insert(ibytes.end(), index_bytes, index_bytes + count * sizeof(std::uint32_t)); }; - for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) { - const MeshInfo& m = d.meshes[mi]; - if (m.index_count) appendIdx(m.ebo_byte_offset / sizeof(std::uint32_t), m.index_count); + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < sidecar_data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index]; + if (mesh_info.index_count) { + appendIdx(mesh_info.ebo_byte_offset / sizeof(std::uint32_t), mesh_info.index_count); + } } - for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) { - const MeshInfo& m = d.meshes[mi]; - if (m.lod1_index_count) - appendIdx(m.lod1_ebo_byte_offset / sizeof(std::uint32_t), m.lod1_index_count); + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < sidecar_data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index]; + if (mesh_info.lod1_index_count) { + appendIdx(mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t), mesh_info.lod1_index_count); + } } } #endif // !__EMSCRIPTEN__ (bake-only serialisation helpers) @@ -118,14 +129,14 @@ struct SidecarHeader { // foo.ifcdb -> foo.ifcview // foo (no ext) -> foo.ifcview static std::string sidecarPath(const std::string& ifc_path) { - std::string p = ifc_path; - while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back(); - auto slash = p.find_last_of("/\\"); - auto dot = p.find_last_of('.'); + std::string path = ifc_path; + while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.pop_back(); + auto slash = path.find_last_of("/\\"); + auto dot = path.find_last_of('.'); std::string stem = (dot != std::string::npos && (slash == std::string::npos || dot > slash)) - ? p.substr(0, dot) - : p; + ? path.substr(0, dot) + : path; return stem + ".ifcview"; } @@ -152,18 +163,18 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { FILE* f = fopen(path.c_str(), "wb"); if (!f) return false; - auto wr = [&](const void* p, std::size_t n) { - return fwrite(p, 1, n, f) == n; + auto write_bytes = [&](const void* data, std::size_t byte_count) { + return fwrite(data, 1, byte_count, f) == byte_count; }; - auto wrU64 = [&](std::uint64_t v) { return wr(&v, sizeof(v)); }; + auto wrU64 = [&](std::uint64_t v) { return write_bytes(&v, sizeof(v)); }; auto wrBlock = [&](const std::vector& raw) -> bool { auto z = SidecarCompress::compress(raw.data(), raw.size(), kSidecarZstdLevel); if (raw.size() > 0 && z.empty()) return false; // compress failed - return wrU64(z.size()) && wrU64(raw.size()) && (z.empty() || wr(z.data(), z.size())); + return wrU64(z.size()) && wrU64(raw.size()) && (z.empty() || write_bytes(z.data(), z.size())); }; SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN }; - if (!wr(&hdr, sizeof(hdr))) { fclose(f); return false; } + if (!write_bytes(&hdr, sizeof(hdr))) { fclose(f); return false; } // --- Geometry section: per-chunk zstd(vertex) + zstd(index) frames ------- // Offsets in the chunk TOC are relative to the geometry section start, so @@ -174,19 +185,19 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { std::vector chunks = data.chunks; // fill blob offsets below std::vector vraw, iraw; - for (auto& c : chunks) { - extractChunkGeometry(data, c, vraw, iraw); + for (auto& sidecar_chunk : chunks) { + extractChunkGeometry(data, sidecar_chunk, vraw, iraw); auto vz = SidecarCompress::compress(vraw.data(), vraw.size(), kSidecarZstdLevel); auto iz = SidecarCompress::compress(iraw.data(), iraw.size(), kSidecarZstdLevel); if ((vraw.size() && vz.empty()) || (iraw.size() && iz.empty())) { fclose(f); return false; } - c.v_comp_off = std::uint64_t(ftell(f) - geom_start); - c.v_comp_size = vz.size(); - c.v_raw_size = vraw.size(); - if (!vz.empty() && !wr(vz.data(), vz.size())) { fclose(f); return false; } - c.i_comp_off = std::uint64_t(ftell(f) - geom_start); - c.i_comp_size = iz.size(); - c.i_raw_size = iraw.size(); - if (!iz.empty() && !wr(iz.data(), iz.size())) { fclose(f); return false; } + sidecar_chunk.v_comp_off = std::uint64_t(ftell(f) - geom_start); + sidecar_chunk.v_comp_size = vz.size(); + sidecar_chunk.v_raw_size = vraw.size(); + if (!vz.empty() && !write_bytes(vz.data(), vz.size())) { fclose(f); return false; } + sidecar_chunk.i_comp_off = std::uint64_t(ftell(f) - geom_start); + sidecar_chunk.i_comp_size = iz.size(); + sidecar_chunk.i_raw_size = iraw.size(); + if (!iz.empty() && !write_bytes(iz.data(), iz.size())) { fclose(f); return false; } } const long geom_end = ftell(f); if (geom_start < 0 || geom_end < 0) { fclose(f); return false; } @@ -195,23 +206,23 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { if (fseek(f, geom_end, SEEK_SET) != 0) { fclose(f); return false; } // --- Critical metadata block (zstd): meshes, instances, georef, chunk TOC - std::vector crit; - appendVec(crit, data.meshes); - appendVec(crit, data.instances); - appendBytes(crit, &data.has_coordinate_operation, 4); - appendBytes(crit, data.coordinate_operation_meters, sizeof(double) * 16); - appendBytes(crit, &data.project_length_to_meters, sizeof(double)); - appendBytes(crit, &data.map_unit_to_meters, sizeof(double)); - appendVec(crit, chunks); - if (!wrBlock(crit)) { fclose(f); return false; } + std::vector critical_metadata; + appendVec(critical_metadata, data.meshes); + appendVec(critical_metadata, data.instances); + appendBytes(critical_metadata, &data.has_coordinate_operation, 4); + appendBytes(critical_metadata, data.coordinate_operation_meters, sizeof(double) * 16); + appendBytes(critical_metadata, &data.project_length_to_meters, sizeof(double)); + appendBytes(critical_metadata, &data.map_unit_to_meters, sizeof(double)); + appendVec(critical_metadata, chunks); + if (!wrBlock(critical_metadata)) { fclose(f); return false; } // --- Deferred metadata block (zstd): element tree + string table --------- - std::vector def; - appendVec(def, data.elements); + std::vector deferred_metadata; + appendVec(deferred_metadata, data.elements); std::uint32_t stbl_len = static_cast(data.string_table.size()); - appendBytes(def, &stbl_len, 4); - appendBytes(def, data.string_table.data(), stbl_len); - if (!wrBlock(def)) { fclose(f); return false; } + appendBytes(deferred_metadata, &stbl_len, 4); + appendBytes(deferred_metadata, data.string_table.data(), stbl_len); + if (!wrBlock(deferred_metadata)) { fclose(f); return false; } fclose(f); return true; @@ -257,28 +268,30 @@ std::optional readSidecar(const std::string& ifc_path) { if (hdr.magic != SIDECAR_MAGIC || hdr.version != SIDECAR_VERSION || hdr.endian != SIDECAR_ENDIAN) return fail(); - auto rd = [&](void* p, std::size_t k) { return fread(p, 1, k, f) == k; }; - auto rdU64 = [&](std::uint64_t& v) { return rd(&v, sizeof(v)); }; + auto read_bytes = [&](void* data, std::size_t byte_count) { + return fread(data, 1, byte_count, f) == byte_count; + }; + auto rdU64 = [&](std::uint64_t& v) { return read_bytes(&v, sizeof(v)); }; std::uint64_t geom_bytes = 0; if (!rdU64(geom_bytes)) return fail(); std::vector geom(static_cast(geom_bytes)); - if (geom_bytes && !rd(geom.data(), geom.size())) return fail(); + if (geom_bytes && !read_bytes(geom.data(), geom.size())) return fail(); auto readBlock = [&](std::vector& out) -> bool { std::uint64_t comp = 0, raw = 0; if (!rdU64(comp) || !rdU64(raw)) return false; std::vector z(static_cast(comp)); - if (comp && !rd(z.data(), z.size())) return false; + if (comp && !read_bytes(z.data(), z.size())) return false; out.assign(std::size_t(raw), 0); return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size()); }; - std::vector crit, def; - if (!readBlock(crit) || !readBlock(def)) return fail(); + std::vector critical_metadata, deferred_metadata; + if (!readBlock(critical_metadata) || !readBlock(deferred_metadata)) return fail(); fclose(f); SidecarData data; - BufReader cr{ crit.data(), crit.size() }; + BufReader cr{ critical_metadata.data(), critical_metadata.size() }; if (!cr.takeVec(data.meshes)) return std::nullopt; if (!cr.takeVec(data.instances)) return std::nullopt; if (!cr.take(&data.has_coordinate_operation, 4)) return std::nullopt; @@ -287,7 +300,7 @@ std::optional readSidecar(const std::string& ifc_path) { if (!cr.take(&data.map_unit_to_meters, sizeof(double))) return std::nullopt; if (!cr.takeVec(data.chunks)) return std::nullopt; - BufReader dr{ def.data(), def.size() }; + BufReader dr{ deferred_metadata.data(), deferred_metadata.size() }; if (!dr.takeVec(data.elements)) return std::nullopt; std::uint32_t stbl_len = 0; if (!dr.take(&stbl_len, 4)) return std::nullopt; @@ -296,46 +309,68 @@ std::optional readSidecar(const std::string& ifc_path) { // Reconstruct the whole-model vertex/index arrays from the per-chunk blobs. std::size_t vsize = 0, isize = 0; - for (const auto& m : data.meshes) { + for (const auto& mesh_info : data.meshes) { vsize = std::max(vsize, - std::size_t(m.vbo_byte_offset) + std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES); - isize = std::max(isize, m.ebo_byte_offset / sizeof(std::uint32_t) + m.index_count); - if (m.lod1_index_count) - isize = std::max(isize, m.lod1_ebo_byte_offset / sizeof(std::uint32_t) + m.lod1_index_count); + std::size_t(mesh_info.vbo_byte_offset) + + std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES); + isize = std::max( + isize, mesh_info.ebo_byte_offset / sizeof(std::uint32_t) + mesh_info.index_count); + if (mesh_info.lod1_index_count) { + isize = std::max( + isize, + mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t) + mesh_info.lod1_index_count); + } } data.vertices.assign(vsize, 0); data.indices.assign(isize, 0); - for (const auto& c : data.chunks) { - if (c.v_comp_off + c.v_comp_size > geom.size() || - c.i_comp_off + c.i_comp_size > geom.size()) return std::nullopt; - std::vector vraw(static_cast(c.v_raw_size)); - std::vector iraw(static_cast(c.i_raw_size)); - if (!SidecarCompress::decompress(geom.data() + c.v_comp_off, c.v_comp_size, vraw.data(), vraw.size()) || - !SidecarCompress::decompress(geom.data() + c.i_comp_off, c.i_comp_size, iraw.data(), iraw.size())) + for (const auto& sidecar_chunk : data.chunks) { + if (sidecar_chunk.v_comp_off + sidecar_chunk.v_comp_size > geom.size() || + sidecar_chunk.i_comp_off + sidecar_chunk.i_comp_size > geom.size()) return std::nullopt; + std::vector vraw(static_cast(sidecar_chunk.v_raw_size)); + std::vector iraw(static_cast(sidecar_chunk.i_raw_size)); + if (!SidecarCompress::decompress( + geom.data() + sidecar_chunk.v_comp_off, sidecar_chunk.v_comp_size, vraw.data(), vraw.size()) || + !SidecarCompress::decompress( + geom.data() + sidecar_chunk.i_comp_off, sidecar_chunk.i_comp_size, iraw.data(), iraw.size())) return std::nullopt; const auto* iu = reinterpret_cast(iraw.data()); std::size_t vcur = 0, icur = 0; - const std::uint32_t end = c.first_mesh + c.mesh_count; - for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) { - const MeshInfo& m = data.meshes[mi]; - const std::size_t vn = std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; - if (vcur + vn <= vraw.size() && m.vbo_byte_offset + vn <= data.vertices.size()) - std::memcpy(&data.vertices[m.vbo_byte_offset], vraw.data() + vcur, vn); - vcur += vn; + const std::uint32_t end = sidecar_chunk.first_mesh + sidecar_chunk.mesh_count; + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = data.meshes[mesh_index]; + const std::size_t vertex_byte_count = + std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + if (vcur + vertex_byte_count <= vraw.size() && + mesh_info.vbo_byte_offset + vertex_byte_count <= data.vertices.size()) { + std::memcpy(&data.vertices[mesh_info.vbo_byte_offset], vraw.data() + vcur, vertex_byte_count); + } + vcur += vertex_byte_count; } - for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) { - const MeshInfo& m = data.meshes[mi]; - if (!m.index_count) continue; - if (icur + m.index_count <= iraw.size() / 4) - std::memcpy(&data.indices[m.ebo_byte_offset / sizeof(std::uint32_t)], iu + icur, m.index_count * 4); - icur += m.index_count; + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = data.meshes[mesh_index]; + if (!mesh_info.index_count) continue; + if (icur + mesh_info.index_count <= iraw.size() / 4) { + std::memcpy(&data.indices[mesh_info.ebo_byte_offset / sizeof(std::uint32_t)], + iu + icur, + mesh_info.index_count * 4); + } + icur += mesh_info.index_count; } - for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) { - const MeshInfo& m = data.meshes[mi]; - if (!m.lod1_index_count) continue; - if (icur + m.lod1_index_count <= iraw.size() / 4) - std::memcpy(&data.indices[m.lod1_ebo_byte_offset / sizeof(std::uint32_t)], iu + icur, m.lod1_index_count * 4); - icur += m.lod1_index_count; + for (std::uint32_t mesh_index = sidecar_chunk.first_mesh; + mesh_index < end && mesh_index < data.meshes.size(); + ++mesh_index) { + const MeshInfo& mesh_info = data.meshes[mesh_index]; + if (!mesh_info.lod1_index_count) continue; + if (icur + mesh_info.lod1_index_count <= iraw.size() / 4) { + std::memcpy(&data.indices[mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t)], + iu + icur, + mesh_info.lod1_index_count * 4); + } + icur += mesh_info.lod1_index_count; } } return data; diff --git a/src/ifcviewer/SidecarLayout.cpp b/src/ifcviewer/SidecarLayout.cpp index c7f699440b..a80abc6216 100644 --- a/src/ifcviewer/SidecarLayout.cpp +++ b/src/ifcviewer/SidecarLayout.cpp @@ -26,37 +26,42 @@ #include void reorderSidecarByMorton(SidecarData& sd) { - const std::size_t n = sd.meshes.size(); - if (n < 2) return; + const std::size_t mesh_count = sd.meshes.size(); + if (mesh_count < 2) return; // Per-mesh centroid + instance count, exactly as the loader computes them // before chunk planning (average of instance world-AABB centres). - std::vector cx(n, 0.0f), cy(n, 0.0f), cz(n, 0.0f); - std::vector cnt(n, 0); + std::vector mesh_centroid_x(mesh_count, 0.0f), + mesh_centroid_y(mesh_count, 0.0f), + mesh_centroid_z(mesh_count, 0.0f); + std::vector mesh_instance_count(mesh_count, 0); for (const auto& inst : sd.instances) { - if (inst.mesh_id >= n) continue; - cx[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]); - cy[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]); - cz[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]); - ++cnt[inst.mesh_id]; + if (inst.mesh_id >= mesh_count) continue; + mesh_centroid_x[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]); + mesh_centroid_y[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]); + mesh_centroid_z[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]); + ++mesh_instance_count[inst.mesh_id]; } - for (std::size_t i = 0; i < n; ++i) { - if (cnt[i] > 0) { - const float inv = 1.0f / float(cnt[i]); - cx[i] *= inv; cy[i] *= inv; cz[i] *= inv; + for (std::size_t i = 0; i < mesh_count; ++i) { + if (mesh_instance_count[i] > 0) { + const float inv = 1.0f / float(mesh_instance_count[i]); + mesh_centroid_x[i] *= inv; + mesh_centroid_y[i] *= inv; + mesh_centroid_z[i] *= inv; } } // order[new_id] = old mesh id, in the loader's Morton order. const std::vector order = - ChunkPlanner::sortMeshIdsByMorton(n, cx, cy, cz, cnt); + ChunkPlanner::sortMeshIdsByMorton( + mesh_count, mesh_centroid_x, mesh_centroid_y, mesh_centroid_z, mesh_instance_count); // Greedy-pack the sorted order into chunks (the same plan the loader used // to derive). Each chunk is a CONSECUTIVE run of `order`, so once we lay // meshes out in `order` the chunk is a contiguous mesh range — recorded in // the TOC as {first_mesh, mesh_count}. - std::vector mesh_vertex_count(n, 0); - for (std::size_t i = 0; i < n; ++i) mesh_vertex_count[i] = sd.meshes[i].vertex_count; + std::vector mesh_vertex_count(mesh_count, 0); + for (std::size_t i = 0; i < mesh_count; ++i) mesh_vertex_count[i] = sd.meshes[i].vertex_count; const std::vector> packed = ChunkPlanner::greedyPackChunks( order, mesh_vertex_count, INSTANCED_VERTEX_STRIDE_BYTES, WGPU_CHUNK_VERTEX_BYTES_LIMIT); @@ -74,58 +79,61 @@ void reorderSidecarByMorton(SidecarData& sd) { // MeshInfo.first_instance: the baker leaves it 0 for every mesh and stores // instances ungrouped, so first_instance describes nothing. Grouping here // by mesh_id both reorders instances correctly AND fixes first_instance. - std::vector> insts_by_mesh(n); - for (std::uint32_t ii = 0; ii < sd.instances.size(); ++ii) { - const std::uint32_t mid = sd.instances[ii].mesh_id; - if (mid < n) insts_by_mesh[mid].push_back(ii); + std::vector> insts_by_mesh(mesh_count); + for (std::uint32_t instance_index = 0; instance_index < sd.instances.size(); ++instance_index) { + const std::uint32_t mesh_id = sd.instances[instance_index].mesh_id; + if (mesh_id < mesh_count) insts_by_mesh[mesh_id].push_back(instance_index); } std::vector new_vertices; new_vertices.reserve(sd.vertices.size()); std::vector new_indices; new_indices.reserve(sd.indices.size()); - std::vector new_meshes(n); + std::vector new_meshes(mesh_count); std::vector new_instances; new_instances.reserve(sd.instances.size()); // Pass A: vertices + LOD0 indices + instances, mesh-by-mesh in the new // order, recording the new offsets on each MeshInfo. - for (std::uint32_t ni = 0; ni < n; ++ni) { - const std::uint32_t old = order[ni]; - const MeshInfo& om = sd.meshes[old]; - MeshInfo nm = om; // carries AABB; offsets/instance fields overwritten below + for (std::uint32_t new_mesh_index = 0; new_mesh_index < mesh_count; ++new_mesh_index) { + const std::uint32_t old = order[new_mesh_index]; + const MeshInfo& old_mesh_info = sd.meshes[old]; + MeshInfo new_mesh_info = old_mesh_info; // carries AABB; offsets/instance fields overwritten below - nm.vbo_byte_offset = std::uint32_t(new_vertices.size()); - const std::size_t vbytes = std::size_t(om.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + new_mesh_info.vbo_byte_offset = std::uint32_t(new_vertices.size()); + const std::size_t vbytes = std::size_t(old_mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; new_vertices.insert(new_vertices.end(), - sd.vertices.begin() + om.vbo_byte_offset, - sd.vertices.begin() + om.vbo_byte_offset + vbytes); + sd.vertices.begin() + old_mesh_info.vbo_byte_offset, + sd.vertices.begin() + old_mesh_info.vbo_byte_offset + vbytes); - nm.ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t)); - const std::size_t i0 = om.ebo_byte_offset / sizeof(std::uint32_t); + new_mesh_info.ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t)); + const std::size_t i0 = old_mesh_info.ebo_byte_offset / sizeof(std::uint32_t); new_indices.insert(new_indices.end(), sd.indices.begin() + i0, - sd.indices.begin() + i0 + om.index_count); + sd.indices.begin() + i0 + old_mesh_info.index_count); - nm.first_instance = std::uint32_t(new_instances.size()); - nm.instance_count = std::uint32_t(insts_by_mesh[old].size()); - for (std::uint32_t ii : insts_by_mesh[old]) { - InstanceCpu ic = sd.instances[ii]; - ic.mesh_id = ni; - new_instances.push_back(ic); + new_mesh_info.first_instance = std::uint32_t(new_instances.size()); + new_mesh_info.instance_count = std::uint32_t(insts_by_mesh[old].size()); + for (std::uint32_t instance_index : insts_by_mesh[old]) { + InstanceCpu instance = sd.instances[instance_index]; + instance.mesh_id = new_mesh_index; + new_instances.push_back(instance); } - new_meshes[ni] = nm; + new_meshes[new_mesh_index] = new_mesh_info; } // Pass B: LOD1 indices appended after all LOD0 (same global layout as the // baker), in the new order, so a chunk's LOD1 slice is contiguous too. - for (std::uint32_t ni = 0; ni < n; ++ni) { - const MeshInfo& om = sd.meshes[order[ni]]; - MeshInfo& nm = new_meshes[ni]; - if (om.lod1_index_count == 0) { nm.lod1_ebo_byte_offset = 0; continue; } - nm.lod1_ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t)); - const std::size_t l0 = om.lod1_ebo_byte_offset / sizeof(std::uint32_t); + for (std::uint32_t new_mesh_index = 0; new_mesh_index < mesh_count; ++new_mesh_index) { + const MeshInfo& old_mesh_info = sd.meshes[order[new_mesh_index]]; + MeshInfo& new_mesh_info = new_meshes[new_mesh_index]; + if (old_mesh_info.lod1_index_count == 0) { + new_mesh_info.lod1_ebo_byte_offset = 0; + continue; + } + new_mesh_info.lod1_ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t)); + const std::size_t l0 = old_mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t); new_indices.insert(new_indices.end(), sd.indices.begin() + l0, - sd.indices.begin() + l0 + om.lod1_index_count); + sd.indices.begin() + l0 + old_mesh_info.lod1_index_count); } sd.vertices = std::move(new_vertices); diff --git a/src/ifcviewer/StreamingLoader.cpp b/src/ifcviewer/StreamingLoader.cpp index 3238e5f321..dea53f79c3 100644 --- a/src/ifcviewer/StreamingLoader.cpp +++ b/src/ifcviewer/StreamingLoader.cpp @@ -52,25 +52,25 @@ struct SidecarHeaderRaw { // walks the metadata tail through one of these so a truncated buffer fails // cleanly (return false) instead of reading out of bounds. struct BufCursor { - const uint8_t* p; - size_t remaining; + const uint8_t* cursor; + size_t remaining_bytes; bool take(void* dst, size_t bytes) { - if (bytes > remaining) return false; - std::memcpy(dst, p, bytes); - p += bytes; - remaining -= bytes; + if (bytes > remaining_bytes) return false; + std::memcpy(dst, cursor, bytes); + cursor += bytes; + remaining_bytes -= bytes; return true; } // Read a uint32 length prefix followed by length*sizeof(T) elements. template - bool takeVec(std::vector& v) { + bool takeVec(std::vector& values) { uint32_t n; if (!take(&n, 4)) return false; - if (uint64_t(n) * sizeof(T) > remaining) return false; - v.resize(n); - if (n > 0 && !take(v.data(), size_t(n) * sizeof(T))) return false; + if (uint64_t(n) * sizeof(T) > remaining_bytes) return false; + values.resize(n); + if (n > 0 && !take(values.data(), size_t(n) * sizeof(T))) return false; return true; } }; @@ -119,7 +119,7 @@ bool parseSidecarDeferred(const uint8_t* data, size_t n, SidecarData& out) { if (!c.takeVec(out.elements)) return false; uint32_t stbl_len = 0; if (!c.take(&stbl_len, 4)) return false; - if (stbl_len > c.remaining) return false; + if (stbl_len > c.remaining_bytes) return false; out.string_table.resize(stbl_len); if (stbl_len > 0 && !c.take(out.string_table.data(), stbl_len)) return false; return true; diff --git a/src/ifcviewer/StreamingThread.cpp b/src/ifcviewer/StreamingThread.cpp index 15ef91d919..39b136c0aa 100644 --- a/src/ifcviewer/StreamingThread.cpp +++ b/src/ifcviewer/StreamingThread.cpp @@ -26,23 +26,23 @@ StreamingThread::~StreamingThread() { } void StreamingThread::start() { - std::unique_lock lk(mu_); + std::unique_lock lock(mu_); if (running_) return; shutdown_ = false; running_ = true; - lk.unlock(); + lock.unlock(); worker_ = std::thread(&StreamingThread::workerLoop, this); } void StreamingThread::stop() { { - std::unique_lock lk(mu_); + std::unique_lock lock(mu_); if (!running_) return; shutdown_ = true; } cv_.notify_all(); if (worker_.joinable()) worker_.join(); - std::unique_lock lk(mu_); + std::unique_lock lock(mu_); running_ = false; requests_.clear(); results_.clear(); @@ -50,7 +50,7 @@ void StreamingThread::stop() { bool StreamingThread::enqueue(Request req) { { - std::unique_lock lk(mu_); + std::unique_lock lock(mu_); if (!running_ || shutdown_) return false; requests_.push_back(std::move(req)); } @@ -59,20 +59,20 @@ bool StreamingThread::enqueue(Request req) { } std::vector StreamingThread::drainResults() { - std::vector out; + std::vector results; { - std::unique_lock lk(mu_); - out.reserve(results_.size()); + std::unique_lock lock(mu_); + results.reserve(results_.size()); while (!results_.empty()) { - out.push_back(std::move(results_.front())); + results.push_back(std::move(results_.front())); results_.pop_front(); } } - return out; + return results; } std::size_t StreamingThread::inFlightApprox() const { - std::unique_lock lk(mu_); + std::unique_lock lock(mu_); return requests_.size() + (in_progress_ ? 1u : 0u); } @@ -80,8 +80,8 @@ void StreamingThread::workerLoop() { for (;;) { Request req; { - std::unique_lock lk(mu_); - cv_.wait(lk, [this]() { return shutdown_ || !requests_.empty(); }); + std::unique_lock lock(mu_); + cv_.wait(lock, [this]() { return shutdown_ || !requests_.empty(); }); if (shutdown_ && requests_.empty()) return; req = std::move(requests_.front()); requests_.pop_front(); @@ -94,18 +94,18 @@ void StreamingThread::workerLoop() { // us. The vbytes / idx buffers are allocated here on the worker // thread — they cross back to the main thread when the result // is drained and applied (pool.alloc + queueWriteBuffer). - Result res; - res.model_id = req.model_id; - res.chunk_idx = req.chunk_idx; - res.success = readChunkGeometryCompressed( + Result result; + result.model_id = req.model_id; + result.chunk_idx = req.chunk_idx; + result.success = readChunkGeometryCompressed( req.file_path, req.geometry_section_offset, req.v_comp_off, req.v_comp_size, req.v_raw_size, req.i_comp_off, req.i_comp_size, req.i_raw_size, - res.vbytes, res.idx); + result.vbytes, result.idx); { - std::unique_lock lk(mu_); - results_.push_back(std::move(res)); + std::unique_lock lock(mu_); + results_.push_back(std::move(result)); in_progress_ = false; } } From cb1ab9cbfbeaec44719600f2b7be3961a85cab08 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 08:44:09 +1000 Subject: [PATCH 03/11] Remove wgpu memory probe Drop the standalone WGPU memory-allocation diagnostic and its conditional CMake target.\n\nGenerated with the assistance of an AI coding tool. --- cmake/CMakeLists.txt | 6 - src/wgpu-mem-probe/CMakeLists.txt | 19 --- src/wgpu-mem-probe/main.cpp | 264 ------------------------------ 3 files changed, 289 deletions(-) delete mode 100644 src/wgpu-mem-probe/CMakeLists.txt delete mode 100644 src/wgpu-mem-probe/main.cpp diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index a18862a234..bc5e37a3bf 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -722,12 +722,6 @@ if(BUILD_BONSAIVIEWER) add_subdirectory(../src/bonsaiviewer bonsaiviewer) endif() -# Standalone wgpu diagnostic tool (no bonsai) — fast iteration on -# wgpu-native probing without the full bonsai build. -if(BUILD_BONSAIVIEWER_WGPU AND NOT BUILD_BONSAIVIEWER) - add_subdirectory(../src/wgpu-mem-probe wgpu-mem-probe) -endif() - # Cmake uninstall target if(NOT TARGET uninstall) configure_file( diff --git a/src/wgpu-mem-probe/CMakeLists.txt b/src/wgpu-mem-probe/CMakeLists.txt deleted file mode 100644 index e37c2b1fe3..0000000000 --- a/src/wgpu-mem-probe/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -################################################################################ -# # -# Standalone wgpu memory-allocation probe. No Qt, no surface, just wgpu. # -# Built only when BUILD_BONSAIVIEWER_WGPU is on so it tracks the same # -# wgpu_native version as the viewer. # -# # -################################################################################ - -message("Running CMakeLists.txt in /src/wgpu-mem-probe") - -add_executable(WgpuMemProbe main.cpp) - -target_link_libraries(WgpuMemProbe PRIVATE wgpu_native) - -if(UNIX AND NOT APPLE AND WGPU_NATIVE_LIB_DIR) - set_target_properties(WgpuMemProbe PROPERTIES - BUILD_RPATH "${WGPU_NATIVE_LIB_DIR}" - ) -endif() diff --git a/src/wgpu-mem-probe/main.cpp b/src/wgpu-mem-probe/main.cpp deleted file mode 100644 index 627644b627..0000000000 --- a/src/wgpu-mem-probe/main.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// Standalone wgpu memory-allocation probe. Headless — no surface, no Qt. -// Discovers what the adapter reports vs what the runtime actually grants: -// -// 1. Print all relevant adapter + device limits. -// 2. Single-allocation probe: try createBuffer at descending sizes, -// report which sizes succeed/refuse. -// 3. Cumulative allocation probe: keep allocating (without releasing) -// until the driver refuses, halving the requested size on each -// refusal. Reports total bytes / count we got to. -// -// Build: ninja -C build-viewer-wgpu WgpuMemProbe -// Run: ./build-viewer-wgpu/wgpu-mem-probe/WgpuMemProbe - -#include - -#include -#include -#include -#include -#include - -namespace { - -// Drain async wgpu events. Both adapter/device requests AND error scope -// pops fire on the instance's event loop. -void processEventsUntil(WGPUInstance instance, const bool* done) { - while (!*done) { - wgpuInstanceProcessEvents(instance); - } -} - -// Capture an error scope pop. Treats both Validation and OOM as "the -// allocation failed" — wgpu-native lumps "Not enough memory" into -// Validation, while spec-compliant impls (Dawn / browsers) classify -// as OutOfMemory. -struct ScopeResult { - bool done = false; - bool error = false; - WGPUErrorType type = WGPUErrorType_NoError; -}; - -void popScope(WGPUDevice device, WGPUInstance instance, ScopeResult* out) { - WGPUPopErrorScopeCallbackInfo cb = {}; - cb.mode = WGPUCallbackMode_AllowProcessEvents; - cb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, - WGPUStringView, void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - r->error = (type != WGPUErrorType_NoError); - r->type = type; - }; - cb.userdata1 = out; - wgpuDevicePopErrorScope(device, cb); - processEventsUntil(instance, &out->done); -} - -// Try createBuffer(size). Returns the buffer if successful (caller -// owns and must release), or nullptr otherwise. Captures both OOM -// and Validation error scopes — wgpu-native classifies OOM as -// Validation, so checking only OOM misses the signal. -WGPUBuffer tryAllocate(WGPUInstance instance, WGPUDevice device, - uint64_t size, const char* label) { - wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation); - wgpuDevicePushErrorScope(device, WGPUErrorFilter_OutOfMemory); - - WGPUBufferDescriptor desc = {}; - desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; - desc.size = size; - desc.label.data = label; - desc.label.length = std::strlen(label); - WGPUBuffer buf = wgpuDeviceCreateBuffer(device, &desc); - - ScopeResult oom, validation; - popScope(device, instance, &oom); - popScope(device, instance, &validation); - - if (!buf || oom.error || validation.error) { - if (buf) wgpuBufferRelease(buf); - return nullptr; - } - return buf; -} - -// Returns a std::string so multiple humanSize calls in one printf can -// coexist (static-buffer version had every "%s" point at the same -// last-written buffer). -std::string humanSize(uint64_t b) { - char buf[32]; - if (b >= (1ull << 30)) std::snprintf(buf, sizeof(buf), "%.2f GB", double(b) / double(1ull << 30)); - else if (b >= (1ull << 20)) std::snprintf(buf, sizeof(buf), "%.1f MB", double(b) / double(1ull << 20)); - else if (b >= (1ull << 10)) std::snprintf(buf, sizeof(buf), "%.1f KB", double(b) / double(1ull << 10)); - else std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)b); - return buf; -} - -// Suppress wgpu-native's own logging during probe so the output isn't -// drowned in "[wgpu device error 2]" noise from the OOM attempts that -// the error scopes have already captured. -void onUncapturedError(WGPUDevice const*, WGPUErrorType, WGPUStringView, - void*, void*) { - // intentionally silent -} - -} // namespace - -int main() { - WGPUInstance instance = wgpuCreateInstance(nullptr); - if (!instance) { std::printf("wgpuCreateInstance failed\n"); return 1; } - - // Request adapter (headless — no surface). HighPerformance for the - // discrete GPU on hybrid systems. - struct AdapterReq { WGPUAdapter adapter = nullptr; bool done = false; bool ok = false; }; - AdapterReq areq; - WGPURequestAdapterOptions opts = {}; - opts.powerPreference = WGPUPowerPreference_HighPerformance; - - WGPURequestAdapterCallbackInfo acb = {}; - acb.mode = WGPUCallbackMode_AllowProcessEvents; - acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, - WGPUStringView, void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - if (status == WGPURequestAdapterStatus_Success) { - r->adapter = adapter; - r->ok = true; - } - }; - acb.userdata1 = &areq; - wgpuInstanceRequestAdapter(instance, &opts, acb); - while (!areq.done) wgpuInstanceProcessEvents(instance); - if (!areq.ok) { std::printf("RequestAdapter failed\n"); return 1; } - - // Adapter info. - WGPUAdapterInfo info = {}; - wgpuAdapterGetInfo(areq.adapter, &info); - std::printf("Adapter:\n"); - std::printf(" vendor : %.*s\n", int(info.vendor.length), info.vendor.data); - std::printf(" device : %.*s\n", int(info.device.length), info.device.data); - std::printf(" desc : %.*s\n", int(info.description.length), info.description.data); - std::printf(" backend : %d\n", int(info.backendType)); - wgpuAdapterInfoFreeMembers(info); - - WGPULimits alimits = {}; - wgpuAdapterGetLimits(areq.adapter, &alimits); - std::printf("\nAdapter limits:\n"); - std::printf(" maxBufferSize = %s\n", humanSize(alimits.maxBufferSize).c_str()); - std::printf(" maxStorageBufferBindingSize = %s\n", humanSize(alimits.maxStorageBufferBindingSize).c_str()); - std::printf(" maxStorageBuffersPerStage = %u\n", alimits.maxStorageBuffersPerShaderStage); - - // Request device with adapter's max limits (so we don't artificially - // restrict ourselves to the WebGPU floor). - WGPUDeviceDescriptor ddesc = {}; - ddesc.requiredLimits = &alimits; - ddesc.uncapturedErrorCallbackInfo.callback = onUncapturedError; - - struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; }; - DeviceReq dreq; - WGPURequestDeviceCallbackInfo dcb = {}; - dcb.mode = WGPUCallbackMode_AllowProcessEvents; - dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device, - WGPUStringView, void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - if (status == WGPURequestDeviceStatus_Success) { - r->device = device; - r->ok = true; - } - }; - dcb.userdata1 = &dreq; - wgpuAdapterRequestDevice(areq.adapter, &ddesc, dcb); - while (!dreq.done) wgpuInstanceProcessEvents(instance); - if (!dreq.ok) { std::printf("RequestDevice failed\n"); return 1; } - - WGPULimits dlimits = {}; - wgpuDeviceGetLimits(dreq.device, &dlimits); - std::printf("\nDevice limits (granted):\n"); - std::printf(" maxBufferSize = %s\n", humanSize(dlimits.maxBufferSize).c_str()); - std::printf(" maxStorageBufferBindingSize = %s\n", humanSize(dlimits.maxStorageBufferBindingSize).c_str()); - - // ---- Test 1: Single-allocation probe. ------------------------------ - // Try createBuffer at descending sizes, release after each. Tells us - // the biggest single buffer the driver will grant at all (independent - // of fragmentation from prior allocations). - std::printf("\nSingle-allocation probe (each released before next):\n"); - static const uint64_t test_sizes[] = { - 16ull << 30, 8ull << 30, 4ull << 30, - 2ull << 30, 1ull << 30, - 512ull << 20, 256ull << 20, 128ull << 20, 64ull << 20, - }; - for (uint64_t s : test_sizes) { - WGPUBuffer b = tryAllocate(instance, dreq.device, s, "probe.single"); - std::printf(" %-10s : %s\n", humanSize(s).c_str(), b ? "OK" : "REFUSED"); - if (b) wgpuBufferRelease(b); - } - - // ---- Test 2: Cumulative allocation. -------------------------------- - // Keep allocating without releasing, halving the requested size on - // each refusal. Discovers actual total VRAM the runtime will let us - // park behind one device. THIS is what determines the upper bound - // of a multi-sub-buffer streaming pool. - std::printf("\nCumulative allocation probe (halve on refusal," - " stop at 64 MB floor):\n"); - constexpr uint64_t MIN_BYTES = 64ull << 20; - uint64_t try_size = dlimits.maxBufferSize; - if (try_size > (4ull << 30)) try_size = 4ull << 30; // 4 GB sane cap - std::vector retained; - uint64_t total = 0; - while (try_size >= MIN_BYTES) { - char label[64]; - std::snprintf(label, sizeof(label), "probe.cum.%zu", retained.size()); - WGPUBuffer b = tryAllocate(instance, dreq.device, try_size, label); - if (b) { - retained.push_back(b); - total += try_size; - std::printf(" + sub-buffer %2zu : %-9s (cumulative %s, %zu buffers)\n", - retained.size() - 1, humanSize(try_size).c_str(), - humanSize(total).c_str(), retained.size()); - } else { - std::printf(" - refused at %-9s (halving)\n", humanSize(try_size).c_str()); - try_size /= 2; - } - } - std::printf("\nFinal: %zu sub-buffers totalling %s\n", - retained.size(), humanSize(total).c_str()); - - // Release retained buffers. - for (WGPUBuffer b : retained) wgpuBufferRelease(b); - retained.clear(); - total = 0; - - // ---- Test 3: Uniform-size cumulative probe. ------------------------ - // Start with a smaller per-buffer size and keep stacking. Tells us - // whether the "max single size first" strategy leaves total VRAM on - // the table — e.g. on hardware where 2×2GB is refused but 4×1GB - // works (heap fragmentation favours smaller allocs). - static const uint64_t fixed_sizes[] = { - 1ull << 30, // 1 GB each - 512ull << 20, // 512 MB each - 256ull << 20, // 256 MB each - }; - for (uint64_t fixed : fixed_sizes) { - std::printf("\nFixed-size %s cumulative probe:\n", - humanSize(fixed).c_str()); - std::vector bufs; - uint64_t cum = 0; - for (;;) { - char label[64]; - std::snprintf(label, sizeof(label), "probe.fixed.%zu", bufs.size()); - WGPUBuffer b = tryAllocate(instance, dreq.device, fixed, label); - if (!b) break; - bufs.push_back(b); - cum += fixed; - } - std::printf(" %zu buffers × %s = %s\n", - bufs.size(), humanSize(fixed).c_str(), - humanSize(cum).c_str()); - for (WGPUBuffer b : bufs) wgpuBufferRelease(b); - } - - wgpuDeviceRelease(dreq.device); - wgpuAdapterRelease(areq.adapter); - wgpuInstanceRelease(instance); - return 0; -} From d08c53d756c68e2b5ead13b9df46eb228a969758 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 08:48:41 +1000 Subject: [PATCH 04/11] Remove stale WGPU mac workflow Drop the obsolete diagnostic macOS WGPU workflow that referenced removed standalone viewer paths and targets.\n\nGenerated with the assistance of an AI coding tool. --- .github/workflows/ci-wgpu-mac.yml | 123 ------------------------------ 1 file changed, 123 deletions(-) delete mode 100644 .github/workflows/ci-wgpu-mac.yml diff --git a/.github/workflows/ci-wgpu-mac.yml b/.github/workflows/ci-wgpu-mac.yml deleted file mode 100644 index c868f76b83..0000000000 --- a/.github/workflows/ci-wgpu-mac.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: wgpu sanity (macOS) - -# Lightweight compile-only check for the wgpu backend on macOS. Skips the -# IfcViewerWgpuMinimal executable (its surface creation has no Metal path -# yet — see task #32) and just confirms IfcViewerWgpu links and the pure- -# CPU state tests pass. -# -# Goal: catch portability regressions in the wgpu source on Apple Silicon -# / Intel Mac without needing the full IfcGeom + OCCT + Python toolchain -# that build_osx.yml carries. Runs in ~5 minutes. - -on: - push: - branches: - - ifcviewer-wgpu - - main - - v0.8.0 - paths: - - 'src/ifcviewer-wgpu/**' - - 'src/ifcviewer-wgpu-minimal/**' - - 'src/wgpu-mem-probe/**' - - 'src/ifcviewer/SidecarCache.*' - - 'src/ifcviewer/InstancedGeometry.h' - - 'src/ifcviewer/VertexQuantization.h' - - 'cmake/CMakeLists.txt' - - '.github/workflows/ci-wgpu-mac.yml' - pull_request: - paths: - - 'src/ifcviewer-wgpu/**' - - 'src/ifcviewer-wgpu-minimal/**' - - 'src/wgpu-mem-probe/**' - - 'src/ifcviewer/SidecarCache.*' - - 'src/ifcviewer/InstancedGeometry.h' - - 'src/ifcviewer/VertexQuantization.h' - - 'cmake/CMakeLists.txt' - - '.github/workflows/ci-wgpu-mac.yml' - workflow_dispatch: - -jobs: - build_wgpu_mac: - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - arch: arm64 - runner: macos-14 - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Install dependencies via Homebrew - run: | - brew update - # qt installs Qt6; eigen is header-only. Boost is required by - # the top-level CMakeLists unconditionally (line 328) even when - # IfcGeom/IfcPython are off — until ifcviewer-core is extracted - # (task #12) we have to humour it. - brew install qt eigen boost - # patchelf isn't on mac — wgpu-native's SONAME workaround is - # Linux-only and CMake guards on UNIX AND NOT APPLE so this is fine. - - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2.22 - with: - key: mac-wgpu-${{ matrix.arch }} - - - name: Configure (standalone wgpu, tests on) - shell: bash - # The top-level CMakeLists.txt unconditionally find_package's - # CGAL/OpenCASCADE/etc. before our BUILD_BONSAIVIEWER gate kicks - # in, so we explicitly disable every IfcGeom/IfcParse/IfcPython - # subproject — we only want IfcViewerWgpu + its state tests. - run: | - QT_PREFIX="$(brew --prefix qt)" - cmake -B build \ - -S cmake \ - -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_BONSAIVIEWER=OFF \ - -DBUILD_BONSAIVIEWER_WGPU=ON \ - -DBUILD_BONSAIVIEWER_TESTS=ON \ - -DBUILD_IFCGEOM=OFF \ - -DBUILD_IFCPYTHON=OFF \ - -DBUILD_CONVERT=OFF \ - -DBUILD_EXAMPLES=OFF \ - -DBUILD_GEOMSERVER=OFF \ - -DWITH_OPENCASCADE=OFF \ - -DWITH_CGAL=OFF \ - -DCOLLADA_SUPPORT=OFF \ - -DCMAKE_PREFIX_PATH="${QT_PREFIX}" \ - -DQT_DIR="${QT_PREFIX}" \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - - name: Build IfcViewerWgpu + state tests - # Skip IfcViewerWgpuMinimal: its createSurface has no Metal path - # yet (task #32). Static lib compiles cleanly because the X11 / - # Wayland surface blocks are wrapped in #if defined(Q_OS_LINUX). - # No `-- -j N`: Ninja parallelises by default (it rejects bare - # `-j` unlike make). - run: | - cmake --build build \ - --target IfcViewerWgpu test_wgpu_selection test_wgpu_visibility - - - name: Run state tests - working-directory: build - # No -R filter: only wgpu state tests are built in this config, - # so ctest discovers exactly those. - run: ctest --output-on-failure - - - name: Upload CMake configure log on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: cmake-log-${{ matrix.arch }} - path: | - build/CMakeCache.txt - build/CMakeFiles/CMakeConfigureLog.yaml - retention-days: 14 From cd3d70172bf2fa86c3aa8b173af05ef010cf5b6c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 09:08:28 +1000 Subject: [PATCH 05/11] ifcviewer: marquee box-select on web + suppress the canvas context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring rubber-band box-select to the web on the Web preset's select button (RMB). - Core: factor the pick-pass encode + rect copy out of picksInRect into encodeBoxPickToStaging (mirroring how single-pick shares encodePickReadbackToStaging), shared by the sync picksInRect (desktop) and a new async picksInRectAsync (web) — the latter maps the staging buffer via a spontaneous callback because the sync spin-map hangs the JS loop. New applyMarqueeToSelection (plain replace / Shift add / Ctrl remove). - Web main_web: a select-button drag past the click threshold draws a marquee rubber-band (a plain DOM
positioned in CSS px — no GPU overlay pass, which the web lib lacks) and on release box-picks the rect (device px) and applies it to the selection. A click (no drag) still single-picks. - Web shell.html: the #marquee div + styling, and — the reported bug — a contextmenu preventDefault on the canvas so RMB (now the select button) doesn't pop the browser menu. (Firefox still forces its native menu on Shift+RightClick; that's a browser escape hatch pages can't override.) Tests: applyMarqueeToSelection replace/add/remove + id-0 (Catch2, 124 total); web smoke marquee drag → rubber-band shown → selection changes → hidden (10/10). Desktop picksInRect unchanged in behaviour; BonsaiViewer builds. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer-web/main_web.cpp | 77 ++++++++++--- src/ifcviewer-web/shell.html | 13 +++ src/ifcviewer-web/tests/smoke.spec.mjs | 29 +++++ src/ifcviewer/ViewportCore.cpp | 111 +++++++++++++++---- src/ifcviewer/ViewportCore.h | 38 ++++++- src/ifcviewer/tests/test_viewport_camera.cpp | 22 ++++ 6 files changed, 247 insertions(+), 43 deletions(-) diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp index 8208a9250a..95dfac91e9 100644 --- a/src/ifcviewer-web/main_web.cpp +++ b/src/ifcviewer-web/main_web.cpp @@ -35,9 +35,12 @@ #include #include +#include #include #include +#include #include +#include namespace { @@ -110,6 +113,24 @@ int canvasCssHeight() { return (h > 1.0) ? int(h) : 1; } +// Marquee rectangle overlay. The rubber-band is a plain DOM
(shell.html) +// positioned in CSS px — the canvas fills the viewport, so canvas-relative +// coords are viewport coords. Cheaper + pixel-perfect vs a GPU overlay pass +// (which the web lib doesn't have anyway). +void showMarquee(int x, int y, int w, int h) { + EM_ASM({ + var m = document.getElementById('marquee'); + if (m) { + m.style.display = 'block'; + m.style.left = $0 + 'px'; m.style.top = $1 + 'px'; + m.style.width = $2 + 'px'; m.style.height = $3 + 'px'; + } + }, x, y, w, h); +} +void hideMarquee() { + EM_ASM({ var m = document.getElementById('marquee'); if (m) m.style.display = 'none'; }); +} + NavKind classifyPress(const ViewportCore::NavBindings& b, int em_button, bool shift, bool ctrl, bool alt) { using MB = ViewportCore::MouseBtn; using M = ViewportCore::NavMod; @@ -152,7 +173,14 @@ EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void* user) { app->nav_drag_px += std::abs(dx) + std::abs(dy); if (app->nav_kind == NavKind::Orbit) app->core.orbitBy(dx, dy); else if (app->nav_kind == NavKind::Pan) app->core.panBy(dx, dy, canvasCssHeight()); - // NavKind::Select drag → marquee box-select (next step). + else if (app->nav_kind == NavKind::Select && app->nav_drag_px > kClickDragThresholdPx) { + // Select-button drag → draw the marquee rubber-band (CSS px). + const long x0 = std::min(app->down_x, e->targetX); + const long y0 = std::min(app->down_y, e->targetY); + showMarquee(int(x0), int(y0), + int(std::labs(long(e->targetX) - app->down_x)), + int(std::labs(long(e->targetY) - app->down_y))); + } return EM_TRUE; } @@ -163,23 +191,36 @@ EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) { app->nav_active = false; app->nav_kind = NavKind::None; - // Select-button release with no real drag → pick the object under the cursor - // and route it through selection (Shift add, Ctrl remove, plain replace). - // Async readback: the highlight appears a frame after the result lands. - if (was_active && kind == NavKind::Select && app->ready && - app->nav_drag_px <= kClickDragThresholdPx) { - const double dpr = emscripten_get_device_pixel_ratio(); - const int px = int(app->down_x * dpr); - const int py = int(app->down_y * dpr); - const bool add = e->shiftKey; - const bool remove = e->ctrlKey; - app->core.pickObjectAtAsync(px, py, [app, add, remove](std::uint32_t id) { - app->core.applyPickToSelection(id, add, remove); - // Demo the v15 on-demand deferred fetch: log the picked object's - // IFC GUID (first pick fetches the property block off the network). - if (id != 0) app->core.logSelectedObjectGuidWeb(id); - app->host.requestFrame(); - }); + if (was_active && kind == NavKind::Select && app->ready) { + const double dpr = emscripten_get_device_pixel_ratio(); + const bool add = e->shiftKey; + const bool remove = e->ctrlKey; + if (app->nav_drag_px > kClickDragThresholdPx) { + // Marquee drag → box-pick the rect (device px) and apply to selection. + hideMarquee(); + const long x0 = std::min(app->down_x, e->targetX); + const long y0 = std::min(app->down_y, e->targetY); + const int rx = int(x0 * dpr), ry = int(y0 * dpr); + const int rw = int(std::labs(long(e->targetX) - app->down_x) * dpr); + const int rh = int(std::labs(long(e->targetY) - app->down_y) * dpr); + app->core.picksInRectAsync(rx, ry, rw, rh, + [app, add, remove](std::vector ids) { + app->core.applyMarqueeToSelection(ids, add, remove); + app->host.requestFrame(); + }); + } else { + // No real drag → single pick under the cursor (Shift add, Ctrl remove, + // plain replace). Async readback: highlight lands a frame later. + const int px = int(app->down_x * dpr); + const int py = int(app->down_y * dpr); + app->core.pickObjectAtAsync(px, py, [app, add, remove](std::uint32_t id) { + app->core.applyPickToSelection(id, add, remove); + // v15 on-demand deferred fetch: log the picked object's IFC GUID + // (first pick fetches the property block off the network). + if (id != 0) app->core.logSelectedObjectGuidWeb(id); + app->host.requestFrame(); + }); + } } return EM_TRUE; } diff --git a/src/ifcviewer-web/shell.html b/src/ifcviewer-web/shell.html index 17ec6f082f..f3339d9c8d 100644 --- a/src/ifcviewer-web/shell.html +++ b/src/ifcviewer-web/shell.html @@ -8,6 +8,10 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #viewer-canvas { display: block; width: 100vw; height: 100vh; outline: none; background: #1a1d24; } + /* Marquee (box-select) rubber-band. Positioned in CSS px by main_web; never + eats pointer events so the drag keeps reaching the canvas. */ + #marquee { position: fixed; display: none; z-index: 50; pointer-events: none; + border: 1px solid #4a9eff; background: rgba(74, 158, 255, 0.15); } /* Log overlay sits bottom-left and never eats pointer events (so it can't block orbit drags over the canvas). It auto-scrolls to the newest line. Capped small; collapses further once the app is live. */ @@ -63,6 +67,7 @@ +
@@ -302,6 +307,14 @@ // the current scene (federation). Multiple files can be picked at once. Each // File is registered as its own byte-source (kept alive in __ifcvSources for // lazy Blob.slice reads) and streamed independently. + // RMB is the select/marquee button in the Web nav preset, so suppress the + // browser context menu over the canvas. (Firefox forces its native menu on + // Shift+RightClick regardless — a browser escape hatch pages can't override.) + var viewerCanvas = document.getElementById('viewer-canvas'); + if (viewerCanvas) { + viewerCanvas.addEventListener('contextmenu', function(ev) { ev.preventDefault(); }); + } + var openBtn = document.getElementById('open-btn'); var addBtn = document.getElementById('add-btn'); var fileInput = document.getElementById('file-input'); diff --git a/src/ifcviewer-web/tests/smoke.spec.mjs b/src/ifcviewer-web/tests/smoke.spec.mjs index 6316c4e264..fc7acc0a82 100644 --- a/src/ifcviewer-web/tests/smoke.spec.mjs +++ b/src/ifcviewer-web/tests/smoke.spec.mjs @@ -343,3 +343,32 @@ test('hide selected removes geometry after a pick', async ({ page }) => { await page.waitForTimeout(400); expect(gpuErrors, gpuErrors.join('\n')).toEqual([]); }); + +test('RMB marquee drag box-selects (Web preset)', async ({ page }) => { + const gpuErrors = []; + page.on('console', (m) => { if (/Uncaptured WebGPU error|is invalid/i.test(m.text())) gpuErrors.push(m.text()); }); + await ready(page); + + const box = await page.locator('#viewer-canvas').boundingBox(); + const cx = box.x + box.width / 2, cy = box.y + box.height / 2; + const before = await shot(page); + await page.mouse.move(cx - 120, cy - 90); + await page.mouse.down({ button: 'right' }); + await page.mouse.move(cx - 40, cy - 30, { steps: 4 }); + const midDragVisible = await page.evaluate(() => { + const m = document.getElementById('marquee'); + return !!(m && getComputedStyle(m).display !== 'none'); + }); + await page.mouse.move(cx + 120, cy + 90, { steps: 6 }); + await page.mouse.up({ button: 'right' }); + await page.waitForTimeout(600); // async box-pick + apply + render + const after = await shot(page); + const hiddenAfter = await page.evaluate(() => { + const m = document.getElementById('marquee'); + return !!(m && getComputedStyle(m).display === 'none'); + }); + expect(midDragVisible, 'marquee rubber-band was not shown during the drag').toBe(true); + expect(Buffer.compare(before, after), 'box-select did not change the canvas').not.toBe(0); + expect(hiddenAfter, 'marquee was not hidden after release').toBe(true); + expect(gpuErrors, gpuErrors.join('\n')).toEqual([]); +}); diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 240bf494eb..91a9e8eb47 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -5018,6 +5018,17 @@ void ViewportCore::applyPickToSelection(std::uint32_t object_id, bool add, bool else selection_.replace(object_id); } +void ViewportCore::applyMarqueeToSelection(const std::vector& ids, + bool add, bool remove) { + if (!add && !remove) selection_.clear(); // plain marquee replaces + for (std::uint32_t id : ids) { + if (id == 0) continue; + if (remove) selection_.remove(id); + else selection_.add(id); // replace (post-clear) or add + } + host_->requestFrame(); +} + void ViewportCore::hideSelected() { if (selection_.count() == 0) return; for (uint32_t id : selection_.selectionIds()) visibility_.hide(id); @@ -5110,19 +5121,20 @@ void ViewportCore::pickObjectAtAsync(int x_pixels, int y_pixels, } #endif // __EMSCRIPTEN__ -std::vector ViewportCore::picksInRect(int x, int y, int w, int h) { - std::vector out; - if (w <= 0 || h <= 0) return out; - if (!pick_pipeline_ || !device_ || !queue_ || models_gpu_.empty()) return out; - if (configured_w_ <= 0 || configured_h_ <= 0) return out; +bool ViewportCore::encodeBoxPickToStaging(int& x, int& y, int& w, int& h, + std::uint64_t& padded_bpr_out, + std::uint64_t& needed_bytes_out) { + if (w <= 0 || h <= 0) return false; + if (!pick_pipeline_ || !device_ || !queue_ || models_gpu_.empty()) return false; + if (configured_w_ <= 0 || configured_h_ <= 0) return false; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x + w > configured_w_) w = configured_w_ - x; if (y + h > configured_h_) h = configured_h_ - y; - if (w <= 0 || h <= 0) return out; + if (w <= 0 || h <= 0) return false; ensurePickAttachments(configured_w_, configured_h_); - if (!pick_color_view_ || !pick_depth_view_) return out; + if (!pick_color_view_ || !pick_depth_view_) return false; // Padded bytes-per-row. R32UInt = 4 B/texel; align to 256 B. constexpr std::uint64_t kWgpuBytesPerRowAlign = 256; @@ -5144,7 +5156,7 @@ std::vector ViewportCore::picksInRect(int x, int y, int w, int h) box_pick_staging_buffer_ = wgpuDeviceCreateBuffer(device_, &sb); box_pick_staging_capacity_ = cap; } - if (!box_pick_staging_buffer_) return out; + if (!box_pick_staging_buffer_) return false; WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr); @@ -5212,22 +5224,14 @@ std::vector ViewportCore::picksInRect(int x, int y, int w, int h) wgpuCommandBufferRelease(cmd); wgpuCommandEncoderRelease(enc); - struct MapReq { bool done = false; bool ok = false; }; - MapReq req; - WGPUBufferMapCallbackInfo mcb = {}; - mcb.mode = kAsyncCbMode; - mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/, - void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - r->ok = (status == WGPUMapAsyncStatus_Success); - }; - mcb.userdata1 = &req; - wgpuBufferMapAsync(box_pick_staging_buffer_, WGPUMapMode_Read, - 0, needed_bytes, mcb); - while (!req.done) waitTickInstance(instance_); - if (!req.ok) return out; + padded_bpr_out = padded_bpr; + needed_bytes_out = needed_bytes; + return true; +} +std::vector ViewportCore::collectMappedBoxPickIds( + std::uint64_t padded_bpr, int w, int h, std::uint64_t needed_bytes) { + std::vector out; const std::uint8_t* mapped = static_cast( wgpuBufferGetConstMappedRange(box_pick_staging_buffer_, 0, needed_bytes)); std::unordered_set seen; @@ -5242,12 +5246,71 @@ std::vector ViewportCore::picksInRect(int x, int y, int w, int h) } } wgpuBufferUnmap(box_pick_staging_buffer_); - out.reserve(seen.size()); for (std::uint32_t id : seen) out.push_back(id); return out; } +std::vector ViewportCore::picksInRect(int x, int y, int w, int h) { + std::uint64_t padded_bpr = 0, needed_bytes = 0; + if (!encodeBoxPickToStaging(x, y, w, h, padded_bpr, needed_bytes)) return {}; + + struct MapReq { bool done = false; bool ok = false; }; + MapReq req; + WGPUBufferMapCallbackInfo mcb = {}; + mcb.mode = kAsyncCbMode; + mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/, + void* ud1, void* /*ud2*/) { + auto* r = static_cast(ud1); + r->done = true; + r->ok = (status == WGPUMapAsyncStatus_Success); + }; + mcb.userdata1 = &req; + wgpuBufferMapAsync(box_pick_staging_buffer_, WGPUMapMode_Read, 0, needed_bytes, mcb); + while (!req.done) waitTickInstance(instance_); + if (!req.ok) return {}; + return collectMappedBoxPickIds(padded_bpr, w, h, needed_bytes); +} + +#if defined(__EMSCRIPTEN__) +void ViewportCore::picksInRectAsync(int x, int y, int w, int h, + std::function)> cb) { + auto miss = [&cb]() { if (cb) cb({}); }; + if (box_pick_async_in_flight_) { miss(); return; } + std::uint64_t padded_bpr = 0, needed_bytes = 0; + if (!encodeBoxPickToStaging(x, y, w, h, padded_bpr, needed_bytes)) { miss(); return; } + + // Stash the (clamped) rect so the spontaneous map callback can walk the + // padded staging rows without recomputing. + box_pick_async_w_ = w; + box_pick_async_h_ = h; + box_pick_async_padded_bpr_ = padded_bpr; + box_pick_async_bytes_ = needed_bytes; + box_pick_async_in_flight_ = true; + box_pick_async_cb_ = std::move(cb); + + WGPUBufferMapCallbackInfo mcb = {}; + mcb.mode = kAsyncCbMode; // AllowSpontaneous on web + mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/, + void* ud1, void* /*ud2*/) { + auto* self = static_cast(ud1); + std::vector ids; + if (status == WGPUMapAsyncStatus_Success) { + ids = self->collectMappedBoxPickIds(self->box_pick_async_padded_bpr_, + self->box_pick_async_w_, + self->box_pick_async_h_, + self->box_pick_async_bytes_); + } + auto cb = std::move(self->box_pick_async_cb_); + self->box_pick_async_cb_ = nullptr; + self->box_pick_async_in_flight_ = false; + if (cb) cb(std::move(ids)); + }; + mcb.userdata1 = this; + wgpuBufferMapAsync(box_pick_staging_buffer_, WGPUMapMode_Read, 0, needed_bytes, mcb); +} +#endif + bool ViewportCore::pickSurfaceAt(int x_pixels, int y_pixels, std::uint32_t& object_id_out, Eigen::Vector3f& world_pos_out, diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 4de8b188b3..e6ea2c261e 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -632,6 +632,20 @@ public: // async (pickObjectAtAsync) readbacks. Caller validates bounds/attachments. void encodePickReadbackToStaging(int x_pixels, int y_pixels, bool want_normal); + // Encode the pick pass + copy the (x,y,w,h) object_id sub-rect into + // box_pick_staging_buffer_ and submit. Clamps the rect (x/y/w/h in-out) and + // reports the padded bytes-per-row + total mapped size. Shared by the sync + // picksInRect and async picksInRectAsync — they differ only in the map. + // False if nothing is pickable or the rect is empty. + bool encodeBoxPickToStaging(int& x, int& y, int& w, int& h, + std::uint64_t& padded_bpr_out, + std::uint64_t& needed_bytes_out); + // Read the (already-mapped) box-pick staging buffer → unique non-zero ids in + // the w×h rect (rows padded to padded_bpr). Unmaps before returning. + std::vector collectMappedBoxPickIds(std::uint64_t padded_bpr, + int w, int h, + std::uint64_t needed_bytes); + // Tear down every pick-owned wgpu resource (pipeline + MRTs + // staging buffers). Called from shutdown() before device_ dies. void releasePickResources(); @@ -648,6 +662,11 @@ public: // Marks selection_ dirty for the next render's flush. void applyPickToSelection(std::uint32_t object_id, bool add, bool remove); + // Apply a marquee box-pick result to the selection: plain = replace with + // `ids`, add = union, remove = subtract. Schedules a frame. + void applyMarqueeToSelection(const std::vector& ids, + 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 @@ -673,9 +692,18 @@ public: // Marquee box select: encode the pick pass, copy the (x, y, w, h) // sub-rect of the object_id MRT back, return the set of unique - // non-zero ids. Synchronous (rare interaction). + // non-zero ids. Synchronous (rare interaction) — desktop only path. std::vector picksInRect(int x, int y, int w, int h); +#if defined(__EMSCRIPTEN__) + // Async marquee box select for web (the sync spin-map would hang the JS + // loop). Same pick pass + rect copy as picksInRect, mapped via a spontaneous + // callback that delivers the unique non-zero ids to `cb`. One in flight at a + // time (a box-pick issued while another is mapping is dropped → cb({})). + void picksInRectAsync(int x, int y, int w, int h, + std::function)> cb); +#endif + // Run pickObjectAt + raycast against every instance carrying the // hit object_id, then return the closest hit's world position, // world normal, and (optionally) the bounding-sphere radius. The @@ -919,6 +947,14 @@ private: // pick_async_cb_ fires with object_id when the spontaneous map resolves. bool pick_async_in_flight_ = false; std::function pick_async_cb_; + // Async box-pick (marquee) state (web). Rect dims are stashed so the + // spontaneous map callback knows how to walk the padded staging rows. + bool box_pick_async_in_flight_ = false; + std::function)> box_pick_async_cb_; + int box_pick_async_w_ = 0; + int box_pick_async_h_ = 0; + std::uint64_t box_pick_async_padded_bpr_ = 0; + std::uint64_t box_pick_async_bytes_ = 0; #endif // ---- Frame uniforms + selection bind ---------------------------------- diff --git a/src/ifcviewer/tests/test_viewport_camera.cpp b/src/ifcviewer/tests/test_viewport_camera.cpp index cca86eed37..01f4a5f018 100644 --- a/src/ifcviewer/tests/test_viewport_camera.cpp +++ b/src/ifcviewer/tests/test_viewport_camera.cpp @@ -196,3 +196,25 @@ TEST_CASE("hideSelected hides the selection; showAll restores", "[camera][visibi core.showAll(); REQUIRE(core.hiddenCount() == 0); } + +TEST_CASE("applyMarqueeToSelection: replace / add / remove", "[camera][selection]") { + MockHost host; ViewportCore core(&host); + // No public selection accessor, so verify via hideSelected → hiddenCount. + SECTION("plain marquee replaces the selection") { + core.applyMarqueeToSelection({1, 2, 3}, /*add*/false, /*remove*/false); + core.hideSelected(); + REQUIRE(core.hiddenCount() == 3); + } + SECTION("add unions, remove subtracts") { + core.applyMarqueeToSelection({5}, false, false); // replace → {5} + core.applyMarqueeToSelection({6, 7}, true, false); // add → {5,6,7} + core.applyMarqueeToSelection({6}, false, true); // remove → {5,7} + core.hideSelected(); + REQUIRE(core.hiddenCount() == 2); + } + SECTION("id 0 is ignored") { + core.applyMarqueeToSelection({0, 9, 0}, false, false); + core.hideSelected(); + REQUIRE(core.hiddenCount() == 1); + } +} From 791ff26697b40c0e9f7e5a2a452286a7100af151 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 09:39:27 +1000 Subject: [PATCH 06/11] Update Bonsai viewer licensing docs Update Bonsai viewer headers to identify Bonsai and GPL licensing, add Bonsai Viewer documentation, and document debug output capture. Generated with the assistance of an AI coding tool. --- src/bonsaiviewer/CMakeLists.txt | 12 +- src/bonsaiviewer/ElementRegistry.cpp | 12 +- src/bonsaiviewer/ElementRegistry.h | 12 +- src/bonsaiviewer/MainWindow.cpp | 12 +- src/bonsaiviewer/MainWindow.h | 12 +- src/bonsaiviewer/Measurement.cpp | 12 +- src/bonsaiviewer/Measurement.h | 12 +- src/bonsaiviewer/SessionState.cpp | 12 +- src/bonsaiviewer/SessionState.h | 12 +- src/bonsaiviewer/ViewerSettings.cpp | 12 +- src/bonsaiviewer/ViewerSettings.h | 12 +- src/bonsaiviewer/components/Buttons.cpp | 12 +- src/bonsaiviewer/components/Buttons.h | 12 +- src/bonsaiviewer/components/Dialog.cpp | 12 +- src/bonsaiviewer/components/Dialog.h | 12 +- src/bonsaiviewer/components/KeyValueTable.cpp | 12 +- src/bonsaiviewer/components/KeyValueTable.h | 12 +- src/bonsaiviewer/components/Panel.cpp | 12 +- src/bonsaiviewer/components/Panel.h | 12 +- src/bonsaiviewer/components/Section.cpp | 12 +- src/bonsaiviewer/components/Section.h | 12 +- src/bonsaiviewer/components/Style.cpp | 12 +- src/bonsaiviewer/components/Style.h | 12 +- src/bonsaiviewer/components/SvgIcon.cpp | 12 +- src/bonsaiviewer/components/SvgIcon.h | 12 +- src/bonsaiviewer/components/Tabs.cpp | 12 +- src/bonsaiviewer/components/Tabs.h | 12 +- src/bonsaiviewer/docs/debug-output.rst | 160 ++++++++++++++++++ src/bonsaiviewer/main.cpp | 12 +- .../modules/connectors/Discovery.cpp | 12 +- .../modules/connectors/Discovery.h | 12 +- .../modules/connectors/PickerDialog.cpp | 12 +- .../modules/connectors/PickerDialog.h | 12 +- .../modules/connectors/Process.cpp | 12 +- src/bonsaiviewer/modules/connectors/Process.h | 12 +- .../modules/connectors/Registry.cpp | 12 +- .../modules/connectors/Registry.h | 12 +- .../modules/models/AddModelDialog.cpp | 12 +- .../modules/models/AddModelDialog.h | 12 +- src/bonsaiviewer/modules/models/Commands.cpp | 12 +- src/bonsaiviewer/modules/models/Commands.h | 12 +- .../modules/models/FederationItemModel.cpp | 12 +- .../modules/models/FederationItemModel.h | 12 +- src/bonsaiviewer/modules/models/Panel.cpp | 12 +- src/bonsaiviewer/modules/models/Panel.h | 12 +- .../modules/models/SettingsDialog.cpp | 12 +- .../modules/models/SettingsDialog.h | 12 +- .../modules/models/SettingsView.cpp | 12 +- .../modules/models/SettingsView.h | 12 +- src/bonsaiviewer/modules/models/Types.h | 12 +- src/bonsaiviewer/modules/models/View.cpp | 12 +- src/bonsaiviewer/modules/models/View.h | 12 +- src/bonsaiviewer/modules/project/Commands.cpp | 12 +- src/bonsaiviewer/modules/project/Commands.h | 12 +- .../modules/project/RecentProjects.cpp | 12 +- .../modules/project/RecentProjects.h | 12 +- .../modules/project/SaveProjectDialog.cpp | 12 +- .../modules/project/SaveProjectDialog.h | 12 +- src/bonsaiviewer/modules/properties/Panel.cpp | 12 +- src/bonsaiviewer/modules/properties/Panel.h | 12 +- src/bonsaiviewer/modules/properties/Types.h | 12 +- src/bonsaiviewer/modules/properties/View.cpp | 12 +- src/bonsaiviewer/modules/properties/View.h | 12 +- src/bonsaiviewer/modules/settings/Dialog.cpp | 12 +- src/bonsaiviewer/modules/settings/Dialog.h | 12 +- .../modules/spatial_hierarchy/Panel.cpp | 12 +- .../modules/spatial_hierarchy/Panel.h | 12 +- .../modules/spatial_hierarchy/Types.h | 12 +- .../modules/spatial_hierarchy/View.cpp | 12 +- .../modules/spatial_hierarchy/View.h | 12 +- src/bonsaiviewer/modules/todo/Panel.cpp | 12 +- src/bonsaiviewer/modules/todo/Panel.h | 12 +- .../modules/viewport/Commands.cpp | 12 +- src/bonsaiviewer/modules/viewport/Commands.h | 12 +- src/bonsaiviewer/modules/viewport/Panel.cpp | 12 +- src/bonsaiviewer/modules/viewport/Panel.h | 12 +- src/bonsaiviewer/modules/viewport/View.cpp | 12 +- src/bonsaiviewer/modules/viewport/View.h | 12 +- .../docs/bonsai-viewer.rst | 10 ++ src/ifcopenshell-python/docs/index.rst | 1 + src/ifcopenshell-python/docs/introduction.rst | 9 +- 81 files changed, 640 insertions(+), 464 deletions(-) create mode 100644 src/bonsaiviewer/docs/debug-output.rst create mode 100644 src/ifcopenshell-python/docs/bonsai-viewer.rst diff --git a/src/bonsaiviewer/CMakeLists.txt b/src/bonsaiviewer/CMakeLists.txt index 7a19ea418f..ae16c4af4a 100644 --- a/src/bonsaiviewer/CMakeLists.txt +++ b/src/bonsaiviewer/CMakeLists.txt @@ -1,19 +1,19 @@ # This file was generated with the assistance of an AI coding tool. ################################################################################ # # -# This file is part of IfcOpenShell. # +# This file is part of Bonsai. # # # -# IfcOpenShell is free software: you can redistribute it and/or modify # -# it under the terms of the Lesser GNU General Public License as published by # +# Bonsai is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3.0 of the License, or # # (at your option) any later version. # # # -# IfcOpenShell is distributed in the hope that it will be useful, # +# Bonsai is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# Lesser GNU General Public License for more details. # +# GNU General Public License for more details. # # # -# You should have received a copy of the Lesser GNU General Public License # +# You should have received a copy of the GNU General Public License # # along with this program. If not, see . # # # ################################################################################ diff --git a/src/bonsaiviewer/ElementRegistry.cpp b/src/bonsaiviewer/ElementRegistry.cpp index 33ec5c8cdd..ed83bd1504 100644 --- a/src/bonsaiviewer/ElementRegistry.cpp +++ b/src/bonsaiviewer/ElementRegistry.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/ElementRegistry.h b/src/bonsaiviewer/ElementRegistry.h index 469e4b5996..5f5cc7a76d 100644 --- a/src/bonsaiviewer/ElementRegistry.h +++ b/src/bonsaiviewer/ElementRegistry.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/MainWindow.cpp b/src/bonsaiviewer/MainWindow.cpp index 4aee23127c..452274b827 100644 --- a/src/bonsaiviewer/MainWindow.cpp +++ b/src/bonsaiviewer/MainWindow.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/MainWindow.h b/src/bonsaiviewer/MainWindow.h index 6f8567ac86..9a8a2ed43e 100644 --- a/src/bonsaiviewer/MainWindow.h +++ b/src/bonsaiviewer/MainWindow.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/Measurement.cpp b/src/bonsaiviewer/Measurement.cpp index b24c072269..dd3175df25 100644 --- a/src/bonsaiviewer/Measurement.cpp +++ b/src/bonsaiviewer/Measurement.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/Measurement.h b/src/bonsaiviewer/Measurement.h index 83f2dbfba5..253eee279b 100644 --- a/src/bonsaiviewer/Measurement.h +++ b/src/bonsaiviewer/Measurement.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/SessionState.cpp b/src/bonsaiviewer/SessionState.cpp index 4a5563e05d..ee0b5617c7 100644 --- a/src/bonsaiviewer/SessionState.cpp +++ b/src/bonsaiviewer/SessionState.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/SessionState.h b/src/bonsaiviewer/SessionState.h index 902a933ccb..0ae486912e 100644 --- a/src/bonsaiviewer/SessionState.h +++ b/src/bonsaiviewer/SessionState.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/ViewerSettings.cpp b/src/bonsaiviewer/ViewerSettings.cpp index c7e92a9a6c..196cb806f4 100644 --- a/src/bonsaiviewer/ViewerSettings.cpp +++ b/src/bonsaiviewer/ViewerSettings.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/ViewerSettings.h b/src/bonsaiviewer/ViewerSettings.h index a3eea1ac15..96ef1e87d8 100644 --- a/src/bonsaiviewer/ViewerSettings.h +++ b/src/bonsaiviewer/ViewerSettings.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Buttons.cpp b/src/bonsaiviewer/components/Buttons.cpp index 622fd66264..bce4f87291 100644 --- a/src/bonsaiviewer/components/Buttons.cpp +++ b/src/bonsaiviewer/components/Buttons.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Buttons.h b/src/bonsaiviewer/components/Buttons.h index 39dc65d664..e8b26c287b 100644 --- a/src/bonsaiviewer/components/Buttons.h +++ b/src/bonsaiviewer/components/Buttons.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Dialog.cpp b/src/bonsaiviewer/components/Dialog.cpp index ce7a9ffe45..1f308440d4 100644 --- a/src/bonsaiviewer/components/Dialog.cpp +++ b/src/bonsaiviewer/components/Dialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Dialog.h b/src/bonsaiviewer/components/Dialog.h index a4f7fc47f8..b8b3cf0a9a 100644 --- a/src/bonsaiviewer/components/Dialog.h +++ b/src/bonsaiviewer/components/Dialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/KeyValueTable.cpp b/src/bonsaiviewer/components/KeyValueTable.cpp index 191cad8b8e..9f0012e7e4 100644 --- a/src/bonsaiviewer/components/KeyValueTable.cpp +++ b/src/bonsaiviewer/components/KeyValueTable.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/KeyValueTable.h b/src/bonsaiviewer/components/KeyValueTable.h index 714ec8d668..2c839981aa 100644 --- a/src/bonsaiviewer/components/KeyValueTable.h +++ b/src/bonsaiviewer/components/KeyValueTable.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Panel.cpp b/src/bonsaiviewer/components/Panel.cpp index 9969f9d754..77f42a0d1f 100644 --- a/src/bonsaiviewer/components/Panel.cpp +++ b/src/bonsaiviewer/components/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Panel.h b/src/bonsaiviewer/components/Panel.h index 9cbdace240..fee72dd765 100644 --- a/src/bonsaiviewer/components/Panel.h +++ b/src/bonsaiviewer/components/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Section.cpp b/src/bonsaiviewer/components/Section.cpp index 5caedd3d68..f66753dd4b 100644 --- a/src/bonsaiviewer/components/Section.cpp +++ b/src/bonsaiviewer/components/Section.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Section.h b/src/bonsaiviewer/components/Section.h index 8726e32ece..d083ae8870 100644 --- a/src/bonsaiviewer/components/Section.h +++ b/src/bonsaiviewer/components/Section.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Style.cpp b/src/bonsaiviewer/components/Style.cpp index 6f1196a3f1..fd1bcffc1a 100644 --- a/src/bonsaiviewer/components/Style.cpp +++ b/src/bonsaiviewer/components/Style.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Style.h b/src/bonsaiviewer/components/Style.h index 90a900e50c..5dc39f73b9 100644 --- a/src/bonsaiviewer/components/Style.h +++ b/src/bonsaiviewer/components/Style.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/SvgIcon.cpp b/src/bonsaiviewer/components/SvgIcon.cpp index 92091c631f..0ab12d3f7e 100644 --- a/src/bonsaiviewer/components/SvgIcon.cpp +++ b/src/bonsaiviewer/components/SvgIcon.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/SvgIcon.h b/src/bonsaiviewer/components/SvgIcon.h index b0577b8638..0098e28c04 100644 --- a/src/bonsaiviewer/components/SvgIcon.h +++ b/src/bonsaiviewer/components/SvgIcon.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Tabs.cpp b/src/bonsaiviewer/components/Tabs.cpp index c1af24c058..acdcdec205 100644 --- a/src/bonsaiviewer/components/Tabs.cpp +++ b/src/bonsaiviewer/components/Tabs.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/components/Tabs.h b/src/bonsaiviewer/components/Tabs.h index a9e40c2677..511b9f936a 100644 --- a/src/bonsaiviewer/components/Tabs.h +++ b/src/bonsaiviewer/components/Tabs.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/docs/debug-output.rst b/src/bonsaiviewer/docs/debug-output.rst new file mode 100644 index 0000000000..a5110098bf --- /dev/null +++ b/src/bonsaiviewer/docs/debug-output.rst @@ -0,0 +1,160 @@ +.. This file was generated with the assistance of an AI coding tool. + +Capturing debug output +====================== + +When BonsaiViewer misbehaves — a crash, a black viewport, a hang — the +single most useful thing you can hand off to a developer is a +**stack trace at the point of failure**. This page covers how to capture +one on Linux, macOS, and Windows. + +Pre-flight: always run from a terminal +-------------------------------------- + +GUI launches discard everything BonsaiViewer prints. Double-clicking +``BonsaiViewer`` (or ``open -a`` on macOS, or a Start Menu shortcut on +Windows) hides every ``qWarning``, ``[wgpu] …`` line, and runtime error +message. **Always launch from a terminal first** when something is +wrong: + +.. code-block:: bash + + # Linux + /path/to/BonsaiViewer + + # macOS + /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer + +.. code-block:: bat + + :: Windows + "C:\path\to\BonsaiViewer.exe" + +If the problem reproduces just by running from a terminal, copy the +console output verbatim — it's usually enough on its own. If the +process disappears without printing anything useful, you need a +debugger; pick the section below for your OS. + +Linux +----- + +GDB is the standard debugger and ships with every distribution. + +.. code-block:: bash + + gdb --args /path/to/BonsaiViewer + (gdb) run + # ... reproduce the crash ... + (gdb) bt 30 # backtrace of the crashing thread, top 30 frames + (gdb) thread apply all bt # all threads, full backtraces + +If BonsaiViewer crashed *without* a debugger and you have core dumps +enabled (``ulimit -c unlimited``), open the core file directly: + +.. code-block:: bash + + gdb /path/to/BonsaiViewer /path/to/core. + (gdb) bt 30 + +Most distributions hand crashes to ``systemd-coredump``; ``coredumpctl +list`` shows recent dumps and ``coredumpctl debug BonsaiViewer`` jumps +straight into GDB. + +For runtime memory errors (use-after-free, buffer overflows) that don't +crash immediately, run under ``valgrind`` or rebuild with ASAN +(``-fsanitize=address``). + +macOS +----- + +LLDB is bundled with the Xcode Command Line Tools (``xcode-select +--install`` if you don't already have it). + +.. code-block:: bash + + lldb -- /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer + (lldb) run + # ... reproduce the crash ... + (lldb) bt 30 # backtrace of the crashing thread + (lldb) thread backtrace all # all threads + +macOS also writes automatic crash reports (``.ips`` files since macOS +12) at ``~/Library/Logs/DiagnosticReports/``. ``ls -lt +~/Library/Logs/DiagnosticReports/ | head`` shows the most recent ones. +Open the newest ``BonsaiViewer-*.ips`` in Console.app or any text +editor — the JSON contains a full symbolicated stack. + +Obj-C use-after-free aborts (``message sent to deallocated instance +…``) often show up as a vague segfault with NSZombieEnabled disabled. +Re-run with zombies turned on to get the real recipient of the dead +message: + +.. code-block:: bash + + NSZombieEnabled=YES lldb -- /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer + (lldb) run + +Windows +------- + +There's no preinstalled debugger on Windows; install **WinDbg Preview** +from the Microsoft Store (free) or grab **ProcDump** as a single +standalone ``.exe`` from +https://learn.microsoft.com/en-us/sysinternals/downloads/procdump. + +**Live debugging with WinDbg Preview** + +1. Install ``WinDbg Preview`` from the Microsoft Store. +2. ``File → Launch executable → Browse`` to ``BonsaiViewer.exe``. +3. Press F5 to start. Reproduce the crash. +4. When WinDbg breaks in, type ``kn30`` in the command bar at the + bottom — that's the stack trace with frame numbers. + +**Post-mortem dump with ProcDump (no install)** + +Useful when the crash is hard to reach inside a debugger (e.g. it +happens during a Bonsai-launched child process): + +.. code-block:: bat + + procdump -ma -e BonsaiViewer.exe -accepteula + +In a second terminal, run ``BonsaiViewer.exe``. When it crashes, +ProcDump writes ``BonsaiViewer.exe_YYMMDD_HHMMSS.dmp`` in the current +directory. Open that ``.dmp`` in WinDbg Preview +(``File → Open dump file``) and type ``kn30``. + +**Getting symbolicated frames** + +The shipping ``BonsaiViewer.exe`` doesn't include ``.pdb`` symbols. The +stack will show DLL boundaries (which is often enough to narrow a bug +down to ``BonsaiViewer.exe`` vs. ``wgpu_native.dll`` vs. +``KERNELBASE.dll``), but function names will be addresses. To get +symbol names: + +1. Find the matching ``BonsaiViewer.pdb`` in the build artifacts of the + workflow run that produced your ``.exe`` (under "Upload Build + Logs"). +2. Drop it next to ``BonsaiViewer.exe``. +3. Re-launch under WinDbg; it'll pick up the symbols automatically. + +For runtime memory errors that don't crash immediately, the +**Application Verifier** (in the Windows SDK) is the closest analogue +to valgrind / ASAN. + +What to send the developer +-------------------------- + +A stack trace alone is usually enough. If the crash is reliable but +needs specific input to trigger, also include: + +1. The exact command line you launched with (env vars matter — e.g. + ``WGPU_PRESENT_MODE=immediate``). +2. The model file (or its publicly-shareable equivalent) that triggers + the crash, plus the smallest model that *doesn't* trigger it. +3. The terminal output from before the crash (the ``[wgpu …]`` / + ``Sidecar metadata read: …`` / ``Streamer done: …`` lines tell us + which subsystem was active when things went south). +4. Your GPU + driver version and OS version. On Linux: ``glxinfo | + grep "OpenGL renderer"`` and ``uname -r``. On macOS: Apple menu → + About this Mac. On Windows: ``dxdiag``. diff --git a/src/bonsaiviewer/main.cpp b/src/bonsaiviewer/main.cpp index ff2f79a87a..59ef765e19 100644 --- a/src/bonsaiviewer/main.cpp +++ b/src/bonsaiviewer/main.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Discovery.cpp b/src/bonsaiviewer/modules/connectors/Discovery.cpp index c60ddac38e..e4f3d6b877 100644 --- a/src/bonsaiviewer/modules/connectors/Discovery.cpp +++ b/src/bonsaiviewer/modules/connectors/Discovery.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Discovery.h b/src/bonsaiviewer/modules/connectors/Discovery.h index 6932dde740..876d982fcf 100644 --- a/src/bonsaiviewer/modules/connectors/Discovery.h +++ b/src/bonsaiviewer/modules/connectors/Discovery.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/PickerDialog.cpp b/src/bonsaiviewer/modules/connectors/PickerDialog.cpp index 4cc11c4eef..710009bdb1 100644 --- a/src/bonsaiviewer/modules/connectors/PickerDialog.cpp +++ b/src/bonsaiviewer/modules/connectors/PickerDialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/PickerDialog.h b/src/bonsaiviewer/modules/connectors/PickerDialog.h index 40083e987a..b1011505c4 100644 --- a/src/bonsaiviewer/modules/connectors/PickerDialog.h +++ b/src/bonsaiviewer/modules/connectors/PickerDialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Process.cpp b/src/bonsaiviewer/modules/connectors/Process.cpp index b85e7d54f5..6dbd1b900a 100644 --- a/src/bonsaiviewer/modules/connectors/Process.cpp +++ b/src/bonsaiviewer/modules/connectors/Process.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Process.h b/src/bonsaiviewer/modules/connectors/Process.h index ae83533e51..edaf682962 100644 --- a/src/bonsaiviewer/modules/connectors/Process.h +++ b/src/bonsaiviewer/modules/connectors/Process.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Registry.cpp b/src/bonsaiviewer/modules/connectors/Registry.cpp index 1a244d8c1b..bb4a34df26 100644 --- a/src/bonsaiviewer/modules/connectors/Registry.cpp +++ b/src/bonsaiviewer/modules/connectors/Registry.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/connectors/Registry.h b/src/bonsaiviewer/modules/connectors/Registry.h index 6947332c99..3eb9f555a7 100644 --- a/src/bonsaiviewer/modules/connectors/Registry.h +++ b/src/bonsaiviewer/modules/connectors/Registry.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/AddModelDialog.cpp b/src/bonsaiviewer/modules/models/AddModelDialog.cpp index 0bb796bbb7..98df960344 100644 --- a/src/bonsaiviewer/modules/models/AddModelDialog.cpp +++ b/src/bonsaiviewer/modules/models/AddModelDialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/AddModelDialog.h b/src/bonsaiviewer/modules/models/AddModelDialog.h index eda3c1061a..d12bc9fe9e 100644 --- a/src/bonsaiviewer/modules/models/AddModelDialog.h +++ b/src/bonsaiviewer/modules/models/AddModelDialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/Commands.cpp b/src/bonsaiviewer/modules/models/Commands.cpp index 4aadb3ca5a..8ed35b4b07 100644 --- a/src/bonsaiviewer/modules/models/Commands.cpp +++ b/src/bonsaiviewer/modules/models/Commands.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/Commands.h b/src/bonsaiviewer/modules/models/Commands.h index 9130508b04..1904ad66de 100644 --- a/src/bonsaiviewer/modules/models/Commands.h +++ b/src/bonsaiviewer/modules/models/Commands.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/FederationItemModel.cpp b/src/bonsaiviewer/modules/models/FederationItemModel.cpp index 8c206e24a3..290052eead 100644 --- a/src/bonsaiviewer/modules/models/FederationItemModel.cpp +++ b/src/bonsaiviewer/modules/models/FederationItemModel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/FederationItemModel.h b/src/bonsaiviewer/modules/models/FederationItemModel.h index 3cc094dbbd..4d8ece84f8 100644 --- a/src/bonsaiviewer/modules/models/FederationItemModel.h +++ b/src/bonsaiviewer/modules/models/FederationItemModel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/Panel.cpp b/src/bonsaiviewer/modules/models/Panel.cpp index 8567e6cca3..4a3a630a34 100644 --- a/src/bonsaiviewer/modules/models/Panel.cpp +++ b/src/bonsaiviewer/modules/models/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/Panel.h b/src/bonsaiviewer/modules/models/Panel.h index dadea2ec98..6316847687 100644 --- a/src/bonsaiviewer/modules/models/Panel.h +++ b/src/bonsaiviewer/modules/models/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/SettingsDialog.cpp b/src/bonsaiviewer/modules/models/SettingsDialog.cpp index a864aa674d..998eac6310 100644 --- a/src/bonsaiviewer/modules/models/SettingsDialog.cpp +++ b/src/bonsaiviewer/modules/models/SettingsDialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/SettingsDialog.h b/src/bonsaiviewer/modules/models/SettingsDialog.h index 68d70b5a72..70a0ff5b7f 100644 --- a/src/bonsaiviewer/modules/models/SettingsDialog.h +++ b/src/bonsaiviewer/modules/models/SettingsDialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/SettingsView.cpp b/src/bonsaiviewer/modules/models/SettingsView.cpp index bfe9f2196a..53389be52d 100644 --- a/src/bonsaiviewer/modules/models/SettingsView.cpp +++ b/src/bonsaiviewer/modules/models/SettingsView.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/SettingsView.h b/src/bonsaiviewer/modules/models/SettingsView.h index 1d5114d4d8..346d1014db 100644 --- a/src/bonsaiviewer/modules/models/SettingsView.h +++ b/src/bonsaiviewer/modules/models/SettingsView.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/Types.h b/src/bonsaiviewer/modules/models/Types.h index 1e80a3e939..c496e16687 100644 --- a/src/bonsaiviewer/modules/models/Types.h +++ b/src/bonsaiviewer/modules/models/Types.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/View.cpp b/src/bonsaiviewer/modules/models/View.cpp index 2c158e21e1..9a8619c682 100644 --- a/src/bonsaiviewer/modules/models/View.cpp +++ b/src/bonsaiviewer/modules/models/View.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/models/View.h b/src/bonsaiviewer/modules/models/View.h index 6fb67eca09..7ae862b94b 100644 --- a/src/bonsaiviewer/modules/models/View.h +++ b/src/bonsaiviewer/modules/models/View.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/Commands.cpp b/src/bonsaiviewer/modules/project/Commands.cpp index 7761fdb474..2a2e5122fe 100644 --- a/src/bonsaiviewer/modules/project/Commands.cpp +++ b/src/bonsaiviewer/modules/project/Commands.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/Commands.h b/src/bonsaiviewer/modules/project/Commands.h index 386076c116..a821c24ec8 100644 --- a/src/bonsaiviewer/modules/project/Commands.h +++ b/src/bonsaiviewer/modules/project/Commands.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/RecentProjects.cpp b/src/bonsaiviewer/modules/project/RecentProjects.cpp index 21ed2652d6..b97390a72f 100644 --- a/src/bonsaiviewer/modules/project/RecentProjects.cpp +++ b/src/bonsaiviewer/modules/project/RecentProjects.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/RecentProjects.h b/src/bonsaiviewer/modules/project/RecentProjects.h index 27bf79fd8e..07fec3c6fa 100644 --- a/src/bonsaiviewer/modules/project/RecentProjects.h +++ b/src/bonsaiviewer/modules/project/RecentProjects.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/SaveProjectDialog.cpp b/src/bonsaiviewer/modules/project/SaveProjectDialog.cpp index 872849d0f2..f77e54d1aa 100644 --- a/src/bonsaiviewer/modules/project/SaveProjectDialog.cpp +++ b/src/bonsaiviewer/modules/project/SaveProjectDialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/project/SaveProjectDialog.h b/src/bonsaiviewer/modules/project/SaveProjectDialog.h index 0e49d69fc0..7d009bbe6d 100644 --- a/src/bonsaiviewer/modules/project/SaveProjectDialog.h +++ b/src/bonsaiviewer/modules/project/SaveProjectDialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/properties/Panel.cpp b/src/bonsaiviewer/modules/properties/Panel.cpp index 991bf0018d..d45d858681 100644 --- a/src/bonsaiviewer/modules/properties/Panel.cpp +++ b/src/bonsaiviewer/modules/properties/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/properties/Panel.h b/src/bonsaiviewer/modules/properties/Panel.h index 7e52cbb084..1e5558be3a 100644 --- a/src/bonsaiviewer/modules/properties/Panel.h +++ b/src/bonsaiviewer/modules/properties/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/properties/Types.h b/src/bonsaiviewer/modules/properties/Types.h index 7c4f81ba61..a6a285aa29 100644 --- a/src/bonsaiviewer/modules/properties/Types.h +++ b/src/bonsaiviewer/modules/properties/Types.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/properties/View.cpp b/src/bonsaiviewer/modules/properties/View.cpp index 02c022c18c..0e23e57c5c 100644 --- a/src/bonsaiviewer/modules/properties/View.cpp +++ b/src/bonsaiviewer/modules/properties/View.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/properties/View.h b/src/bonsaiviewer/modules/properties/View.h index 1805a11fb5..9db8fbce61 100644 --- a/src/bonsaiviewer/modules/properties/View.h +++ b/src/bonsaiviewer/modules/properties/View.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/settings/Dialog.cpp b/src/bonsaiviewer/modules/settings/Dialog.cpp index f93e114531..699b954906 100644 --- a/src/bonsaiviewer/modules/settings/Dialog.cpp +++ b/src/bonsaiviewer/modules/settings/Dialog.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/settings/Dialog.h b/src/bonsaiviewer/modules/settings/Dialog.h index 2266060fc9..79d6070c3c 100644 --- a/src/bonsaiviewer/modules/settings/Dialog.h +++ b/src/bonsaiviewer/modules/settings/Dialog.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/spatial_hierarchy/Panel.cpp b/src/bonsaiviewer/modules/spatial_hierarchy/Panel.cpp index 0b8dfaee7b..927f170e34 100644 --- a/src/bonsaiviewer/modules/spatial_hierarchy/Panel.cpp +++ b/src/bonsaiviewer/modules/spatial_hierarchy/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/spatial_hierarchy/Panel.h b/src/bonsaiviewer/modules/spatial_hierarchy/Panel.h index f3b10f7294..aaa91c858a 100644 --- a/src/bonsaiviewer/modules/spatial_hierarchy/Panel.h +++ b/src/bonsaiviewer/modules/spatial_hierarchy/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/spatial_hierarchy/Types.h b/src/bonsaiviewer/modules/spatial_hierarchy/Types.h index 2f5f0301c3..1818944648 100644 --- a/src/bonsaiviewer/modules/spatial_hierarchy/Types.h +++ b/src/bonsaiviewer/modules/spatial_hierarchy/Types.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/spatial_hierarchy/View.cpp b/src/bonsaiviewer/modules/spatial_hierarchy/View.cpp index 05b8615f05..353fecf7dc 100644 --- a/src/bonsaiviewer/modules/spatial_hierarchy/View.cpp +++ b/src/bonsaiviewer/modules/spatial_hierarchy/View.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/spatial_hierarchy/View.h b/src/bonsaiviewer/modules/spatial_hierarchy/View.h index c3260f469f..6e93e2b725 100644 --- a/src/bonsaiviewer/modules/spatial_hierarchy/View.h +++ b/src/bonsaiviewer/modules/spatial_hierarchy/View.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/todo/Panel.cpp b/src/bonsaiviewer/modules/todo/Panel.cpp index 5042672e75..51c0bd7b92 100644 --- a/src/bonsaiviewer/modules/todo/Panel.cpp +++ b/src/bonsaiviewer/modules/todo/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/todo/Panel.h b/src/bonsaiviewer/modules/todo/Panel.h index d5f50716f4..155bf1825e 100644 --- a/src/bonsaiviewer/modules/todo/Panel.h +++ b/src/bonsaiviewer/modules/todo/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/Commands.cpp b/src/bonsaiviewer/modules/viewport/Commands.cpp index a6166d07b0..3651338f9f 100644 --- a/src/bonsaiviewer/modules/viewport/Commands.cpp +++ b/src/bonsaiviewer/modules/viewport/Commands.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/Commands.h b/src/bonsaiviewer/modules/viewport/Commands.h index dea82a32ee..0c1d0021bd 100644 --- a/src/bonsaiviewer/modules/viewport/Commands.h +++ b/src/bonsaiviewer/modules/viewport/Commands.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/Panel.cpp b/src/bonsaiviewer/modules/viewport/Panel.cpp index ca6c694028..45f61c095e 100644 --- a/src/bonsaiviewer/modules/viewport/Panel.cpp +++ b/src/bonsaiviewer/modules/viewport/Panel.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/Panel.h b/src/bonsaiviewer/modules/viewport/Panel.h index 6d6db34080..aeae45060c 100644 --- a/src/bonsaiviewer/modules/viewport/Panel.h +++ b/src/bonsaiviewer/modules/viewport/Panel.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/View.cpp b/src/bonsaiviewer/modules/viewport/View.cpp index 1f49941f71..60640b4cd1 100644 --- a/src/bonsaiviewer/modules/viewport/View.cpp +++ b/src/bonsaiviewer/modules/viewport/View.cpp @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/bonsaiviewer/modules/viewport/View.h b/src/bonsaiviewer/modules/viewport/View.h index 196b4b4cff..be36e6987f 100644 --- a/src/bonsaiviewer/modules/viewport/View.h +++ b/src/bonsaiviewer/modules/viewport/View.h @@ -1,19 +1,19 @@ // This file was generated with the assistance of an AI coding tool. /******************************************************************************** * * - * This file is part of IfcOpenShell. * + * This file is part of Bonsai. * * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * + * Bonsai is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * - * IfcOpenShell is distributed in the hope that it will be useful, * + * Bonsai is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * Lesser GNU General Public License for more details. * + * GNU General Public License for more details. * * * - * You should have received a copy of the Lesser GNU General Public License * + * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ diff --git a/src/ifcopenshell-python/docs/bonsai-viewer.rst b/src/ifcopenshell-python/docs/bonsai-viewer.rst new file mode 100644 index 0000000000..7d546fb13b --- /dev/null +++ b/src/ifcopenshell-python/docs/bonsai-viewer.rst @@ -0,0 +1,10 @@ +.. This file was generated with the assistance of an AI coding tool. + +Bonsai Viewer +============= + +Bonsai Viewer is a high performance viewing and coordination tool for IFC +models. It is designed for quickly opening large projects, inspecting model +geometry and metadata, and coordinating issues across federated project data. + +For more information, visit the `Bonsai website `_. diff --git a/src/ifcopenshell-python/docs/index.rst b/src/ifcopenshell-python/docs/index.rst index 895dbece88..a460b55f09 100644 --- a/src/ifcopenshell-python/docs/index.rst +++ b/src/ifcopenshell-python/docs/index.rst @@ -13,6 +13,7 @@ Let's learn IfcOpenShell! ifcopenshell-python ifcconvert bonsai + bonsai-viewer .. toctree:: :hidden: diff --git a/src/ifcopenshell-python/docs/introduction.rst b/src/ifcopenshell-python/docs/introduction.rst index 6357667c19..427589a6db 100644 --- a/src/ifcopenshell-python/docs/introduction.rst +++ b/src/ifcopenshell-python/docs/introduction.rst @@ -3,7 +3,7 @@ Introduction **IfcOpenShell** is an open source software library for software developers and BIM powerusers working with Industry Foundation Classes (`IFC `_). -In addition to a C++ and Python API, **IfcOpenShell** comes with an ecosystem of tools, notably including **IfcConvert** (an application to convert IFC models to other formats), **Bonsai** (an add-on to Blender providing a graphical IFC authoring platform), and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF, bSDD, and IDS. +In addition to a C++ and Python API, **IfcOpenShell** comes with an ecosystem of tools, notably including **IfcConvert** (an application to convert IFC models to other formats), **Bonsai** (an add-on to Blender providing a graphical IFC authoring platform and viewer), and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF, bSDD, and IDS. Things you can do ----------------- @@ -58,6 +58,9 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool "`IfcOpenShell-Python `_", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions." "`IfcConvert `_", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself." "`Bonsai `_", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!" + "`Bonsai Viewer `_", "A high performance viewing and coordination tool for opening and inspecting IFC models." + "`IfcViewer `_", "A WebGPU-based desktop IFC viewer and shared viewer core used by the standalone viewer targets." + "IfcViewerWeb", "A WebAssembly and WebGPU viewer target for viewing IFC models in a web browser." "`BCF `_", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API." "`BIMServer-Plugin `_", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models." "`BIMTester `_", "A utility that allows you to write Gherkin-based tests for models." @@ -81,7 +84,9 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool .. note:: - **IfcOpenShell** and all of its libraries are licensed under LGPL-3.0-or-later. Two exceptions to this are **Bonsai** and **IfcSverchok**, which are both licensed under GPL-3.0-or-later. + **IfcOpenShell** and its libraries and viewer targets are licensed under + LGPL-3.0-or-later. Exceptions include **Bonsai**, **Bonsai Viewer**, and + **IfcSverchok**, which are licensed under GPL-3.0-or-later. .. toctree:: :hidden: From a14cecf68b5d1e62d9eb0b4d320e3368a297f4db Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 09:44:46 +1000 Subject: [PATCH 07/11] Document viewer test commands Add IfcOpenShell-Python and IfcViewer test-running documentation, including desktop CTest targets and web Playwright smoke tests. Move the web test README content into the Sphinx docs.\n\nGenerated with the assistance of an AI coding tool. --- .../docs/ifcopenshell-python.rst | 1 + .../ifcopenshell-python/running_tests.rst | 84 ++++++++++++ src/ifcopenshell-python/docs/ifcviewer.rst | 16 +++ .../docs/ifcviewer/running_tests.rst | 128 ++++++++++++++++++ src/ifcopenshell-python/docs/index.rst | 1 + src/ifcviewer-web/tests/README.md | 45 ------ 6 files changed, 230 insertions(+), 45 deletions(-) create mode 100644 src/ifcopenshell-python/docs/ifcopenshell-python/running_tests.rst create mode 100644 src/ifcopenshell-python/docs/ifcviewer.rst create mode 100644 src/ifcopenshell-python/docs/ifcviewer/running_tests.rst delete mode 100644 src/ifcviewer-web/tests/README.md diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python.rst b/src/ifcopenshell-python/docs/ifcopenshell-python.rst index bbe503c64b..7831bf94eb 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python.rst @@ -19,3 +19,4 @@ capabilities of the C++ core are available in Python. ifcopenshell-python/selector_syntax ifcopenshell-python/schema_querying ifcopenshell-python/validation + ifcopenshell-python/running_tests diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/running_tests.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/running_tests.rst new file mode 100644 index 0000000000..7692b3371f --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/running_tests.rst @@ -0,0 +1,84 @@ +.. This file was generated with the assistance of an AI coding tool. + +Running tests +============= + +IfcOpenShell-Python tests live in ``src/ifcopenshell-python/test`` and use +``pytest``. From the repository root, enter the Python package directory before +running the test suite: + +.. code-block:: bash + + cd src/ifcopenshell-python + +Install the test runner in the Python environment you are using for +development: + +.. code-block:: bash + + pip install pytest + +Full test suite +--------------- + +Use the Makefile target for the default test suite: + +.. code-block:: bash + + make test + +This runs: + +.. code-block:: bash + + pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py + +The ``pytest-blender`` plugin is disabled because these are IfcOpenShell-Python +tests, not Bonsai Blender tests. The shape builder tests are split into a +separate target because they require Blender's ``mathutils`` package. + +Parallel tests +-------------- + +For a faster local run, install ``pytest-xdist`` and use the parallel target: + +.. code-block:: bash + + pip install pytest-xdist + make test-parallel + +This automatically uses the available CPU count and runs the same tests as +``make test``. + +Shape builder tests +------------------- + +The shape builder tests require ``mathutils``. Run them separately: + +.. code-block:: bash + + pip install mathutils + make test-mathutils + +Running individual tests +------------------------ + +You can run an individual file or test directly with ``pytest``: + +.. code-block:: bash + + pytest -p no:pytest-blender test/test_file.py + pytest -p no:pytest-blender test/util/test_unit.py + pytest -p no:pytest-blender test/test_file.py::TestFile::test_creating_a_new_file + +Coverage +-------- + +To generate an HTML coverage report, install ``coverage`` and run: + +.. code-block:: bash + + pip install coverage + make coverage + +The report is written to ``htmlcov``. diff --git a/src/ifcopenshell-python/docs/ifcviewer.rst b/src/ifcopenshell-python/docs/ifcviewer.rst new file mode 100644 index 0000000000..8ec61b270d --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcviewer.rst @@ -0,0 +1,16 @@ +.. This file was generated with the assistance of an AI coding tool. + +IfcViewer +========= + +IfcViewer is the WebGPU-based IFC viewer used by Bonsai Viewer and the +standalone viewer targets. It includes shared loading, sidecar, streaming, +selection, visibility, and viewport state code used by both desktop and web +frontends. + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Contents: + + ifcviewer/running_tests diff --git a/src/ifcopenshell-python/docs/ifcviewer/running_tests.rst b/src/ifcopenshell-python/docs/ifcviewer/running_tests.rst new file mode 100644 index 0000000000..9d231cbb66 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcviewer/running_tests.rst @@ -0,0 +1,128 @@ +.. This file was generated with the assistance of an AI coding tool. + +Running tests +============= + +IfcViewer has two test layers: + +1. **Desktop tests**: C++ unit tests for the shared viewer core and desktop + support code. +2. **Web tests**: headless-browser smoke tests for the Emscripten/WebGPU + frontend. + +The desktop tests are in ``src/ifcviewer/tests``. The web tests are in +``src/ifcviewer-web/tests``. + +Desktop tests +------------- + +The desktop tests are Catch2 executables registered with CTest. They cover +pure viewer logic such as sidecar layout, streaming loaders, selection, +visibility, instance composition, buffer-pool allocation, and viewport camera +state. + +Configure a desktop build with viewer tests enabled: + +.. code-block:: bash + + cmake -S cmake -B build-viewer-wgpu \ + -G Ninja \ + -DBUILD_BONSAIVIEWER=ON \ + -DBUILD_BONSAIVIEWER_TESTS=ON + +Build and run all registered tests: + +.. code-block:: bash + + cmake --build build-viewer-wgpu + ctest --test-dir build-viewer-wgpu --output-on-failure + +To run only the IfcViewer tests, filter by test name: + +.. code-block:: bash + + ctest --test-dir build-viewer-wgpu -R "test_(sidecar|streaming|selection|visibility|buffer|viewport|federation|instance|chunk|lod)" --output-on-failure + +You can also build or run a single test executable directly: + +.. code-block:: bash + + cmake --build build-viewer-wgpu --target test_sidecar_cache + ./build-viewer-wgpu/ifcviewer/tests/test_sidecar_cache + +Common test targets include: + +* ``test_sidecar_compress`` +* ``test_sidecar_cache`` +* ``test_streaming_loader`` +* ``test_instanced_geometry`` +* ``test_chunk_planner`` +* ``test_sidecar_layout`` +* ``test_instance_compose`` +* ``test_selection`` +* ``test_visibility`` +* ``test_buffer_pool`` +* ``test_viewport_camera`` +* ``test_federation`` + +Web tests +--------- + +The web tests are Playwright smoke tests for the WebGPU/Emscripten build. They +load the built page in Chrome, wait for WebGPU initialisation, and assert that +the embedded sample renders non-blank, interactions change the framebuffer, +and no uncaptured WebGPU errors are logged. + +First build the web viewer from the repository root. This requires an +Emscripten environment: + +.. code-block:: bash + + source /path/to/emsdk_env.sh + emcmake cmake -S src/ifcviewer-web -B build-web + ninja -C build-web IfcViewerWeb + +Install the web test dependencies once: + +.. code-block:: bash + + cd src/ifcviewer-web/tests + npm install + +Run the web smoke tests: + +.. code-block:: bash + + npm test + +The Playwright configuration starts ``serve.mjs`` automatically. The server +serves ``build-web`` on ``http://localhost:8124``. Override the build directory +or port with environment variables: + +.. code-block:: bash + + WEB_BUILD_DIR=/path/to/build-web PORT=9000 npm test + +Chrome requirements +~~~~~~~~~~~~~~~~~~~ + +The test configuration uses the system Chrome channel, so a system Chrome such +as ``google-chrome-stable`` must be installed. You do not need to run +``npx playwright install`` unless you change the Playwright browser channel. + +Headless and CI runs +~~~~~~~~~~~~~~~~~~~~ + +WebGPU in headless Linux environments can be sensitive to the GPU and browser +configuration. The default configuration runs headed against the machine's real +GPU. On a headless machine, use Xvfb: + +.. code-block:: bash + + xvfb-run -a npm test + +For a GPU-less runner, change ``headless`` to ``true`` in +``playwright.config.mjs`` and provide a SwiftShader Vulkan ICD, for example via +``VK_ICD_FILENAMES=/path/to/vk_swiftshader_icd.json`` together with a Chrome +``--use-angle=swiftshader`` argument. This is slower than a real GPU but is +enough for render-non-blank smoke checks. diff --git a/src/ifcopenshell-python/docs/index.rst b/src/ifcopenshell-python/docs/index.rst index a460b55f09..3f62c1af66 100644 --- a/src/ifcopenshell-python/docs/index.rst +++ b/src/ifcopenshell-python/docs/index.rst @@ -12,6 +12,7 @@ Let's learn IfcOpenShell! ifcopenshell ifcopenshell-python ifcconvert + ifcviewer bonsai bonsai-viewer diff --git a/src/ifcviewer-web/tests/README.md b/src/ifcviewer-web/tests/README.md deleted file mode 100644 index 8def5a3d8f..0000000000 --- a/src/ifcviewer-web/tests/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Web smoke tests - -Headless-browser smoke tests for the WebGPU/Emscripten ifcviewer build. -They load the built page in a real Chrome, wait for wgpu init, and assert -the embedded sample **renders non-blank**, an **orbit drag changes the -framebuffer**, and **no uncaptured WebGPU errors** are logged. Every web -bring-up bug so far (blank render, error-buffer cascade, an overlay -swallowing mouse input) is this shape. - -## Prerequisites - -- The web build must exist at `build-web/` (repo root): - ```sh - source /path/to/emsdk_env.sh - emcmake cmake -S src/ifcviewer-web -B build-web - ninja -C build-web IfcViewerWeb - ``` -- Node + a system Chrome (`google-chrome-stable`). The config uses - `channel: 'chrome'`, so you do **not** need `npx playwright install`. - -## Run - -```sh -cd src/ifcviewer-web/tests -npm install # one-time: pulls @playwright/test -npm test -``` - -`serve.mjs` statically serves `build-web` on :8124 (override with -`WEB_BUILD_DIR=/path PORT=...`). Playwright starts it automatically. - -## Headless / CI - -WebGPU + headless on Linux is finicky, so the default config runs -**headed** against the machine's real GPU. On a headless box: - -```sh -xvfb-run -a npm test -``` - -For a GPU-less runner, flip `headless: true` in -`playwright.config.mjs` and provide a SwiftShader Vulkan ICD -(`VK_ICD_FILENAMES=.../vk_swiftshader_icd.json`) plus -`--use-angle=swiftshader`. Browser WebGPU over SwiftShader is slow but -adequate for a render-non-blank assertion. From 9ad10c009b4279781d9173dea92fb173fb8ea9a4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 10:00:25 +1000 Subject: [PATCH 08/11] Add Bonsai Viewer about license Replace the settings About placeholder with product, GPL, and third-party license information. Generated with the assistance of an AI coding tool. --- src/bonsaiviewer/modules/settings/Dialog.cpp | 75 +++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/bonsaiviewer/modules/settings/Dialog.cpp b/src/bonsaiviewer/modules/settings/Dialog.cpp index 699b954906..b10455edad 100644 --- a/src/bonsaiviewer/modules/settings/Dialog.cpp +++ b/src/bonsaiviewer/modules/settings/Dialog.cpp @@ -274,11 +274,84 @@ void SettingsDialog::setupUi() { return tab; }; + auto make_about_tab = [this]() { + auto* tab = new QWidget(this); + auto* layout = new QVBoxLayout(tab); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(components::style::metrics::padding); + layout->setAlignment(Qt::AlignTop); + + auto make_rich_label = [](const QString& text, QWidget* parent) { + const QString link_color = bonsaiviewer::ViewerSettings::instance().color("icon_accent_color"); + auto* label = new QLabel(QString("%2").arg(link_color, text), parent); + label->setTextFormat(Qt::RichText); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + label->setOpenExternalLinks(true); + label->setWordWrap(true); + return label; + }; + + auto* about_section = new components::Section( + "About Bonsai Viewer", components::SectionHeaderMode::Visible, tab); + auto* about_body = new QWidget(about_section); + auto* about_layout = new QVBoxLayout(about_body); + about_layout->setContentsMargins(0, 0, 0, 0); + about_layout->setSpacing(8); + about_layout->addWidget(make_rich_label( + "Bonsai Viewer
" + "A high performance IFC viewing and coordination tool for large OpenBIM projects.

" + "bonsaibim.org | " + "Source code", + about_body)); + about_section->addBodyWidget(about_body); + layout->addWidget(about_section); + + auto* license_section = new components::Section("License", components::SectionHeaderMode::Visible, tab); + auto* license_body = new QWidget(license_section); + auto* license_layout = new QVBoxLayout(license_body); + license_layout->setContentsMargins(0, 0, 0, 0); + license_layout->setSpacing(8); + license_layout->addWidget(make_rich_label( + "Bonsai Viewer is free software: you can redistribute it and/or modify it under the terms of " + "the GNU General Public License as published by the Free Software Foundation, either " + "version 3.0 of the License, or (at your option) any later version.

" + "Bonsai Viewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; " + "without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. " + "See the GNU General Public License " + "for more details.", + license_body)); + license_section->addBodyWidget(license_body); + layout->addWidget(license_section); + + auto* dependencies_section = new components::Section( + "Third-party licenses", components::SectionHeaderMode::Visible, tab); + auto* dependencies_body = new QWidget(dependencies_section); + auto* dependencies_layout = new QVBoxLayout(dependencies_body); + dependencies_layout->setContentsMargins(0, 0, 0, 0); + dependencies_layout->setSpacing(8); + dependencies_layout->addWidget(make_rich_label( + "
    " + "
  • IfcOpenShell and IfcViewer - LGPL-3.0-or-later
  • " + "
  • Qt - LGPL-3.0-only, GPL-3.0-only, or commercial license depending on distribution
  • " + "
  • Open CASCADE Technology - LGPL-2.1-only with OCCT exception
  • " + "
  • Eigen - MPL-2.0
  • " + "
  • wgpu-native - MPL-2.0
  • " + "
  • zstd - BSD-3-Clause
  • " + "
  • meshoptimizer - MIT, when enabled
  • " + "
" + "See the bundled dependency notices and source distributions for the complete license texts.", + dependencies_body)); + dependencies_section->addBodyWidget(dependencies_body); + layout->addWidget(dependencies_section); + layout->addStretch(1); + return tab; + }; + addTab("Interface", interface_tab); addTab("Keybindings", make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here.")); addTab("Graphics", graphics_tab); addTab("Connectors", buildConnectorsTab()); - addTab("About", make_placeholder_tab("About", "Version, credits, and environment information will live here.")); + addTab("About", make_about_tab()); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); if (auto* ok = buttons->button(QDialogButtonBox::Ok)) { From da5c0b7991e8fa36f739912f3d4e8f526ace6cd8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Jul 2026 17:31:02 +1000 Subject: [PATCH 09/11] =?UTF-8?q?ifcviewer:=20section-plane=20cut=20tool?= =?UTF-8?q?=20on=20web=20=E2=80=94=20shared=20gizmo,=20true-face=20pick,?= =?UTF-8?q?=20drag/Del?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/ifcviewer-web/CMakeLists.txt | 2 +- src/ifcviewer-web/main_web.cpp | 93 +++- src/ifcviewer-web/shell.html | 9 + src/ifcviewer-web/tests/smoke.spec.mjs | 22 + src/ifcviewer/CMakeLists.txt | 2 + src/ifcviewer/OverlayRenderer.cpp | 271 +--------- src/ifcviewer/OverlayRenderer.h | 19 +- src/ifcviewer/SectionGizmoRenderer.cpp | 369 +++++++++++++ src/ifcviewer/SectionGizmoRenderer.h | 79 +++ src/ifcviewer/ViewportCore.cpp | 512 +++++++++++++++---- src/ifcviewer/ViewportCore.h | 90 +++- src/ifcviewer/ViewportWindow.cpp | 123 +---- src/ifcviewer/ViewportWindow.h | 19 +- src/ifcviewer/tests/test_viewport_camera.cpp | 19 + 14 files changed, 1093 insertions(+), 536 deletions(-) create mode 100644 src/ifcviewer/SectionGizmoRenderer.cpp create mode 100644 src/ifcviewer/SectionGizmoRenderer.h diff --git a/src/ifcviewer-web/CMakeLists.txt b/src/ifcviewer-web/CMakeLists.txt index e7dbb004f5..bbd7d70624 100644 --- a/src/ifcviewer-web/CMakeLists.txt +++ b/src/ifcviewer-web/CMakeLists.txt @@ -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','_hide_selected_c','_isolate_selected_c','_show_all_c','_toggle_xray_c','_xray_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','_toggle_section_c','_clear_section_c','_section_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. diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp index 95dfac91e9..afab844e8b 100644 --- a/src/ifcviewer-web/main_web.cpp +++ b/src/ifcviewer-web/main_web.cpp @@ -67,6 +67,11 @@ struct AppState { // bindings (ViewportCore::navBindings), so any preset works on web too. bool nav_active = false; NavKind nav_kind = NavKind::None; + // Section-cut tool: while active, a select-button click drops a clip plane + // at the picked surface (K toggles, Shift+K clears — matches desktop). + bool section_tool_active = false; + // True while dragging a section-plane gizmo (LMB down on the arrow → slide). + bool section_dragging = false; // Accumulated |movement| since mousedown, in CSS px. A select-button release // under the click threshold (no real drag) is treated as a pick; a drag will // become a marquee. Captures the down position (canvas-relative CSS px). @@ -146,6 +151,14 @@ EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void* user) { auto* app = static_cast(user); // In fly mode a click exits (matches the desktop app). if (app->fly_mode) { setFlyMode(app, false); return EM_TRUE; } + // Section tool: LMB on a plane's gizmo arrow grabs it to slide (logical px). + if (app->section_tool_active && e->button == 0) { + const int hit = app->core.hitTestSectionGizmo(int(e->targetX), int(e->targetY)); + if (hit >= 0 && app->core.beginSectionDrag(hit, int(e->targetX), int(e->targetY))) { + app->section_dragging = true; + return EM_TRUE; // claim the press — don't orbit + } + } const NavKind kind = classifyPress(app->core.navBindings(), e->button, e->shiftKey, e->ctrlKey, e->altKey); if (kind != NavKind::None) { @@ -166,6 +179,11 @@ EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void* user) { app->core.flyLook(float(e->movementX), float(e->movementY)); return EM_TRUE; } + // Section gizmo drag: slide the grabbed plane along its normal (logical px). + if (app->section_dragging) { + app->core.updateSectionDrag(int(e->targetX), int(e->targetY)); + return EM_TRUE; + } if (!app->nav_active) return EM_FALSE; const float dx = float(e->movementX); @@ -191,11 +209,33 @@ EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) { app->nav_active = false; app->nav_kind = NavKind::None; - if (was_active && kind == NavKind::Select && app->ready) { - const double dpr = emscripten_get_device_pixel_ratio(); - const bool add = e->shiftKey; - const bool remove = e->ctrlKey; - if (app->nav_drag_px > kClickDragThresholdPx) { + // End a section-gizmo drag (took over the press; no pick/orbit on release). + if (app->section_dragging) { + app->core.endSectionDrag(); + app->section_dragging = false; + return EM_TRUE; + } + + if (!was_active || !app->ready) return EM_TRUE; + const double dpr = emscripten_get_device_pixel_ratio(); + const bool no_drag = app->nav_drag_px <= kClickDragThresholdPx; + + // Section tool claims a LEFT-button click ("click a surface to cut"); LMB + // drag still orbits. Takes priority over nav while the tool is active. + if (app->section_tool_active && e->button == 0 && no_drag) { + const int px = int(app->down_x * dpr), py = int(app->down_y * dpr); + app->core.pickSurfaceAtAsync(px, py, [app](ViewportCore::SurfaceHit hit) { + if (hit.found) // pad past the AABB so the cut reads as a cap + app->core.addSectionPlaneAtSurface(hit.world_pos, hit.world_normal, + hit.aabb_radius * 1.5f); + app->host.requestFrame(); + }); + return EM_TRUE; + } + + if (kind == NavKind::Select) { + const bool add = e->shiftKey, remove = e->ctrlKey; + if (!no_drag) { // Marquee drag → box-pick the rect (device px) and apply to selection. hideMarquee(); const long x0 = std::min(app->down_x, e->targetX); @@ -209,14 +249,11 @@ EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) { app->host.requestFrame(); }); } else { - // No real drag → single pick under the cursor (Shift add, Ctrl remove, - // plain replace). Async readback: highlight lands a frame later. - const int px = int(app->down_x * dpr); - const int py = int(app->down_y * dpr); + // Single pick under the cursor (Shift add, Ctrl remove, plain replace). + const int px = int(app->down_x * dpr), py = int(app->down_y * dpr); app->core.pickObjectAtAsync(px, py, [app, add, remove](std::uint32_t id) { app->core.applyPickToSelection(id, add, remove); - // v15 on-demand deferred fetch: log the picked object's IFC GUID - // (first pick fetches the property block off the network). + // v15 on-demand element metadata fetch: log the picked object's IFC GUID. if (id != 0) app->core.logSelectedObjectGuidWeb(id); app->host.requestFrame(); }); @@ -300,6 +337,25 @@ EM_BOOL onKeyDown(int, const EmscriptenKeyboardEvent* e, void* user) { return EM_TRUE; } if (!std::strcmp(code, "KeyX") && alt) { app->core.toggleXray(); return EM_TRUE; } + // Section tool: K toggles drop-a-plane mode, Shift+K clears all cuts. + if (!std::strcmp(code, "KeyK")) { + if (shift) app->core.clearSectionPlanes(); + else { + app->section_tool_active = !app->section_tool_active; + Log::info() << "[section] tool " + << (app->section_tool_active ? "active — click a surface" : "off"); + } + app->host.requestFrame(); + return EM_TRUE; + } + // Del/Backspace removes the most recent cut while the tool is active. + if (app->section_tool_active && + (!std::strcmp(code, "Delete") || !std::strcmp(code, "Backspace"))) { + const int n = app->core.sectionPlaneCount(); + if (n > 0) app->core.removeSectionPlane(n - 1); + app->host.requestFrame(); + 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(); @@ -438,6 +494,21 @@ extern "C" EMSCRIPTEN_KEEPALIVE void show_all_c() { if (g_app && g_app-> 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; } +// Section-cut tool: toggle the drop-a-plane mode, clear all planes, query state. +extern "C" EMSCRIPTEN_KEEPALIVE void toggle_section_c() { + if (!g_app || !g_app->ready) return; + g_app->section_tool_active = !g_app->section_tool_active; + Log::info() << "[section] tool " + << (g_app->section_tool_active ? "active — click a surface to cut" : "off"); + g_app->host.requestFrame(); +} +extern "C" EMSCRIPTEN_KEEPALIVE void clear_section_c() { + if (g_app && g_app->ready) { g_app->core.clearSectionPlanes(); g_app->host.requestFrame(); } +} +extern "C" EMSCRIPTEN_KEEPALIVE int section_is_active_c() { + return (g_app && g_app->ready && g_app->section_tool_active) ? 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; diff --git a/src/ifcviewer-web/shell.html b/src/ifcviewer-web/shell.html index f3339d9c8d..47b42ccafe 100644 --- a/src/ifcviewer-web/shell.html +++ b/src/ifcviewer-web/shell.html @@ -96,6 +96,9 @@ + + +
Starting…