mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 03:14:23 +00:00
c2b168b45b42ccd8ee400aab90134186bd644765
19911 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1a17ba9e6d |
ifcviewer: de-Qt QColor/QPoint/QSet/QElapsedTimer in ViewportWindow
Last round of straight-swap Qt value types in ViewportWindow + its
overlay co-pilot.
setBackgroundColor(const QColor&) → (float r, float g, float b, float a)
QColor background_color_ → Eigen::Vector4f (linear, 0..1)
QPoint {nav_,box_select_,fps_, } → Eigen::Vector2i
{section_drag_start_mouse_}
QSet<int> fps_keys_held_ → std::unordered_set<int>
QElapsedTimer fps_last_tick_, → Stopwatch (new header in
fly_render_clock_, IfcViewerCore — std::chrono-
render_thread_local_ backed, exposes the existing
timers in render() QElapsedTimer .start/.restart/
.elapsed/.nsecsElapsed surface)
Also propagates the QPoint → Eigen::Vector2i change through
OverlayRenderer::encodeMarquee since the marquee corner coords flow
through that interface.
API-level helpers:
toV2i(QPoint) — small inline in ViewportWindow.cpp, isolates
the QMouseEvent→Vector2i conversion at the
five mouse-event handlers
Stopwatch.h — new file, IfcViewerCore. Same call shape as
QElapsedTimer; backed by std::chrono::steady_clock.
QSet method swaps:
.isEmpty() → .empty()
.contains(k) → .count(k) (C++17, no std contains() until C++20)
.remove(k) → .erase(k)
Eigen::Vector2i doesn't have .manhattanLength(); the box-select drag
threshold uses std::abs(diff.x()) + std::abs(diff.y()) inline.
Bonsai side: View.cpp's setBackgroundColor wrapper now decomposes the
QColor into floats at the call site (kept locally so the bonsai UI
keeps its QColor-driven theming).
Closes #81 + the QElapsedTimer half of #83. QTimer
(pivot_indicator_hide_timer_) still uses Qt — it needs the host's
scheduleOnce mechanism that lands with #85.
Builds: desktop / bonsai / web all green. Tests 100/100.
|
||
|
|
b62e14a06a |
ifcviewer: de-Qt ViewportWindow public API (QString → std::string)
Take QString out of ViewportWindow's outward-facing surface so it can
eventually move into a Qt-free ViewportCore:
void queueLoadSidecar(const QString&) → (const std::string&)
uint32_t loadSidecar(const QString&) → (const std::string&)
QString cameraString() const → std::string …
void captureNextFrameToPng(const QString&, bool)
→ (const std::string&, bool)
void setHudText(const QString&) → (const std::string&)
Internal members also moved off QString:
std::deque<QString> pending_sidecars_ → std::deque<std::string>
QString pending_screenshot_path_ → std::string
Implementation strategy: convert at the boundary where ViewportWindow
still leans on Qt internals — `loadSidecar` bridges to QString once
for QFile/QDir/QFileInfo path handling; the screenshot save path
constructs a QString locally for QImage::save; the OverlayRenderer's
HUD setter still takes QString so setHudText converts before calling
through. Each of those bridges goes away when ViewportCore lands and
OverlayRenderer / SceneLoader / SidecarBuilder get their own de-Qt
sweeps. cameraString now produces its CSV via snprintf — no QString
ever instantiated.
Bonsai-side updates (compile-only):
ifcviewer-minimal/main.cpp — queueLoadSidecar / captureNextFrameToPng
callers add .toStdString() on the QString
parser result
modules/viewport/View.cpp — setHudText callers add .toStdString() to
their `QString::arg(...)` formatter chains;
two `QString()` empty sentinels become
`std::string()`
Measurement.cpp — same pattern, two setHudText sites
ifcviewer/LengthMeasurement.cpp — same, three sites
The cameraString string-streaming fix-up in ViewportWindow.cpp drops
the temporary .toUtf8().constData() bridge from #82 — Log::Stream's
std::string overload now handles it directly.
Builds: desktop / bonsai / web all green. Tests 100/100. Closes #80.
|
||
|
|
6dd3558db9 |
ifcviewer: replace qInfo/qWarning with a Qt-free logger seam
Add Log.h (in IfcViewerCore) — a tiny stream-style logger that backs fprintf(stderr,...), with overloads for the common primitives + char strings. Mimics qInfo()/qWarning()'s syntax surface enough that mass-replacing qInfo()→Log::info() and qWarning()→Log::warn() keeps existing call sites parsing unchanged; .noquote() / .nospace() exist as compat no-ops so chained qInfo().noquote()<<x<<y patterns survive. QString streaming is a transitional concern — the QString → std::string sweep (#80) hasn't landed yet, so ViewportWindow and friends still construct QStrings for log payloads. LogQt.h (in IfcViewer, not Core) adds the QString / QStringView operator<< overloads so those streaming sites work without source changes during the in-flight Qt removal. When #80 retires QString, LogQt.h drops out. ViewportWindow.cpp: 132 qInfo/qWarning callsites converted. The two printf-style qInfo("fmt %s", ...) callsites get fprintf with explicit [info]/[warn] prefixes to keep the output discoverable. Also de-Qt'd: AreaMeasurement.cpp — 1 qInfo("fmt", …) → fprintf SceneLoader.cpp — 4 qDebug + 1 qWarning printf-style → fprintf GeometryStreamer.cpp — 2 qDebug printf-style → fprintf ifcviewer-minimal/main.cpp — 2 qWarning << → Log::warn Drops <QDebug> from each. Closes #82. Builds: desktop / bonsai / web all green. Tests 100/100 pass. |
||
|
|
77cf535b45 |
ifcviewer: de-Qt math types (Eigen everywhere)
Replace Qt math wrappers with Eigen across ViewportWindow, OverlayRenderer, Federation, and the bonsai-side viewport modules. Eigen was already the canonical type for the actually-important matrix work (InstanceCompose, ModelGpuData, federation matrices); QVector3D/QVector4D/QMatrix4x4 were leftover from when Qt was the path of least resistance. They offered nothing over Eigen for our use case beyond a few graphics helpers (lookAt / perspective / ortho) which were 30 lines to write. Substitutions: QMatrix4x4 → Eigen::Matrix4f QVector2D → Eigen::Vector2f QVector3D → Eigen::Vector3f QVector4D → Eigen::Vector4f API rewrites: .lengthSquared() → .squaredNorm() .length() → .norm() .isNull() → .isZero() .setToIdentity() → .setIdentity() .constData() → .data() .toVector3D() → .head<3>() .inverted(&ok) → tryInvert4f(M, out) Q::dotProduct(a,b) → a.dot(b) Q::crossProduct(a,b) → a.cross(b) QMat4x4(... row-major) → Eigen::Map<const Matrix4f>(col-major buf) QMat4x4().lookAt(...) → lookAtRH(eye, target, up) QMat4x4().perspective(.) → perspectiveYFovGL(fovy, aspect, n, f) QMat4x4().ortho(...) → orthoGL(l, r, b, t, n, f) Default-init divergence handled explicitly (QMatrix4x4() = identity, QVector3D() = zero; Eigen leaves both uninitialized). Public API (CameraState, HomeView, ViewportWindow::computeObjectAabb, the addSectionPlaneAtSurface / pickSurfaceAt / raycast signatures) follows through to Eigen too; bonsai-side View.cpp and Commands.cpp updated to match. Camera helpers (lookAtRH, perspectiveYFovGL, orthoGL, tryInvert4f) extracted to a new CameraMath.h so OverlayRenderer's gizmo MVP and ViewportWindow's buildViewProj share the same definitions. Federation drops its <QVector3D> include in favour of <Eigen/Dense> (already had the latter for the georef matrices). Builds: desktop IfcViewerMinimal ✓, BonsaiViewer ✓, web IfcViewerWeb ✓. Tests: 100/100 pass. Closes #78 + #79; opens the door for #80-#83. |
||
|
|
1f2b20fd86 | Fix --convert-back-units on transformation object #8137 | ||
|
|
94fab271cd | Check for empty result after BOPAlgo_MakerVolume and reset manifoldness state #8140 | ||
|
|
8583d0963f | Make faceset duplicate loop detection respect inner/outer #8140 | ||
|
|
77a2284f8a | Re-sew non-manifold operands; interior loop re-orientations affect edge identity #8140 | ||
|
|
4520a72152 | Sane error messages for unsupported items in geometry libs #8106 | ||
|
|
c314dd3ca8 |
ifcviewer: scaffold ViewportHost + ViewportCore (Path A step 1)
Define the boundary the Path-A web-bring-up refactor will move things
across:
- ViewportHost.h is the embedder interface — surface creation,
framebuffer geometry, frame scheduling, quit, and notification
callbacks (onObjectPicked, onToolModeChanged, …). Desktop hosts
forward notifications to Q_SIGNALS; the future web host pushes
them to JS callbacks.
- ViewportCore.{h,cpp} is the platform-agnostic render-core target.
Empty today — the body fills in across the #78-#86 sequence as
each Qt subsystem (matrices, vectors, strings, timers, render
path, input) gets de-Qt'd and moved over.
- ViewportWindow now multiply-inherits ViewportHost alongside QWindow
and implements the host overrides as thin forwarders: createSurface
returns the cached surface_, requestFrame -> requestUpdate, quit ->
QCoreApplication::quit, onObjectPicked -> emit objectPicked.
Renamed the DPR accessor `dpr()` (vs `devicePixelRatio`) to avoid
the inherited-virtual clash with QWindow's qreal-returning version.
No method movement yet — this is purely the architectural scaffold so
subsequent commits have a destination.
|
||
|
|
e55a360aa2 |
web: scaffold IfcViewerWeb (Emscripten clear-color renderer)
First Emscripten target. main_web.cpp brings up a wgpu instance against
a <canvas id="viewer-canvas">, requests adapter+device asynchronously
via the standard webgpu.h callback chain, configures the surface, and
clears to the BonsaiViewer slate background on each RAF tick. No
sidecar load, no pipelines, no scene state yet — the goal is to end-
to-end verify the build + canvas + wgpu plumbing.
src/ifcviewer-web/ is a separate CMake root (not a subdir under the
desktop cmake/CMakeLists.txt) so the web build doesn't have to opt out
of Qt / OpenCASCADE / IfcGeom find_packages it can't satisfy. It adds
src/ifcviewer EXCLUDE_FROM_ALL and consumes only IfcViewerCore.
src/ifcviewer/CMakeLists.txt now gates the wgpu-native fetch + the
Qt-using IfcViewer target + install commands behind NOT EMSCRIPTEN.
The wgpu_native link target still resolves under Emscripten as an
INTERFACE library that activates --use-port=emdawnwebgpu (Dawn's
webgpu.h, replaces the legacy -sUSE_WEBGPU=1).
Build:
source path/to/emsdk_env.sh
emcmake cmake -S src/ifcviewer-web -B build-web -G Ninja
ninja -C build-web
python3 -m http.server --directory build-web 8080
# open http://localhost:8080/IfcViewerWeb.html in a WebGPU-capable
# browser (Chrome 113+, Edge 113+).
Phase B step 3 of #45.
|
||
|
|
25651a1507 |
Fix demo preset crash + scope header refresh
bpy.ops.bim.new_project(preset='demo') crashed in refresh_bim_tool_headers: the post-commit hook fired for every nested bpy.ops.bim.append_library_element during template loading, and the operator context Blender hands to programmatically-invoked nested operators is stripped of the view-layer attributes the refresh reads. Two changes resolve it. Gate the header refresh in tool.Parametric.refresh_post_commit on operator.bl_idname being one of the EDIT_TYPES finish_op idnames. Only validate-gizmo commits (bim.finish_editing_<name>) now trigger the refresh; demo-loader and other non-edit operators skip it. Querying the registry directly is the canonical signal — string-prefix matching would silently drift if ParametricObject.finish_op changes derivation. Harden tool.Blender.get_active_object so its view_layer fallback also uses getattr; the 150+ callers routed through it now tolerate stripped contexts. _resolve_bim_tool_context applies the same defensive pattern to mode / workspace. Tests: - test_handler_restricted_context covers get_active_object's defensive path and the BimTool-family whitelist (excludes annotation, spatial, structural). - test_handler_forward_compat AST-pins that the gate consults EDIT_TYPES (not a string prefix). - test_wall_header_refresh rewritten — three tests cover the gated-by-registry contract: counter bumps for every commit, finish_op operators refresh headers, others don't. Hotkey-driven in-place edits (S_E / C_E) no longer trigger the refresh — they were caught by the pre-refactor "every commit" design. Left out of scope; the new skip-non-finish test pins this as intentional. Generated with the assistance of an AI coding tool. |
||
|
|
c098146c35 |
Remove Autodesk viewer examples
Remove the bonsaiviewer-autodesk Cargo examples that were used for local UI and dialog experiments. Generated with the assistance of an AI coding tool. |
||
|
|
fe6a0452bf |
ifcviewer: split out IfcViewerCore static library
Pull the Qt-free / OpenCASCADE-free files out of the IfcViewer target into a new IfcViewerCore static lib: BufferPool, ChunkPlanner, InstanceCompose, SidecarCache, StreamingLoader, StreamingThread, LodBuilder, plus the header-only InstancedGeometry / ModelGpuData / VertexQuantization / Selection / Visibility headers. IfcViewer PUBLIC- links IfcViewerCore so existing consumers see no change. This is the boundary the Emscripten web target will link against — keeps Qt, IfcGeom, OpenCASCADE, CGAL, and Boost out of the wasm build. Explicit file list, not glob, because the boundary is the whole point. |
||
|
|
cb19f22ee4 |
BufferPool: drop Qt log dependency
Replace qInfo() growth-event logging with fprintf(stderr,...) so BufferPool.cpp has no Qt touchpoints. Lets the test target drop its Qt6::Core link too. Prerequisite for the IfcViewerCore library boundary the web target will link against. |
||
|
|
1fe4570860 |
ifcviewer: extract ChunkPlanner + InstanceCompose; add Tier-1 test trio
The chunk planner (Morton sort + greedy pack) and instance composition (federation × placement matrix chain + world-AABB derive) were inline helpers in ViewportWindow.cpp. Pulled both out as free-function modules so the math + lookup logic can be exercised without a Qt window or a wgpu device. ViewportWindow now delegates; InstanceLookup is a using- alias to InstanceCompose::InstanceLookup. Also added an addSubBufferForTesting / clearSubPoolsForTesting seam to BufferPool so the sub-allocator invariants can be pinned with fake WGPUBuffer handles. The fakes are never dereferenced; the guard drops the sub-pools before destructor would call wgpuBufferRelease. Three new test binaries under src/ifcviewer/tests/, 33 cases / 173 assertions: BufferPool first-fit + alignment + coalescing + multi- sub-pool isolation; ChunkPlanner Morton split / interleave / stable sort / greedy-pack monotonicity and single-mesh-oversize; InstanceCompose identity / translation / order-of-multiplication / large-placement cancellation against federation false origin / column-major writeback / findInstance lookup paths. |
||
|
|
94faaa3160 |
Drop dead Geometry.has_material_styles + sanitation sweep
Two related cleanups bundled because each was too small on its own. == Drop dead Geometry.has_material_styles duplicate == Two parallel has_material_styles implementations existed on HEAD: * Geometry.has_material_styles (tool/geometry.py:853, added by |
||
|
|
749476d1a7 |
docs: rewrite stale GL-era docs (env-vars + viewport_architecture)
Two long-stale docs that described the deleted OpenGL backend are replaced with current-state rewrites under `src/bonsaiviewer/docs/` and wired into the toctree. The originals are removed. ## env-vars.rst (replaces src/ifcviewer/settings.rst) The orphan `src/ifcviewer/settings.rst` was written for the OpenGL backend (`IFC_*` prefix, MDI-specific knobs) and was never wired into any Sphinx toctree — it sat as a one-off file in the C++ source tree, undiscoverable from a normal docs build. * **Dead — dropped entirely.** `IFC_SKIP_MDI`, `IFC_MAX_SUBDRAWS`, `IFC_SUBDRAW_DIAG` were GL-only `glMultiDrawElementsIndirect` instrumentation. wgpu has no MDI. `IFC_FPS_HITCH_MS` no longer exists in source. * **Renamed.** `IFC_HIZ_MOTION` → `WGPU_HIZ_MOTION`, `IFC_CULL_THREADS` → `WGPU_CULL_THREADS`. * **New, previously undocumented.** Ten `WGPU_*` vars added during the port + bring-up (WGPU_HIZ, WGPU_HIZ_TRACE, WGPU_MIN_PX, WGPU_MIN_PX_MOTION, WGPU_FLY_DEBUG, WGPU_NAV_PRESET, WGPU_PRESENT_MODE, WGPU_STREAM_DEBUG, WGPU_STREAM_DEEP_DEBUG, WGPU_STREAM_EVICT_LOG). Descriptions written from each variable's use-site so wording matches actual behaviour. * **LOD-build section kept verbatim.** IFC_LOD_ERROR, IFC_LOD_RATIO, IFC_LOD_MIN_SAVINGS, IFC_LOD_DEBUG — sidecar-bake knobs, backend-agnostic. * **GUI-promoted "old IFC_* graveyard" section dropped.** The file is an env-var reference, not a record of historical spellings. ## viewport_architecture.rst (replaces src/ifcviewer/README.md) The 994-line `src/ifcviewer/README.md` was an archive of the GL-era phase-by-phase perf narrative. ~95% of it described deleted code: OpenGL 4.5 Core, `glMultiDrawElementsIndirect`, VAO/VBO/EBO, `GL_ARB_shader_draw_parameters`, BVH-per-model, sidecar v5/v7/v9 (current is v13), the now-non-existent `./IfcViewer` binary, Phase 3F "static batching next" plans superseded by the chunk-pool architecture, Phase 3E "GPU compute culling removed" since re-added as task #17 pending. Salvaging the ~50 lines of still-correct content would have left a Frankenstein doc internally contradicting itself. Replaced with a focused architecture page covering current reality: consumer split (BonsaiViewer shell vs IfcViewerMinimal standalone), stack (wgpu-native v29, Qt6, IfcOpenShell, IfcUtil, Eigen3, meshoptimizer), five core ideas (unique-mesh instancing, quantized 12 B vertex, chunked streaming on a probed VRAM pool, sidecar v13 fast path, event-driven rendering), per-frame pipeline (cull → upload → streaming → opaque pass → transparent pass → edge → overlay → present), federation + false-origin compose, file map limited to files that actually exist in `src/ifcviewer/` today, build/run via `build_viewer.sh`, cross-refs to env-vars.rst, debug-output.rst, and connectors/. ## Toctree `src/bonsaiviewer/docs/index.rst` gains `env-vars` and `viewport_architecture` entries alongside the existing `connectors/index` and `debug-output`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ce7d2fa329 |
docs: split Autodesk connector docs into user + developer pages
`autodesk.rst` had grown to mix end-user concerns (where do my tokens live, how do I install the bundle, why isn't sign-in working) with developer concerns (cargo build, fmt/clippy/test, packaging script flow, per-OS toolchain notes, CI). Reorganise into: * **`autodesk.rst`** — Autodesk Connector. User-facing. Bonsai-Viewer- level intro (Forma/APS/Docs, "Add from cloud"); install-from-zip per OS; first-run setup (client ID, OAuth port, browser redirect); where settings / cache / OAuth tokens live; proxy / TLS guidance for corporate installs. * **`autodesk_development.rst`** — Autodesk Connector Development. Developer-facing. Tech stack (FLTK, ureq, keyring, dirs, serde, chrono, webbrowser); `cargo build --release`; `cargo test --all-features` / clippy / fmt-check; protocol probing via stdio pipe; packaging via `packaging/build.py`; per-OS build / keychain / codesign notes; CI workflow overview. Absorbs the entirety of the old `autodesk_packaging.rst`, which is removed. `connectors/index.rst` toctree updated: `autodesk_packaging` → `autodesk_development`. `cloud_sync_protocol.rst` untouched — language-agnostic protocol spec. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9d9f4054f6 |
bonsaiviewer-autodesk: replace Python connector with the Rust impl
The Python implementation of the Autodesk Forma connector
(bonsaiviewer_autodesk/) is deprecated. The Rust port that's been
maturing under src/bonsaiviewer-autodesk-rs/ is now the connector
and takes over the original folder name.
## File operations
* `git rm -r src/bonsaiviewer-autodesk` — drop the 18 tracked Python
source/test/packaging files. (~6.5k untracked build artefacts in
venv/build/dist/egg-info are removed too, but those were never in
the index.)
* `mv src/bonsaiviewer-autodesk-rs src/bonsaiviewer-autodesk` —
the Rust impl takes over the canonical folder name.
* `rm -rf src/bonsaiviewer-autodesk-rs-egui` — abandoned egui-based
experiment, never committed.
* `src/bonsaiviewer-autodesk/.gitignore` extended with `/dist` to
keep packaging output out of the index alongside the existing
`/target` rule.
The Rust binary in Cargo.toml already has `name = "bonsaiviewer-
autodesk"` and `connector.json`'s `exec` field already points at that
name — so the connector loader, build_viewer.sh symlink, and
win/build-all-win.py CONNECTOR_DIR all keep working without edits.
## Packaging shape preserved
`packaging/build.py` is rewritten to:
* shell out to `cargo build --release` instead of pyinstaller,
* copy the produced binary + connector.json into the same
`dist/autodesk/` layout the PyInstaller flow produced,
* zip into `dist/autodesk-<os>-<arch>.zip` with the same
naming pattern (CI artifact uploads keep working).
The Rust binary statically links its deps, so unlike PyInstaller
there's no `_internal/` directory — single executable inside
`dist/autodesk/`. Everything downstream (`build_viewer.sh` symlink,
`win/build-all-win.py collect_connector_files`, the zip step in
`build_rocky.yml`) only cares that `dist/autodesk/` exists, so the
on-disk contract is preserved.
Verified locally: `python3 src/bonsaiviewer-autodesk/packaging/build.py`
produces `dist/autodesk/{bonsaiviewer-autodesk, connector.json}`
(3.9 MB stripped ELF) and `dist/autodesk-linux-x86_64.zip` (~1.5 MB
compressed).
## CI updates
* `.github/workflows/build_rocky.yml` and `build_rocky_arm.yml`:
drop the `pip install ".[build]"` step — `packaging/build.py` is
stdlib-only now, the cargo build wrapped inside it does the work.
* `.github/workflows/build_win.yml`: same — drop pip install,
packaging script handles cargo internally.
* `.github/workflows/build-bonsaiviewer-autodesk.yml`: full rewrite
of the dedicated connector test/build workflow. Replaces the
Python {3.11, 3.13} test matrix with `cargo fmt --check`,
`cargo clippy --all-targets -- -D warnings`, and `cargo test
--all-features`. The OS/arch build matrix is unchanged
(linux-x86_64, macos-arm64, macos-x86_64, windows-x86_64) but
installs a Rust toolchain via dtolnay/rust-toolchain@stable and
caches target/ via Swatinem/rust-cache.
`win/build-all-win.py` and `build_viewer.sh` are unchanged — they
only reference the `dist/autodesk/` path, which the new
`packaging/build.py` populates identically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b3fbcd6a66 |
refactor: extract src/ifcutil/ from src/ifcviewer/ (Unit, Geolocation, Placement)
Unit / Geolocation / Placement are schema-agnostic IFC helpers ported
from ifcopenshell.util.{unit,geolocation,placement}. Nothing about
them is viewer-specific: pure IfcParse + Eigen, no Qt, no IfcGeom, no
renderer. Living under src/ifcviewer/ implies an unwanted dependency
direction every time a non-viewer caller (test_federation, the bonsai
SettingsView georef readout, a future standalone IFC tool) wants to
use them.
Move them to a new `src/ifcutil/` static lib (IfcUtil). The lib has
PUBLIC `target_include_directories(${CMAKE_CURRENT_SOURCE_DIR})` so
callers that link IfcUtil can keep `#include "Unit.h"` etc. without
relative-path adjustments — the include dir propagates transitively
via IfcViewer's PUBLIC link.
## Changes
* `git mv src/ifcviewer/{Geolocation,Placement,Unit}.{h,cpp}
→ src/ifcutil/` (history follows the rename).
* `src/ifcutil/CMakeLists.txt`: IfcUtil static lib, PUBLIC links
IfcParse + Eigen3::Eigen, PUBLIC include dir.
* `cmake/CMakeLists.txt`: `add_subdirectory(../src/ifcutil ifcutil)`
before ifcviewer/ so the link target exists when IfcViewer's
CMakeLists runs.
* `src/ifcviewer/CMakeLists.txt`: IfcUtil added to IfcViewer's PUBLIC
link_libraries.
* `src/ifcviewer/tests/CMakeLists.txt`: test_federation drops the
explicit `${IFCVIEWER_SRC}/{Unit,Geolocation,Placement}.cpp`
source list and links `IfcUtil` instead (matches how production
code resolves the symbols).
* `src/bonsaiviewer/modules/models/SettingsView.cpp`: the two
explicit `#include "../../../ifcviewer/{Geolocation,Unit}.h"`
paths swap to `../../../ifcutil/…`. All other callers use bare
`#include "Unit.h"` style and continue to work via the propagated
include dir.
## Verification
* `ninja -C build-viewer` builds clean: IfcUtil + IfcViewer +
IfcViewerMinimal + BonsaiViewer + all four pre-existing
ifcviewer tests + the two from-wgpu tests.
* `test_federation` runs green: 226 assertions in 22 test cases
pass with IfcUtil linked instead of the explicit-source compile.
* `git log --follow` traces e.g. `Geolocation.cpp` back through the
rename to its prior location in src/ifcviewer/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b123ee69d6 |
viewer: two-pass alpha transparency + Alt+X global x-ray cap
## The bug
FZK-Haus windows rendered fully opaque despite every piece of the
data path carrying alpha correctly: vertex format is RGBA u8x4,
InstanceCpu/InstanceGpu carry color_override_rgba8 with its alpha
byte, fs_main returns vec4(rgb, in.color.a). Cause: the main render
pipeline's color target had `blend = nullptr`, which in wgpu disables
the blend stage entirely — fragment RGBA overwrites the back buffer
unmodified, alpha discarded.
## Why "just enable blend" isn't enough
Two failure modes that don't go away with a one-liner:
1. `depthWriteEnabled = True` on the main pipeline would make a
transparent window-frame pane occlude geometry behind it in
depth, so the wall behind the window then fails the depth test
and never draws — you'd see the silhouette of the window with
whatever colour was in the back buffer before, not the wall.
2. Order-dependent blending across transparent surfaces in arbitrary
cull order — overlapping transparent surfaces would shift colours
as the camera moves.
Standard fix for a BIM viewer is two-pass opaque-then-transparent.
## What this commit adds
### Per-mesh "has any alpha < 255" classifier
* `ModelGpuData::mesh_has_alpha` (uint8_t vector, parallel to meshes).
* Sized in `applyCachedModel`.
* Populated in `applyStreamedChunk` by scanning each in-chunk mesh's
vertex bytes for a vertex's alpha byte < 255 (offset 11 within
the 12-byte vertex record — the 4th byte of the third u32, which
the shader reads as `w2 >> 24`). Single chunk-arrival site covers
both sidecar streaming and the worker-result drain. First-load
IFC-without-sidecar geometry still routes opaque until the sidecar
bake completes; A-path scan is deferred.
### Per-chunk opaque/transparent partition during cull
* `Chunk::opaque_visible_vertices` / `opaque_visible_draws`
(per-frame counts).
* Transient `visible_draws_scratch_transparent` +
`transparent_per_draw_vertex_counts` filled alongside the existing
opaque half during the cull walk. Post-walk concat appends
transparent entries onto the opaque half and continues the
cumulative prefix-sum sequence — single buffer, single bind
group, no doubling.
* Classifier inside the cull lambda:
`xray_active ? always_transparent
: override_active ? (override.alpha < 255)
: mesh_has_alpha[mesh_id]`
### Per-chunk uniform layout extension
From `[total_draws, total_verts, 0, 0]` to
`[total_draws, total_verts, opaque_verts, opaque_draws]`. The third
slot is what `render()` passes as `firstVertex` to the transparent-
pass draw call so the shader's vid lands in the transparent range of
the same visible_draws_scratch buffer.
### `main_pipeline_transparent_`
Copy of `main_pipeline_` with `color_target.blend = SrcAlpha /
OneMinusSrcAlpha`. depthWriteEnabled stays True (see below).
### Two-pass `render()`
Opaque pass (`main_pipeline_`, firstVertex=0,
vertexCount=opaque_visible_vertices) then transparent pass
(`main_pipeline_transparent_`, firstVertex=opaque_visible_vertices,
vertexCount=total - opaque). Each loop skips empty halves so an
opaque-only chunk costs one draw call, transparent-only one draw,
mixed chunks two.
### depth_transparent.depthWriteEnabled = True (NOT off)
Initially set False (standard "let further-back geometry paint
through transparent front faces" trick) but that broke the edge-
detect pass: edge detection reads the depth buffer to find
silhouette discontinuities, and windows-without-depth meant the
glass had no silhouette at all (panes looked like framed holes) and
the edges of opaque geometry behind the glass painted through at
full intensity. Keeping the write avoids that — trade-off is depth-
test occlusion between transparent surfaces (closer occludes
farther), which for BIM panes that don't overlap in screen space
is invisible. Real fix for the overlap case is OIT or sort-back-
to-front, not depth-write toggling.
## Alt+X global X-ray (drops in basically free)
* `xray_alpha_cap` field on FrameUniforms + WGSL counterpart, default
1.0 (no effect). fs_main clamps `out.a = min(in.color.a, cap)`.
* `ViewportWindow::xray_alpha_cap_` member, default 1.0. Alt+X
toggles between 1.0 and 0.3.
* Cull classifier sees `xray_alpha_cap_ < 1.0` and forces every
instance into the transparent pass so the blend stage actually
fires (an opaque-pass fragment with capped alpha would still
overwrite the back buffer).
* No per-instance state mutation needed — toggle is a single float
in a uniform plus a re-cull. Excluding objects from x-ray later
would mean tagging them so the classifier skips the force-
transparent branch for them, also small.
Stress-tested on FZK-Haus: window glass visibly translucent with
correct silhouette edges; Alt+X turns the whole scene to a tinted
ghost of itself and back without artefact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
748b4e72a9 |
macOS: re-enable Python wrapper + stage IfcViewerMinimal.app bundle
Three coupled fixes that close the macOS bring-up loop:
## 1. ifcwrap: fix INSTALL_RPATH on Apple
The ifcopenshell_wrapper Python module had `INSTALL_RPATH "$ORIGIN"` set
for "NOT WIN32 AND NOT WASM_BUILD" — but `$ORIGIN` is a Linux ld.so
placeholder, not a macOS dyld one. macOS dyld doesn't expand it; it
bakes the literal string `$ORIGIN` into LC_RPATH, which resolves to
nothing at runtime. The wrapper's hard-link `@rpath/ifcopenshell
.document.rdb.dylib` then fails to load even though INSTALL(TARGETS …
LIBRARY DESTINATION "${python_package_dir}/ifcopenshell") above had
already dropped the plug-in dylib right next to the wrapper.
Split the rpath assignment: `@loader_path` on Apple (the dyld
equivalent of `$ORIGIN`), `$ORIGIN` elsewhere.
This is what
|
||
|
|
8ab5c31e75 |
refactor: merge ifcviewer-wgpu into ifcviewer, drop Wgpu prefix
The GL backend is gone (task #53). The wgpu/non-wgpu folder split and the Wgpu* class prefix were both disambiguation artefacts from the overlap period — now pure dead weight. ## Folder + library merge * `src/ifcviewer-wgpu/` → folded into `src/ifcviewer/` (git mv tracks every file as a rename so blame/log history survives). * `src/ifcviewer-wgpu-minimal/` → `src/ifcviewer-minimal/` (the exe was already named `IfcViewerMinimal`; this just brings the folder + CMake target name into line). * `src/ifcviewer-wgpu/tests/test_wgpu_{selection,visibility}.cpp` → `src/ifcviewer/tests/test_{selection,visibility}.cpp`, folded into the existing `add_ifcviewer_unit_test(...)` helper. * The `IfcViewerWgpu` static library is dissolved — its sources become part of the unified `IfcViewer` static library, which now bundles scene/loader + renderer in one target. The pre-merge circular dependency (IfcViewer linking IfcViewerWgpu just to get the ViewportWindow.h include path that SceneLoader.h needs) goes away. * The wgpu-native FetchContent block, the Cocoa/QuartzCore link on Apple, the OBJCXX-enabled `.mm` source, and the wgpu-native runtime install all move into `src/ifcviewer/CMakeLists.txt` unchanged. ## Type renames (Wgpu prefix dropped from every Wgpu* identifier) WgpuAreaMeasurement → AreaMeasurement WgpuBufferPool → BufferPool WgpuLengthMeasurement → LengthMeasurement WgpuMetalSurface → MetalSurface WgpuModelGpuData → ModelGpuData WgpuOverlayFrame → OverlayFrame WgpuOverlayRenderer → OverlayRenderer WgpuSectionPlane → SectionPlane WgpuSelectionState → SelectionState WgpuStreamingLoader → StreamingLoader WgpuStreamingThread → StreamingThread WgpuViewportWindow → ViewportWindow WgpuVisibilityState → VisibilityState CMake target IfcViewerWgpuMinimal → IfcViewerMinimal (exe name was already this since wgpu shipped as default). Deliberately kept: `onWgpuLog` (wgpu-native log callback — names a binding to an external API, not one of *our* types), and the WGPU* enum/struct prefixes from wgpu-native's own headers. `WgpuMemProbe` lives in the separate `src/wgpu-mem-probe/` standalone diagnostic project and isn't touched. ## Include-path updates Every `#include "../ifcviewer-wgpu/Wgpu<X>.h"` → `"../ifcviewer/<X>.h"`, every in-directory `#include "Wgpu<X>.h"` → `"<X>.h"`. Includes from sibling subdirectories (modules/, etc.) are updated to point at `../../../ifcviewer/` instead of `../../../ifcviewer-wgpu/`. ## cmake/CMakeLists.txt simplification The redundant `add_subdirectory(ifcviewer-wgpu)` blocks (one inside the BUILD_BONSAIVIEWER fan-in, one in the BONSAIVIEWER-less standalone block) collapse into a single unconditional `add_subdirectory(../src/ifcviewer ifcviewer)`. The standalone block keeps only `wgpu-mem-probe` (the diagnostic tool, unrelated to the viewer lib). ## Verification * Full build green: `IfcViewer` static lib, `IfcViewerMinimal` exe, `BonsaiViewer` exe, all four pre-existing ifcviewer unit tests, and the two new-location tests (`test_selection`, `test_visibility`). * No stray `Wgpu<X>` identifier remains across `src/ifcviewer/`, `src/bonsaiviewer/`, `src/ifcviewer-minimal/` (verified by grep). * Renames tracked by git as `R` entries — `git log --follow` on ViewportWindow.cpp etc. continues to show history through the move. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7f87408b78 |
bonsaiviewer: rework add-models false-origin guess + drop fly-mode input lag
Two independent threads that landed on this branch.
## 1. Federation false-origin guess: arm-on-add + frame-on-origin
The
|
||
|
|
0e922074b9 |
Adopt _CommitWallDraftsFirstMixin on 7 wall operators
The 7 multi-wall operators (UnjoinWalls, UnjoinWallPathConnection,
ExtendWallsToUnderside, ExtendWallsToWall, SplitWall, MergeWall,
JoinWallsIntersection) each opened their _execute with an identical
prologue:
_commit_pending_wall_edits_for_selection(context)
# ... operator-specific logic
— flushing any in-progress wall parametric drafts so the operator
acts on committed IFC state rather than the draft preview box.
Extract that prologue into _CommitWallDraftsFirstMixin: its _execute
calls the commit helper, then delegates to a subclass-supplied
_perform. Subclasses inherit the mixin first in their bases tuple so
the mixin's _execute resolves first via the MRO. The IFC transaction
opened by tool.Ifc.Operator.execute still wraps both the commit and
the perform.
Behaviour-equivalent — same call, same order, same selection scope.
Architectural cleanup only: a future multi-wall operator can no
longer forget the commit step. The named helper
_commit_pending_wall_edits_for_selection stays as the single
encapsulation of the names=("wall",) filter; its docstring loses
the stale "every multi-wall operator calls it at the top of
_execute" sentence and now just describes the filter contract.
Matches gizmos-8088's _CommitWallDraftsFirstMixin pattern.
Generated with the assistance of an AI coding tool.
|
||
|
|
90ea256cc3 |
Shift-click add-opening preserves filling placement
The regular bim.add_opening click on the host-add-opening gizmo (wall + door/window co-selected) routes through FilledOpeningGenerator.generate, which snaps the filling to the wall's reference-line axis, optionally rotates 180° when the filling sits on the opposite side, and re-applies an rl1 / rl2 Z-elevation default. That is the right default for "drag a fresh door onto a wall and let the model place it for me", but defeats the workflow where the user has already positioned the filling precisely (e.g. snapped to a window in an adjacent wall, copy- pasted at an exact Z, aligned to a reference object). Holding SHIFT while clicking the gizmo now opts into a "preserve placement" mode: the filling stays at its current matrix_world and the opening is created at the filling's existing position. The opening / filling rels and representation work are unchanged — only the snap-to-axis branch is skipped, so the IFC graph is identical to the regular click; only the spatial position of the filling differs (user-chosen vs auto-snapped). Implementation: * bim/module/void/operator.py: AddOpening gains a hidden preserve_placement BoolProperty + an invoke() that sets it from event.shift. The call into FilledOpeningGenerator.generate forwards the flag. bl_description documents the SHIFT modifier so it surfaces in F3 search / hover tooltip. * bim/module/model/opening.py: FilledOpeningGenerator.generate accepts preserve_placement (default False — backwards-compatible with the other caller, tool.Model.add_filled_opening). The voided_obj.data-gated snap block (raycast + axis projection + rl-Z default + filling_obj.matrix_world write) skips entirely when the flag is True. The opening's matrix_world reads from filling_obj.matrix_world below the gate, so the opening lands at the filling's preserved position automatically. Generated with the assistance of an AI coding tool. |
||
|
|
387bd51b4a |
Use menu pick gizmo for door / window / stair type
The door / window / stair edit-row's type-cycle icon advanced one type per click (CycleDoorType / CycleWindowType / CycleStairType bound to cycle_type_operator). DoorType has 8 IFC variants, WindowType 9, StairType 3 — so cycling past the target was the norm. Threshold rule for cycle-vs-menu: cycle is appropriate for exactly 2 values (advance-one-per-click stays predictable). Three or more values warrants a popup menu. Door / window / stair all qualify; roof (RoofGenerationMethod has 2 values) keeps cycle. Wall has no type cycle. Array is unaffected. Swap to the popup-menu pattern (PickTypeMixin already on HEAD at bim/parametric_lifecycle.py:442): clicking the icon opens a menu listing all type_literal values; selecting one applies it in a single undo step. The hamburger icon (VIEW3D_GT_menu) is wired into BaseParametricGizmoGroup.setup_editing_gizmos whenever pick_type_operator is set (mutually exclusive with cycle_type_operator). Matches gizmos-8088's pattern exactly. Per-feature shape: * door.py: PickDoorType replaces CycleDoorType. GizmoDoorEdition.cycle_type_operator → pick_type_operator. * window.py: PickWindowType replaces CycleWindowType. Same swap. * stair.py: PickStairType replaces CycleStairType (no tool.Ifc.Operator inheritance — stair-type changes BIMStairProperties only, no IFC mutation). Same swap. * bim/module/model/__init__.py: registration entries renamed Cycle* → Pick*. * bim/module/drawing/gizmos.py: drop the CycleTypeMixin / PickTypeMixin / TypeAccessorBase shim re-export — its own docstring already noted "PR5 cleanup drops these" and the three callers (door / window / stair Cycle*Type) it served are gone. Roof's CycleTypeMixin import was already direct from bim.parametric_lifecycle. Also update GizmoMenu docstring to reflect the 2-vs-3+ threshold. Generated with the assistance of an AI coding tool. |
||
|
|
de4c394b50 |
Add host-wall offset gizmos for door/window edit
When entering parametric edit on a door or window that fills a wall opening, four dimension gizmos now measure the distances from the wall edges to the filling's jambs and from the wall's base/top to the sill/header. Dragging any gizmo translates the filling along the wall's local axis; 180°-flipped fillings and slanted LAYER2 walls round-trip correctly. The has_host_wall predicate hides all four when the filling → opening → wall chain cannot be resolved. Generated with the assistance of an AI coding tool. |
||
|
|
ab64b652ff |
Show wall cursor gizmos outside edit mode + axis previews
Four concerns that together make the cursor-anchored gizmos
(extend_x_gizmo, extend_z_gizmo, split_gizmo on GizmoWallEdition)
fully functional and visually informative without entering parametric
edit mode first:
* Drop the props.is_editing gate in _update_cursor_gizmos. The three
bound operators (bim.extend_wall_to_cursor,
bim.extend_wall_height_to_cursor, bim.split_wall_at_cursor) already
poll on wall-selected and commit any pending wall edit before
acting, so single-click without entering edit mode is now the
canonical flow. Matches gizmos-8088's always-on behaviour.
* Register GizmoWallEdition instances in a per-region weakref map
(_active_instances) populated at setup_element_specific_gizmos
time. The WallGizmoPreviewDecorator dereferences this map to read
live is_highlight state off the cursor icons. Without the
registration its _cursor_icon_hovered always returned False and
the hover-gated GPU previews silently never drew. Mirrors the
same pattern already in place on GizmoWallJoinIntersection.
* Add post-operator resync to all three cursor operators
(_maybe_resync_wall_props_from_ifc for the single-wall split /
extend-height paths, _resync_walls_after_mutation for the
selection-wide extend-X path). Without this, props.length /
props.height stayed stale after the operator ran, so the
orientation flips _apply_wall_extend_flips computes from
cursor_local vs wall dimensions kept using the pre-extend values
until the next selection change. Matches gizmos-8088's pattern.
* Hover-gated GPU previews per icon:
- extend-X: filled Z=0 floor quads spanning the wall's offset to
offset+thickness Y band, visible from plan view without side-
view clutter. Grow case (cursor beyond either endpoint): one
green decorator_color_selected quad over the extension. Shrink
case (cursor inside extent): green quad for the portion that
REMAINS + red decorator_color_error quad for the portion the
operator REMOVES.
- extend-Z: vertical lines at the cursor's projected X in the
wall's y=0 reference-line plane. Grow case (cursor above wall
top): one green segment from z=height to z=cursor.z. Shrink
case: green from z=0 to z=cursor.z (REMAINS) + red from
z=cursor.z to z=height (REMOVES).
- split: one red vertical line at the cursor's projected X from
base to wall top — the cut plane.
Quads use QUAD_ALPHA=0.25 so the underlying wall body stays
visible.
* New module-level _fill_quads_alpha helper next to
_stroke_lines_alpha, plus a per-decorator _fill convenience method
and a _wall_floor_quad corner builder.
Modal-active gizmo hiding (is_gizmo_hidden_by_modal) is preserved.
Generated with the assistance of an AI coding tool.
|
||
|
|
99bb1e30ad |
Generalise opening gizmos + DRY toolbar plumbing
Add openings — GizmoWallAddOpening only fired when a wall was active + co-selected with a non-host; slabs and roofs got no in-viewport handle. GizmoHostAddOpening covers all three host types via is_supported_host, dispatching walls to the axis-projection anchor and slabs/roofs to a world-Z anchor lifted just above the host's top face (predictable height regardless of the void's vertical position). Show openings on hosts with their own parametric-edit toolbar — GizmoRoofEdition gains an idle-row toggle_openings_gizmo parallel to the wall's, parked at the cancel-slot X next to the pen. Visible only when the host carries HasOpenings and the edit triad is idle. Roof overrides get_element_height to return the mesh's world-AABB top in object-local Z, so the WHOLE pen-row anchors visibly above sloped or stepped roof bodies. The wall's idle-row toggle now also hides when HasOpenings is empty. Show openings on hosts WITHOUT a parametric-edit toolbar — GizmoHostToggleOpenings scoped strictly to the fallback case: a single host selected, HasOpenings non-empty, NOT a path-connectable wall, NOT a parametric roof. Covers slabs today plus any foreign-authored IfcRoof without BBIM_Roof. Anchored at object origin XY + world-AABB top Z. When slab parametric-edit eventually lands, the slab predicate joins the exclusion list and this gizmo's poll narrows automatically. Operator move — ToggleWallOpenings was already host-agnostic; renamed to ToggleHostOpenings in opening.py (bl_idname bim.toggle_host_openings). Three callers (the wall idle-row binding, GizmoWallFilletToggleOpenings, and workspace.py's hotkey_A_O for Alt+O) now route through the renamed operator. The Alt+O binding is surfaced in the operator's bl_description so it appears in F3 search and hover tooltips. DRY refactors — * GizmoWallAddOpening deleted (subsumed by GizmoHostAddOpening) * tool.Blender.get_object_world_bounding_box added as the world-AABB sibling of the existing local helper; 3 inline call sites in tool/misc.py (set_object_origin_to_bottom, scale_object_to_height) and gizmos.py adopt it (2 other sites in drawing/operator.py and project/operator.py inherently need raw transformed corners for per-corner plane / NDC tests — not AABB candidates) * BaseParametricGizmoGroup gains setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon; wall + roof + any future host gizmo wire up the idle-row toggle with two one-line calls * _resolve_active_host shared poll prologue between the two host gizmos (gate + selection count + active-in-selected + entity lookup + supported-host check) * HasOpenings non-empty checks at 3 sites route through tool.Geometry.has_openings * hotkey_A_O body collapsed to bpy.ops.bim.toggle_host_openings() The forward-compat AST guard pinning "must accept fillet-corner walls" retargets from GizmoWallAddOpening.poll to is_supported_host. Generated with the assistance of an AI coding tool. |
||
|
|
2b91e41fc4 |
bonsaiviewer: stage all IfcOpenShell dylibs (core + plug-ins) on macOS
The .app bundle's Frameworks/ staging rule was only globbing ifcopenshell.*.dylib (the dlopen-only plug-ins) on the assumption that macdeployqt would follow BonsaiViewer's link-time @rpath deps for the lib-prefixed core shared libs. In practice it doesn't — non-Qt @rpath deps whose source path is outside the standard system / Qt prefixes get skipped silently. In a static build this didn't matter: libifcopenshell.geometry, libIfcParse, libIfcViewer, etc. were statically embedded in BonsaiViewer.exe, so there was no runtime dep. With --shared (added in |
||
|
|
cc54237f51 |
viewport: move first-model false-origin guess out of refresh()
Loading a model whose first placement sits at the world origin
stack-overflowed BonsaiViewer instantly on Windows (and macOS).
WinDbg trace was a 5-frame Qt signal-slot cycle hitting the guard
page ~1400 levels deep; Linux escaped only because that machine's
iterator order put a non-origin instance first, which made the guess
return a non-default value and naturally terminated the recursion
after one step.
Root cause is the architecture, not the specific guard inside the
guess function. `ViewportView::refresh()` was connected to six
SessionState signals (projectReset, projectOpened, modelsChanged,
federationChanged, visibilityChanged, modelGeometryReady) and was
calling `maybeGuessFederatedFalseOrigin` on every model on every
fire. That helper called `session_state_->notifyFederationChanged()`
unconditionally after the mutation, which re-emitted
SessionState::federationChanged, which re-entered refresh(), which
re-entered the guess — a hidden emit-in-slot loop. The "current ==
defaults" guard at the top of the guess prevented further mutations
once the value moved off defaults, but on machines where the guess
itself returned defaults the guard never fired and the loop ran
forever.
Cleanup:
* refresh() is now terminal: it reads federation state, pushes it to
the viewport, and returns. No mutations, no signal emissions.
maybeGuessFederatedFalseOrigin is removed from its for-loop.
* The guess is renamed to `tryGuessFirstModelFalseOrigin(uint32_t)`
and is now invoked only from the modelGeometryReady connection,
not from refresh(). Conditions:
1. modelIds().size() == 1 (the just-loaded model is the only
model — i.e. this is the "first model added" edge)
2. federation->federatedFalseOrigin() == defaults (nobody has
set the origin yet — possibly because the previous attempt
guessed defaults and no-op'd, in which case we deliberately
want to retry next time a model lands)
No one-shot flag: add→remove→add cycles re-attempt the guess
precisely while the origin is still default, which is the right
semantics.
* SessionState now relays Federation::federatedFalseOriginChanged
onto its own bus via notifyFederationChanged. This replaces the
manual `session_state_->notifyFederationChanged()` call the old
guess made post-mutation. With the relay in place, any future
mutation site (commands, settings dialog, project load) will
propagate to views automatically — the emit point lives at the
data change, not at every caller. Views still subscribe to
SessionState only; Federation stays a back-end detail.
Reproduced with ISSUE_053_20181220Holter_Tower_10.ifcview on
Windows (build
|
||
|
|
ddee88bed3 |
build_osx: --shared + skip geometry-writer plug-ins (~3x bundle shrink)
Mirrors the Rocky workflow's two-part size reduction (27249770e "Reduce Rocky package size") on macOS: 1. Pass `--shared` to nix/build-all.py. The default builds IfcOpenShell as static libs, which means every plug-in dylib (schemas × 8, kernels × 3, mappings × 8, writers × 8, document serializers × ~4, linework processing) statically embeds a full copy of libIfcParse + libIfcGeom. With --shared the plug-ins reference @rpath/libIfcParse.dylib + @rpath/libIfcGeom.dylib and the per-plug-in dylib drops from ~30-50 MB to a few MB each. Dominant size win. 2. Filter `ifcopenshell.geometry.writer.*.dylib` out of BonsaiViewer's plug-in staging step in src/bonsaiviewer/CMakeLists.txt. These are the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export converters — heavy because each one inlines the full schema, and BonsaiViewer is a viewer, never an exporter, so they're pure deadweight inside the bundle. Additive on top of --shared. Bundle went 300 MB → expected ~100 MB, in line with Linux (~100 MB) and Windows (~80 MB). The IFCOPENSHELL_BUILD_PYTHON_WRAPPER=off gate is unchanged for now — once we confirm BonsaiViewer.app size + functionality look sane, we can ungate the Python wrapper and see if shared-builds-on-macOS shake out its install issues too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1707c36bd8 |
Stack cursor-anchored wall gizmos along screen-up in top view
The extend-X / extend-Z / split icons share the cursor's projected X on the wall axis, separated only by world Z (floor / cursor / wall top). World Z collapses to a single screen point in plan view, so every icon piled onto extend-X's hit target and only the topmost was clickable. Two refinements ported from gizmos-8088: * When ``tool.Blender.is_view_top_down(context)`` reports the camera is near plan-view, swap world-Z stacking for screen-up stacking: anchor all icons at the floor world position and offset each by ``index * CURSOR_STACK_OFFSET`` along ``tool.Blender.get_screen_up_world(context)``. Each icon lands in its own screen-space slot regardless of view rotation. * In the same top-down branch, drop ``extend_z_gizmo`` entirely. A vertical-intent gizmo has no readable cue when looking down +Z — clicking it would mutate the wall in a direction the user can't see change. * Bonus: split's local Z now goes through ``core.extrusion_depth_from_vertical_height(props.height, props.x_angle)`` so the icon lands on the slanted top edge of sloped walls (x_angle != 0) instead of the vertical-height target the wall isn't at. All three helpers (``is_view_top_down``, ``get_screen_up_world``, ``extrusion_depth_from_vertical_height``) already on HEAD from PR2/PR3. Non-top views unchanged — same world-Z stacking + cascading bumps as before. Generated with the assistance of an AI coding tool. |
||
|
|
44f5ee028f |
Port WallGizmoPreviewDecorator from gizmos-8088
Hover-gated viewport preview lines that show where a wall-join / extend / split operator would land before the user clicks. Four preview paths, each gated on a specific icon's ``is_highlight`` state: * **Join intersection** — two LAYER2 walls selected in the ``intersect`` state (non-joined, non-collinear, non-parallel). Draws four lines: each wall's axis at both base and top Z, extending from the wall's nearer endpoint to the projected XY intersection. The pair of lines per wall communicates the full plane the join welds at, not just the floor edge. * **Cursor extend** — single LAYER2 wall, hover on ``extend_x_gizmo``. One line from the wall's nearer X endpoint to the cursor's projected X on the wall axis. * **Cursor extend-Z** — hover on ``extend_z_gizmo``. Vertical line at the cursor's projected X from wall base to cursor Z (the new total height). * **Cursor split** — hover on ``split_gizmo``. Vertical line at the cursor's projected X from wall base to wall top — the cut plane. Warning-red colour matches the icon's destructive-action signal. Hover colour rules for the join preview: * **Join or Fillet hover** → all four lines highlight in ``decorator_color_selected``. Both icons commit a symmetric corner meet, so every line is part of the operation. * **Extend-to-Wall hover** → only the non-active wall's two lines (base + top) highlight. The default-direction extend operator moves the non-active wall into the active one's axis; only that wall's preview should signal motion. * No hover → all four lines in ``decorations_colour``. Three coordinated changes: * ``bim/module/model/wall.py`` gains the ``_classify_wall_join_state`` wrapper over ``core.classify_wall_join_state`` (feeds the ``_are_walls_joined`` flag the core helper expects) AND a ``_active_instances`` per-region weakref ClassVar on ``GizmoWallJoinIntersection`` populated in ``setup()``. Without the weakref registration, the decorator's ``_lookup_active_instance`` call returns None every frame and the hover gates silently evaluate False — the symptom would be preview lines that never switch colour. Both pieces ported from gizmos-8088. * ``bim/module/model/decorator.py`` gains ``WallGizmoPreviewDecorator`` (~280 LOC across the four preview paths + shared helpers ``_stroke`` / ``_active_layer2_wall_for_gizmo_preview`` / ``_join_group_hover_state`` / ``_extended_wall_index``). All cross-file dependencies (``core.classify_wall_join_state``, ``core.wall_join_preview_lines``, ``_stroke_lines_alpha``, ``_cursor_icon_hovered``, ``_lookup_active_instance``, ``tool.Parametric.is_path_connectable_wall``, ``_wall_axis_world_segment_from_geom``) already on HEAD. * ``bim/handler.py`` wires ``WallGizmoPreviewDecorator.install()`` / ``.uninstall()`` alongside the other always-on preview decorators. The decorator self-polls every frame; cost is one selection-count check + one ``is_highlight`` read when no eligible state is active. Verified: headless smoke green, ruff + black clean. Live testing confirms the four preview paths fire correctly when hovering each icon. Generated with the assistance of an AI coding tool. |
||
|
|
ed7b2fc233 |
Stack wall-join trio along screen-up + L/T glyphs
GizmoWallJoinIntersection used to place its icons at state-specific world points: join at floor Z, extend-to-wall at the active wall's top Z, fillet stacked screen-up above join. Same XY at different Z collapses to a single screen pixel in plan / top view, so two icons became one hit target — invisible from above. * position_gizmos now always-stacks along screen-up at a wall-top anchor in both the joined (unjoin + fillet) and the intersecting (extend + join + fillet) states. Order bottom-up is extend / L / fillet. Collinear-merge keeps its single boundary icon (no stack needed). * New _stack_anchor_z picks the active wall's top Z (or the taller of the two on mid-selection-transition frames). New _stack_at lays a tuple of icons along screen-up at the resolved anchor. * Glyph swap: join_icon -> VIEW3D_GT_wall_corner (L), extend_to_wall_icon -> VIEW3D_GT_wall_tee (T). Both classes already existed in bim/module/drawing/gizmos.py from an earlier commit; only the setup() bl_idname strings changed. The previous arrow-merge / arrow-extend pair read as the same direction once stacked. Forward-compat AST contracts in test_wall_gizmos_forward_compat.py pin the new invariants: the L and T bl_idnames must appear in setup(), and position_gizmos must route through _stack_at so a regression that reintroduces a direct billboarded_at write for any state-specific icon fails CI before it flattens the stack again. Also folds in a one-line typo fix in core/spatial.py: assign_container's per-element can_contain check iterated `e` but predicate-tested `root_element` (the outer for-loop variable), so every element in the comprehension was tested against the same container/element pair. Switch the argument to `e`. Generated with the assistance of an AI coding tool. |
||
|
|
2c18155d98 |
Merge ifcopenshell/v0.8.0 into parametric-framework-pt2
Bring in 13 commits from upstream v0.8.0 (tip
|
||
|
|
37aa68e66c |
ifcopenshell plug-in loader: stable anchor + bundle-aware fallbacks
# What Two upstream-shaped fixes to ifcopenshell's runtime plug-in discovery so the schema/serializer plug-ins are findable in deployment layouts other than a flat \`<prefix>/lib/\` (specifically: macOS .app bundles). ## (1) Stable anchor variable instead of a function pointer \`schema_plugin_directory()\` used \`&load_schema_plugins\` as the anchor whose containing module \`dladdr\` is asked to resolve. Function addresses are not reliably equal to a single canonical location across toolchains — on macOS arm64 with BonsaiViewer.app, \`&load_schema_plugins\` took the address of a PLT/stub inside the consumer binary rather than the actual symbol inside \`libIfcParse.dylib\`. \`dladdr\` then dutifully returned the consumer's path and the loader started searching \`BonsaiViewer.app/Contents/MacOS/\` for plug-ins that were never installed there. A variable doesn't suffer from this — it has exactly one canonical address inside its defining dylib. Add \`ifcopenshell_libifcparse_anchor\` (exported via IFC_PARSE_API) and use \`&that\` instead. Standard pattern used by Boost.DLL, GStreamer, \`_dyld_get_image_*\`, etc. ## (2) Bundle-aware fallback search paths The primary search path is \`dirname(libIfcParse)\`. That works for flat installs (Linux \`lib/\`, Windows \`bin/\`) where plug-ins are siblings of libIfcParse. macOS app bundles split the layout: \`macdeployqt\` puts non-Qt @rpath deps in \`Contents/Frameworks/\`, Apple convention asks for \`Contents/PlugIns/\`, and some install rules co-locate libIfcParse with the exe in \`Contents/MacOS/\`. Plug-ins typically end up in a sibling directory, not the same one. In \`add_search_paths_or_default\`, after registering the primary path, also register \`<parent>/PlugIns\`, \`<parent>/Frameworks\`, and \`<parent>/MacOS\` on Apple platforms. \`discover_exact\` short-circuits on the first hit so duplicates and missing directories are harmless. # What this does NOT do The plug-in dylibs still need to actually be inside the app bundle somewhere for these fallbacks to find them — the upstream install rules (\`install(TARGETS …)\` in \`src/ifcparse/CMakeLists.txt\`, \`src/serializers/CMakeLists.txt\`, etc.) put them in \`<prefix>/lib/\` which lives outside \`BonsaiViewer.app\`. That side of the fix is a follow-up — either an explicit bundle-aware install destination on the plug-in targets, or an install(CODE) sweep that mirrors them into the bundle. # What this also un-does Reverts the BonsaiViewer-only \`install(CODE)\` hack that was about to copy \`ifcopenshell.*.dylib\` from \`lib/\` into \`BonsaiViewer.app/Contents/MacOS/\` — superseded by the loader-side fix above, which lets us put the plug-ins anywhere sane inside the bundle without further consumer-side stitching. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1e8c0b86a0 |
Migrate Modifier shim callers + drop the shim block
Completes the PR4/PR5 cleanup the FIXME at tool/blender.py
flagged: every is_<type> / Array.<helper> shim on
tool.Blender.Modifier delegated one-for-one to tool.Parametric /
tool.Array. Callers now reach the canonical home directly, and the
shim block — seven is_<type> classmethods plus the inner class Array
— comes out.
Renames (no semantic change):
* tool.Blender.Modifier.is_<door|railing|roof|stair|wall|window>
→ tool.Parametric.is_<x>
13 sites across tool/loader.py, bim/import_ifc.py,
bim/module/geometry/{data,operator}.py, bim/module/model/{door,
railing,roof,stair,ui,wall,window}.py.
* tool.Blender.Modifier.Array.<helper> → tool.Array.<helper>
4 sites across tool/root.py, bim/import_ifc.py,
bim/module/geometry/operator.py.
* test_parametric_registry.py: the two getattr probes that hunt
predicates by name now look on tool.Parametric. Docstring + the
test function name (test_every_entry_has_modifier_predicate →
test_every_entry_has_parametric_predicate) follow the move.
Kept on tool.Blender.Modifier (non-shim, no equivalent on
tool.Parametric): try_applying_edit_mode,
try_canceling_editing_modifier_parameters_or_path,
is_eligible_for_<x>_modifier (×5), is_array_child, is_slab.
Verified: 109 model-lane tests + 8 parametric-registry tests pass
(the one pre-existing failure in test_wall_header_refresh.py is
unrelated — it patches handler.update_bim_tool_props which has been
renamed). git grep for tool\.Blender\.Modifier\.(is_<type>|Array\.)
returns empty. black + ruff clean on every touched file.
Generated with the assistance of an AI coding tool.
|
||
|
|
ac11044261 |
Add GizmoRoofEdition + fix low-slope normals + cancel restore
Ports roof parametric edit gizmo group from gizmos-8088 and folds in
three roof-mesh bug fixes surfaced during live testing.
Port:
* ``CycleRoofGenerationMethod`` operator (bim.cycle_roof_generation_method)
cycles props.generation_method between "HEIGHT" and "ANGLE". Shift+click
cycles in reverse via the ``CycleTypeMixin`` contract.
* ``GizmoRoofEdition`` gizmo group: 3 dimension gizmos for height
(visible in HEIGHT mode) / slope angle with tan/atan2 rise round-trip
+ degree formatter (ANGLE mode) / roof_thickness. All three handles
anchor at the object's local origin and separate visually via their
declared axes (height/slope +Z, thickness -Z) — height + slope are
mutually exclusive via ``visibility_condition`` so they never paint
at the same time. Anchoring at the origin sidesteps the first-click
default-identity-matrix symptom that footprint-derived anchoring
would have hit on a stale ``RoofData`` cache.
* Lifecycle factory swap: explicit ``EnableEditingRoof / CancelEditingRoof
/ FinishEditingRoof`` classes replaced by ``tool.Parametric.build_edit_lifecycle("roof", _RoofEditMixin, ...)``.
Same bl_idnames out, no external caller changes.
* Registration: ``CycleRoofGenerationMethod`` + ``GizmoRoofEdition``
added to ``bim/module/model/__init__.py`` classes tuple.
* Tests: ``test_roof_gizmos.py`` covering slope round-trip, visibility
gates, cycle operator metadata, and origin-anchored positioning.
Bug fixes:
* ``generate_hipped_roof_bmesh`` flipped the bottom slab face's normal
at low slope angles. The kernel's outward-inference becomes
ambiguous on near-flat geometry once ``remove_doubles`` and
internal-face deletion run, and the early ``recalc_face_normals``
pass at line 389 ran BEFORE the topology was final. A second pass
on the final closed mesh fixes the eave plane (now reliably points
down regardless of slope).
* ``bpypolyskel.polygonize`` can emit a face whose vertex list
contains the same index twice on certain footprint/slope
combinations (a straight-skeleton ridge collapse). ``bm.faces.new``
rejects those with ``found the same (BMVert) used multiple times``,
aborting the whole rebuild. Filter the degenerate faces out so the
rest of the roof renders.
* ``_RoofEditMixin._restore_viewport_after_cancel`` now rebuilds the
bmesh from the just-restored draft via ``update_roof_modifier_bmesh``.
The hook was abstract on ``PathPreservingEditMixin`` and raised
``NotImplementedError`` on cancel-after-edit, leaving the user
stranded.
Also folds in a parallel ``tool/loader.py`` swap from
``tool.Blender.Modifier.is_railing`` to ``tool.Parametric.is_railing``
(consistent with the rest of the loader using ``tool.Parametric.*``).
Verified: headless smoke green, test_parametric_registry.py 8/8,
test_roof_gizmos.py 15/15. ruff + black clean on the touched files.
Generated with the assistance of an AI coding tool.
|
||
|
|
ab9152e32d |
Fix fillet preview crash + surface openings on fillet walls
Three wall-gizmo fixes: * GizmoWallFilletPreview crashed on every draw_prepare after the DRY-colors refactor moved decoration lookups onto self.get_decoration_colors() — that method lives on BillboardingGizmoGroupMixin / BaseParametricGizmoGroup, but GizmoWallFilletPreview inherited only from bpy.types.GizmoGroup. setup() AttributeError'd silently, leaving radius_dim and friends unset. Add the mixin to the bases; rename _position_gizmos to position_gizmos so the mixin's refresh/draw_prepare dispatch lands correctly and drop the now-redundant overrides. * GizmoWallAddOpening's poll gated on the strict is_wall predicate, which rejects fillet-corner walls (no LAYER2 usage by IFC spec). Switch to is_path_connectable_wall on both the active and the partner-exclusion checks so the add-opening icon surfaces over curved corners — matching every other wall-state gizmo's host gate. * Show / hide openings was only available on LAYER2 walls because GizmoWallEdition's parametric edit pipeline (which carries the toggle) refuses fillet bodies. Add GizmoWallFilletToggleOpenings, a dedicated single-icon group that polls on is_fillet_corner_wall and reuses bim.toggle_wall_openings — the body stays untouched. Forward-compat AST guards in test_wall_gizmos_forward_compat.py pin both invariants: every wall GizmoGroup that calls self.get_decoration_colors() must inherit a mixin that provides it, and GizmoWallAddOpening.poll must keep using the looser predicate. Generated with the assistance of an AI coding tool. |
||
|
|
18dc7abb06 |
Split update_bim_tool_props commit vs selection
tool.Parametric.refresh_post_commit was calling update_bim_tool_props after every IFC mutation. The function does two things — refresh read-only header values (extrusion_depth/length/x_angle) and re-target user-intent enums (ifc_class, relating_type_id) from the active object. Doing both on the commit path crashed on IfcAnnotation actives (the type isn't in the bim_tool ifc_class enum) and silently overwrote the user's "what to build next" choice on every other element. Split the function: update_bim_tool_props remains selection-driven and does both halves; new refresh_bim_tool_headers is header-only and is what refresh_post_commit now calls. Behaviour on selection change is preserved. Also ports the upstream PR #8136 try/except guard onto the props.ifc_class write for the selection-driven path. Adds test_handler_forward_compat.py to pin both contracts via AST. Generated with the assistance of an AI coding tool. |
||
|
|
5ffae41bbd |
WgpuViewportWindow: stop using QSurface::MetalSurface on macOS
NSZombieEnabled-lldb on macOS revealed the actual cause of the
"OS_os_log displayLock" / segfault that has been chasing us:
*** -[QMetalLayer displayLock]:
message sent to deallocated instance 0xb5e234e40
Qt's QMetalLayer (its CAMetalLayer subclass installed when surfaceType
== MetalSurface) is dealloc'd while Qt's QCocoaWindow still holds an
internal reference to it. Once wgpu-native bridge-retains the layer in
its Rust surface code and re-publishes the drawable pool from
configureSurface, Qt's QMetalLayer life is implicitly handed to
wgpu-native and Qt's separate ref winds up dangling. The next Qt
expose event sends -displayLock to the dead pointer.
Earlier guesses (Qt-6.11/macOS-26 incompatibility, multi-display) were
both wrong — single-display still crashed, and the Tahoe os_log
selector-cast theory was a red herring; the real error is "deallocated
instance", revealed only by NSZombieEnabled.
# Fix
Set surfaceType to OpenGLSurface on macOS too. On macOS that still
gives us a layer-backed NSView; Qt just doesn't install QMetalLayer.
WgpuMetalSurface_mac.mm's else-branch (the one that always fires when
the existing layer isn't already a CAMetalLayer) now consistently
attaches a vanilla CAMetalLayer we fully own — wgpu-native can do
whatever it wants to the layer's lifetime without stepping on any
Qt-side bookkeeping.
We never bind a real GL context on top of OpenGLSurface — it's just
the most portable "hardware-rendering-ready surface" hint Qt has, and
it's already what Linux and Windows use.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b7549f2476 |
Wire array panel buttons to triad lifecycle
Two bugs in BIM_PT_array: 1. The "is this layer in edit mode" predicate compared a BoolProperty against an int (props.is_editing == i). Python evaluates False == 0 as True, so layer 0 always rendered the per-layer edit form even when no edit was active — clicking validate/cancel then dispatched against a phantom edit state. Switched to props.editing_item_index == i, which defaults to -1 and matches exactly one layer when an edit is active. 2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array / bim.disable_editing_array, a parallel lifecycle that only cleared editing_item_index. Entering edit mode via the viewport gizmo (bim.enable_editing_array, the triad enter) sets is_editing=True and hides array children; the legacy panel exit unwound neither — so committing or cancelling from the panel left is_editing=True with children hidden, and the viewport gizmo thought the edit was still in progress. Re-bound both panel buttons to the canonical triad operators (bim.finish_editing_array / bim.cancel_editing_array), which _ArrayEditMixin already owns and which the viewport gizmo group already uses. Panel and gizmo now share one exit path. The three now-unreachable operators are deleted with their registration entries: EditArray (bim.edit_array), DisableEditingArray (bim.disable_editing_array), and EnableEditingArrayItem (bim.enable_editing_array_item, never called from any UI). The two test/tool/test_model.py sites that drove bim.edit_array as a commit step are switched to bim.finish_editing_array. External scripts or user keymaps bound to bim.edit_array / bim.disable_editing_array will need to update — the replacements are bim.finish_editing_array and bim.cancel_editing_array, both taking no parameters (the layer is read from props.editing_item_index). Partly generated with the assistance of an AI coding tool. |
||
|
|
ba6cfe9c24 |
Fix door swing arcs + declarative SwingArcConfig
The recent per-gizmo-prefs cleanup left ``update_swing_gizmos`` with a stale ``prefs`` reference that raised NameError mid-refresh, so the flip arc's ``matrix_basis`` was never reassigned and the gizmo drifted to the world origin. SINGLE_SWING_RIGHT also lacked an X-mirror on the primary arc, so the swing extended past the door's right edge instead of sweeping back over the panel. Five related fixes / additions: * Drop the leftover ``prefs.decorations_colour[:3]`` per-frame colour override (the setup-time ``decorator_color_special`` is the durable contract — there's no reason to overwrite it every refresh). * Add X-mirror to RIGHT-hinged single-panel transforms so the arc sweeps back over the door rather than past the right edge. * Treat DOUBLE_DOOR_SINGLE_SWING as a two-panel layout: 4 arcs total (left + right panels, each with its own Y-mirrored flip) scaled to ``overall_width / 2``. * Hide all swing arcs for SLIDING_TO_LEFT / SLIDING_TO_RIGHT / DOUBLE_DOOR_SLIDING — sliding doors don't swing. A slide-direction indicator is deferred to a separate change. * Pin ``select_bias = -1000.0`` on every arc gizmo so the big quarter-arc hit shapes don't steal clicks from the smaller dimension and edit gizmos drawn on top. Architectural cleanup driven by the same diff: the imperative 4-create + 50-line update block is replaced by a declarative ``swing_arc_props`` list of ``SwingArcConfig`` entries (mirrors the existing ``dimension_gizmo_props`` pattern). Setup iterates the list and creates one (main, flip) pair per entry under ``gizmo_swing_arc_<name>`` / ``gizmo_swing_arc_<name>_flip``; update iterates the same list and positions each pair via the lambdas. Adding a hypothetical multi-panel variant becomes a config entry rather than two more attribute names plus a transform branch. ``ToggleDoorSwing`` gets a ``description`` classmethod that returns user-facing wording per ``flip_geometry`` branch so the tooltip on hover stops reading like operator internals. ``test/bim/module/model/test_door_gizmos.py`` (new) pins the per-door-type contract: 11 cases covering LEFT / RIGHT hinge positions, DOUBLE_SWING parity with SINGLE_SWING, DOUBLE_DOOR 4-arc layout, the sliding-types hide invariant, ``is_editing=False`` hide invariant, flip-arc matrix re-assignment, and world-matrix pre-multiplication. Verified: ``pytest test/bim/module/model/test_door_gizmos.py`` 11/11 green; combined wall + stair + door gizmo lanes 37/37 green; ruff + black clean on the three touched files. Generated with the assistance of an AI coding tool. |
||
|
|
aba8ac727d |
wgpu: query surface capabilities before configuring present mode
Default WGPUPresentMode was a static Mailbox. That works on DX12 and
on Vulkan with most drivers, but the Metal backend in wgpu-native v29
only exposes [Fifo, Immediate], and asking for Mailbox makes
wgpuSurfaceConfigure panic from Rust:
thread '<unnamed>' panicked at src/lib.rs:605:5:
Error in wgpuSurfaceConfigure: Validation Error
Caused by:
Requested present mode Mailbox is not in the list of supported
present modes: [Fifo, Immediate]
fatal runtime error: failed to initiate panic, error 5, aborting
# Fix
Query wgpuSurfaceGetCapabilities and walk a preference list
(Mailbox → FifoRelaxed → Immediate → Fifo), picking the first mode
the surface actually lists. Fifo is the only spec-required mode and
will always be present, so the loop always finds something.
The env override (WGPU_PRESENT_MODE=...) still wins when set, but
also drops back to Fifo if the requested mode isn't supported on the
current backend — no panic.
# Outcomes per backend
DX12 / Vulkan: picks Mailbox (low input lag, our previous default).
Metal (macOS): picks Immediate (Mailbox unavailable). On Metal the
CAMetalLayer presents through CoreAnimation, so the
compositor still vsync-aligns; Immediate is effectively
low-latency-with-no-tearing on macOS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f66ad22f1d |
macOS: ship libwgpu_native.dylib into the bundle and set the rpath
BonsaiViewer.app crashed at launch on a fresh macOS arm64 mac with:
Library not loaded: @rpath/libwgpu_native.dylib
Referenced from: /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer
Reason: no LC_RPATH's found
The exe had \`LC_LOAD_DYLIB @rpath/libwgpu_native.dylib\` (CMake baked
that in from the upstream dylib's install_name), but zero \`LC_RPATH\`
entries, so dyld had nowhere to look — and macdeployqt hadn't pulled
the dylib in either, since it sat at \`<install_root>/lib/\` rather
than inside the .app.
Two changes:
- \`src/ifcviewer-wgpu/CMakeLists.txt\`: on macOS, when
BUILD_BONSAIVIEWER is on, install libwgpu_native.dylib straight
into \`BonsaiViewer.app/Contents/Frameworks/\` instead of
\`<prefix>/lib/\`. That matches the standard macOS bundle layout.
- \`src/bonsaiviewer/CMakeLists.txt\`: set
\`INSTALL_RPATH "@executable_path/../Frameworks"\` on the
BonsaiViewer target on Apple. That's where dyld looks at launch,
and where the dylib now lives.
Together the @rpath load resolves at launch without depending on
macdeployqt to follow non-Qt @rpath references (it usually only
chases Qt frameworks).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
edc598357b |
wgpu: default present mode to Mailbox (was Fifo)
# Why The stage-1 default was Fifo because it is the only present mode WebGPU spec *requires* every backend to support — conservative, "always works", lowest power. But on DX12 Fifo maps to a DXGI flip-discard swap chain whose default \`MaximumFrameLatency\` is 3, which queues ~50ms of pre-rendered work between submit and display. Even when the FPS counter shows 60 the cursor-bound interactions (marquee, pivot, orbit) feel ~3 frames behind because they *are*. # What Mailbox: vsync-aligned (no tearing) with a one-frame queue (last-frame-wins). ~16ms input→display latency. wgpu-native handles the fallback to Fifo automatically on backends that don't implement Mailbox (Vulkan + NVIDIA on Linux is the historical one). # Trade-off Mailbox uncaps the render loop: with no vsync gating, the \`requestUpdate()\` → render → present loop spins at whatever Qt's event loop allows (~600 Hz on an empty scene), and the GPU does useful-but-discarded work on each redundant frame. On any non-trivial scene the GPU is the bottleneck and the loop self-paces near the display rate. The FPS counter measures \`render()\` invocations, not unique frames the user actually sees — that's expected. # Override WGPU_PRESENT_MODE=fifo restores the old behaviour for the rare laptop-battery / heat-conscious case. fifo_relaxed / immediate also still work as before. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f158ae7377 |
Fix crash in update_bim_tool_props when selected type isn't a valid ifc_class
props.ifc_class is an EnumProperty whose items list only the element/space
types present in the model. Assigning element_type.is_a() crashed with
`enum "<class>" not found` when the selected element's type wasn't a member
(e.g. a raw IfcTypeProduct, or a stale item list mid-rebuild), aborting the
post-commit refresh.
Wrap the assignment in the same try/except TypeError guard already used for
the sibling relating_type_id assignments (added in
|