Commit Graph

19911 Commits

Author SHA1 Message Date
Dion Moult e8a6d2a92f ifcviewer-web: end-to-end rendering on Chrome + Firefox
Wire up wgpu init + scene load + RAF render so the embedded sample
sidecar paints on the canvas in both browsers.

Root-cause fix: BufferPool::addSubBuffer spin-waited on
PopErrorScope, which resolves via JS microtask on Dawn-web. The
spin blocked the JS event loop, so the microtask never fired and
the first allocation hung the page indefinitely. Skip the
error-scope dance on Emscripten; trust the buffer pointer.

ViewportCore: add initWgpuAsyncWeb (nested-callback adapter→device
chain with AllowSpontaneous mode, no spin) and loadSidecarFromPath
(Qt-free entry point). waitTickInstance becomes a no-op shim on
web; cull-threads / streaming_thread_ / wgpuSurfacePresent gated
off; chunk I/O runs inline.

main_web.cpp: AppState + initWgpuAsyncWeb → buildPipelines (+
HiZ/edge/pick) → loadSidecarFromPath → ready flag → Module._app_ptr
handoff. The RAF loop lives in shell.html (NOT here) because any
RAF helper called from inside Dawn-web's wgpu Promise.then chain
stalls the device callback.

CMakeLists.txt: EXIT_RUNTIME=0 + Module.noExitRuntime=true (shell)
keeps wasm alive past main() so the device promise lands; no
Asyncify; export _raf_tick_c so shell.html's RAF can call it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 17:16:14 +10:00
Gorgious56 becbcfdfe7 Add MEP bend preview decorator + join dispatcher
The bend preview gizmo group (commit 2) populated a Scene draft but
the user saw nothing in the viewport until they hit finish — they
had to commit blindly. This commit ports the BendPreviewDecorator
(centerline arc + two leg projections on valid geometry, warning-red
axes on invalid in-segment intersections) and the interactive
GizmoBendPreview group (three dimension widgets for start_length /
end_length / radius plus validate / cancel icons). The bend axis
math lives in a pure compute_bend_preview_polylines helper, fed
into both the gizmo group's per-frame positioning and the GPU
decorator's draw path. MEPSegmentExtendPreviewDecorator lands at
the same time because it shares the decorator install / uninstall
plumbing — renders the extend-to-cursor preview line for the
GizmoPipeSegmentEdition / GizmoDuctSegmentEdition extend icons
when hovered, clamping the projected endpoint to the operator's
minimum so the preview matches where the commit lands. The
MEPJoinSegments dispatcher routes two selected MEP segments to
mep_add_transition (parallel) or enable_bend_preview (non-parallel)
— the F3 search entry point that makes the bend preview testable
before the gizmo-icon dispatch lands.

11 new tests in test_mep_bend_preview.py cover the geometry helper
truth table (parallel rejection, right-angle happy path, near-
collinear rejection, in-segment invalid_axes), the
_intersection_past_near parametrized boundary, registration probes
for the lifecycle operators / join dispatcher / gizmo group /
decorator, and the FinishBendPreview RuntimeError catch contract.
6 extend-preview-line tests (deferred from commit 3) join the
existing 35 in test_mep_segment_edition.py.

Generated with the assistance of an AI coding tool.
2026-06-08 21:18:18 +02:00
Gorgious56 5b79cefee2 Fix #8138: door/window container assignment no-op
Spatial.get_root_element walks aggregate / nest / filled-void /
voided-element chains and core.assign_container assigns the container
to whatever the walk returns. For an IfcDoor the filled-void hop
redirects to the IfcOpeningElement, then voided-element to the host
wall, so a user who selects a door and runs bim.assign_container ends
up targeting the wall — and silently no-ops on the door if the wall is
already in the target storey.

Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a
filling is defined independently of the filling relationship. Major
exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent
ContainedInStructure on doors / windows accordingly. Drop the
filled-void / voided-element hops from the walk; aggregate and nest
remain — those are true sub-part relationships where the parent
legitimately owns the container.

New TestGetRootElement in test/tool pins the new contract (filling
resolves to itself) plus the retained aggregate / nest / loose-element
paths so a future PR that re-adds either hop is caught. Two new
TestAssignContainer cases in test/core pin filling-to-self through the
core layer and per-element can_contain filtering.

Generated with the assistance of an AI coding tool.
2026-06-08 18:53:50 +02:00
Gorgious56 3346a59284 Add MEP pipe / duct segment edit gizmos
Pipe and duct segments had no parametric-edit affordance — the only
length edit path was a property panel value with no live preview.
This commit ports the per-segment parametric edit triad
(enable / finish / cancel) plus a cursor-anchored extend operator
and a cursor-projected split operator into one gizmo group per
segment type. The two PropertyGroups (BIMPipeSegmentProperties,
BIMDuctSegmentProperties) host the draft length plus snap fields
so cancel / no-op-finish restore the segment to its exact pre-edit
visual state including a non-identity pre-edit scale. Length
commits are written through DumbProfileJoiner.set_depth and
auto-dispatch bim.regenerate_distribution_element so adjacent
fittings track the port move. The split operator preserves
downstream port connectivity and runs through tool.Ifc.run for
single-step undo. The two segment types are now first-class
entries in tool.Parametric.EDIT_TYPES, which resolves the FIXME
on auto-commit-on-save dispatch.

35 unit tests cover predicate truth tables, segment_world_length
geometry, preview-via-scale / restore-scale helpers, gizmo class
wiring, lifecycle operator registration, dimension matrix_position
rotation respect, and lifecycle drift-handling. The 6 extend-
preview-line decorator tests stay deferred until the bend preview
decorator commit lands MEPSegmentExtendPreviewDecorator.

Generated with the assistance of an AI coding tool.
2026-06-08 15:01:03 +02:00
Gorgious56 0bf8e9283f Hide parametric gizmos during transform modal
Parametric gizmos (wall/door/window/stair/roof/array/MEP) recompute
matrix_basis every frame from obj.matrix_world. While Blender's
transform modal (G/R/S and the Bonsai macro overrides) drags the
matrix, the gizmos slide off-cursor and fight the transform overlay.

Detect via context.window.modal_operators (Blender 4.2+) — the
collection of running modal operators. Gate poll() (forward-compat)
and draw_prepare() (production path: gizmo.hide=True preserves the
GizmoGroup across the drag instead of destroying it). Cover the
Bonsai macro override for G key (and Shift/Alt/Ctrl+Shift+D) by
matching the BIM_OT_* macro idnames that surface in modal_operators.

Forward-compat test walks every parametric-edit module for GizmoGroup
subclasses and asserts poll returns False with the detector mocked,
so new gizmo groups inherit the hide automatically.

Generated with the assistance of an AI coding tool.
2026-06-08 13:33:08 +02:00
Gorgious56 db7591a867 Add clear_preview_state helper + DRY preview cleanup
Every preview operator (commit + cancel for both bend and wall
fillet) was inlining the same 3-4 line cleanup: set is_active to
False, zero every *_id IntProperty. The new clear_preview_state
helper in preview_base.py introspects bl_rna and applies that
contract generically — adopters become a single call. Two new tests
pin the contract: every *_id IntProperty zeroes, non-id fields stay.

Generated with the assistance of an AI coding tool.
2026-06-08 10:25:59 +02:00
Gorgious56 7b9af9f533 Backport pending-opening-cuts banner from gh8088
Extract the pending_opening_recut tracking, three operators (apply /
dismiss / select), Project-panel banner, and the sibling
multi-instance warning banner (its backend helpers already landed
on this branch) from commit a85ed6032 on gizmos-8088.

All tool.* dependencies (Geometry.reimport_element_representations,
Blender.set_objects_selection, Array.*) and IfcImporter.gross_elements
are already on this branch -- no other diffs from a85ed6032 are
pulled.

The source's narrow except-tuple paraphrase comments are trimmed
to keep only the durable "don't swallow programmer errors" note,
per CLAUDE.md s4a.

Tests: 5 bim-lane tests in test/bim/module/project/
test_pending_opening_cuts.py covering apply happy-path + missing
entity, dismiss, select happy-path + cancellation.

Generated with the assistance of an AI coding tool.
2026-06-08 10:18:37 +02:00
Gorgious56 704a2d36be Add MEP bend preview Scene properties + lifecycle
MEPAddBend exists on the main flow but commits bend geometry with
hardcoded defaults (start_length=0.1, end_length=0.1, radius=0.2)
with no opportunity to tune before commit. The new scene-level
BIMBendPreviewProperties hosts a draft (start_segment_id,
end_segment_id, start_length, end_length, radius); EnableBendPreview
populates it from the two selected MEP segments after asserting they
are non-parallel, FinishBendPreview dispatches MEPAddBend with the
tuned values and clears the draft, CancelBendPreview discards it.
Scene-level placement follows CLAUDE.md 2.9: a bend creates a new
fitting entity between two segments, so neither segment alone owns
the draft. Foundation for the upcoming bend preview gizmo group and
decorator.

Generated with the assistance of an AI coding tool.
2026-06-08 10:17:30 +02:00
Gorgious56 516696cd73 Add partial-state rollback on execute_ifc_operator
When an operator mutated IFC then raised mid-execute the user was left
staring at a raw traceback with the IFC graph captured by the active
transaction but the Blender side stale. Blender does not push an undo
step for a raised operator (the same gap that the CANCELLED-modal arm
patches via bpy.ops.ed.undo_push), so the WARNING the framework can
emit is only honest if it pushes that undo step too. The framework
now detects partial state via ifc_file.transaction.operations,
pushes a Recover undo step, then reports a WARNING naming Ctrl+Z so
the recovery path is discoverable. The bespoke try/except wrapper in
UnjoinWallPathConnection becomes redundant and is retired in the
same change.

Generated with the assistance of an AI coding tool.
2026-06-08 08:45:05 +02:00
Gorgious56 a139adaa2c Apply black formatting to satisfy lint-formatting CI
Three files flagged by black --check on the lint-formatting job:

* bim/module/geometry/operator.py — single-arg `.update(...)` rejoined
  onto one line under the 120-char budget.
* test/bim/module/model/test_wall_gizmos.py — same join on a
  _make_path_rel call.
* test/modal/test_modal.py — pre-existing baseline noise picked up
  via the upstream merge: PEP-8 blank-line separators between top-
  level functions, `0.68+` → `0.68 +`, double quotes, trailing
  whitespace stripped.

No behavioural change; pure whitespace.

Generated with the assistance of an AI coding tool.
2026-06-07 02:05:53 +02:00
Gorgious56 a4a806147d Merge remote-tracking branch 'ifcopenshell/v0.8.0' into bonsai/parametric-framework-features-pt2 2026-06-06 21:47:24 +02:00
Gorgious56 4962e3256d Promote idle-row icons into the slot system
The toggle_openings icon lived outside the IconSlot layout — each
host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon
+ update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition
queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it.
On an arrayed wall the dict was shadowed: find_for_element returns
"array" before "wall" in EDIT_TYPES order, the wall reservation was
never consulted, and the first per-layer ARRAY icon (local X=0.37)
landed 13cm from the wall's toggle_openings (X=0.50) — visually on
top of each other.

Promote idle-row icons into the slot system instead of patching the
dict:

* IconSlot gains an Optional visible_when predicate for state-driven
  visibility (toggle_openings only when the host carries openings).
* BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]]
  + _idle_slot_x_positions() + _idle_row_right_edge() helpers; the
  setup + idle-branch positioning loops mirror the existing
  feature_slots path.
* Wall and roof declare toggle_openings as an idle_slot and drop
  their ad-hoc setup/update calls.
* GizmoArrayEdition's _resolve_feature_idle_max_x walks
  BaseParametricGizmoGroup.REGISTRY and takes the max
  _idle_row_right_edge() across peers whose poll passes — no more
  hardcoded dict, no more find_for_element-order shadowing.
* setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon
  helpers deleted from drawing/gizmos.py.
* 3 forward-compat AST guards pin the new contract.

Also bundles an unrelated array-test fix: TestUsingArrays in
test/tool/test_model.py was asserting against bpy.context.selected_objects
which is a fragile signal after remove_array / apply_array. A new
_array_objects() helper filters bpy.data.objects via the BIM_Array
pset's IfcActuator type instead.

Layout on an arrayed wall after the fix:
  pen        X = 0.00
  toggle     X = 0.50 (idle_slot 0)
  array[0]   X = 0.87 (one ICON_ARRAY_GAP past idle row)
  array[1]   X = 1.27
All separated by the standard inter-icon spacing.

Generated with the assistance of an AI coding tool.
2026-06-06 18:22:27 +02:00
Dion Moult bb0e96b406 ifcviewer-web: fix three web-only init aborts
1) Async-wait spin loops in initWgpu / probeAndCreatePool / pick /
   screenshot finalize all called wgpuInstanceProcessEvents in a tight
   while. wgpu-native drives queued callbacks from there; Dawn-web
   queues the callback for an event-loop tick that never happens
   because wasm doesn't yield back to JS. Page hung at the first
   await (RequestAdapter) and Firefox flagged the tab as slow.

   New `waitTickInstance` helper calls emscripten_sleep(0) on
   Emscripten (Asyncify unwinds wasm, JS resolves WebGPU promises,
   resume) before wgpuInstanceProcessEvents drains completions into
   our callback. Desktop path is unchanged. All five
   `while (!done) ProcessEvents` sites switch to the helper.

2) streaming_thread_.start() inside initWgpu spawns a std::thread.
   On Emscripten that needs -pthread + COOP/COEP headers from the
   hosting page. None of that is wired yet, so the start is gated
   `#if !defined(__EMSCRIPTEN__)`. The sync chunk-load fallback
   already inside driveStreamingLoads carries the load until #88
   replaces it with emscripten_fetch.

3) wgpuSurfacePresent at the end of render() aborted with
   "wgpuSurfacePresent is unsupported (use requestAnimationFrame via
   html5.h instead)" — Dawn-web composites the canvas at the end of
   the RAF tick automatically. Call skipped on Emscripten;
   WebViewportHost::requestFrame is the RAF-tick driver.

Page now loads, brings up wgpu, configures the surface, and renders
one frame with the configured background. The frame log in the
status overlay shows the first-frame startup spike (~1000 fps from
a 1 ms tick); subsequent frames are paint-on-demand, which on the
empty scene means no frames at all — consistent with the desktop
event-driven model.
2026-06-06 21:28:27 +10:00
Dion Moult 1fc15e78f2 ifcviewer-web: wire WebViewportHost + ViewportCore (#87)
Replaces the standalone wgpu-only clear-color spike in main_web.cpp
with a real WebViewportHost implementation: surface creation via the
emdawnwebgpu canvas-selector source, framebufferSize through
emscripten_get_element_css_size + dpr, requestFrame as a deferred
flag the RAF main_loop consumes, quit through emscripten_force_exit.

main_web.cpp now does the same lifecycle the desktop initWgpu shell
does: core_.initWgpu(web_limits=true) → buildPipelines → buildHiz/
Edge/Pick. The render loop runs core_.render() once per RAF tick when
the host has flagged a frame pending, with a surface reconfigure on
size changes.

Builds clean under emcc 6.0 + emdawnwebgpu (1.4 MB wasm, 278 KB JS
glue). Renders an empty scene with the configured background — the
plumbing is end-to-end through the same ViewportCore code path the
desktop build uses. No sidecar load yet: that lands with the
emscripten_fetch streaming backend (#88).
2026-06-06 21:09:35 +10:00
Dion Moult 50014f4842 ifcviewer: move section-plane mutators into ViewportCore (#84-y)
addSectionPlaneAtSurface (camera-facing auto-flip + kMaxSectionPlanes
cap check), removeSectionPlane, and clearSectionPlanes all move to
ViewportCore. ViewportWindow keeps tiny forwarders so the section
tool's input handlers (still VW) call through without seeing the move.

Each method now calls host_->requestFrame() in place of the
isExposed() + requestUpdate() gate, which means the section tool path
becomes the next piece that could exercise the WebViewportHost: a
click-to-add over WebGPU will work as soon as the host is wired,
without further core-side changes.
2026-06-06 20:56:36 +10:00
Dion Moult ebb9c91bdf ifcviewer: move render() body into ViewportCore (#84-x)
The frame loop — surface acquisition, parallel cull dispatch, streaming
drive, two-pass main render, HiZ resolve, edge pass, screenshot
capture, FrameStats emission, interactive / bench heartbeat, bench
summary + auto-quit — all live in ViewportCore now.
ViewportWindow::render() shrinks to the Qt-only prelude:
isExposed() guard, fpsIntegrate() (fly-mode WASD step), then
core_.render().

The overlay renderer stays Qt-bound (OverlayRenderer.h carries
QString labels). Core reaches it via two new ViewportHost virtuals:
encodeOverlaysInMainPass (section gizmos, highlights, pivot, lines,
points — in-MSAA-pass) and encodeOverlaysPostMain (corner axis,
marquee, labels — on the resolved surface). The QtViewportHost
implementation in ViewportWindow forwards each to overlays_.X().

FrameStats moves to its own Qt-free header (FrameStats.h) with
ViewportWindow::FrameStats re-exported as a using-alias so the
bonsai-side signal binding keeps working. ViewportHost::onFrameStats
replaces the placeholder 4-double signature with the typed POD.
OverlayFrame moves alongside (OverlayFrame.h) so the host overlay
callbacks can carry it without dragging Qt into core.

Bench + frame-stats + cull-tuning state (min_pixel_radius_,
motion_min_pixel_radius_, lod1_pixel_threshold_, cull_threads_enabled_,
prev_camera_*, has_prev_camera_, last_cull_was_motion_, last_visible_*,
last_cull_*ms_, last_stream_ms_, bench_*, interactive_frame_count_,
frame_time_ms_window_/_sum_/_count_/_head_) move to ViewportCore; VW
keeps reference aliases so the env-var prelude, setBenchmarkFrames,
and the various tool keybind setters keep compiling unchanged.

Smoke checks: --screenshot path renders + saves a clean PNG;
--benchmark 10 runs the warm gate, prints the per-frame log + summary,
and exits cleanly via host_->quit().
2026-06-06 20:27:33 +10:00
Dion Moult 8fb0a4b24f ifcviewer: extract screenshot capture encode + readback into ViewportCore (#84-w)
The inline screenshot path inside render() — surface-to-buffer copy,
async map, BGRA->RGBA swap into a tightly-packed RGBA8 image — moves
into core_.encodeScreenshotCapture / finalizeScreenshotCapture. render()
calls the two new helpers around its existing queueSubmit.

The PNG write itself stays Qt-bound, but it's now reached through a
new ViewportHost::saveScreenshotRgba8 virtual. The QtViewportHost
override (ViewportWindow::saveScreenshotRgba8) constructs a QImage
around the host-buffer and calls QImage::save("PNG") with the same
log lines as before; a future WebViewportHost will route the bytes
through stb_image_write or a download-URL emit instead. Either way,
the wgpu-side capture path never touches Qt again.

quit-after-screenshot now goes through host_->quit() too, so the
--screenshot CLI exit no longer reaches QCoreApplication::quit()
from render() directly.
2026-06-06 19:48:51 +10:00
Dion Moult b9d77b2e3d ifcviewer: move buildModelBindGroup + captureNextFrameToPng setter into ViewportCore (#84-v)
Two small helpers + the screenshot-quit flag flip across; the render-
side capture encode + readback + PNG save itself stays in VW (those
need a stbi-style PNG writer to replace QImage::save before they can
move, and that's its own commit).

VW keeps tiny forwarders so bonsai's SceneLoader + the CLI screenshot
path don't see the move. The streaming sync-fallback gate already
reads the core-side pending_screenshot_path_, so capture timing is
unchanged.
2026-06-06 19:01:14 +10:00
Dion Moult 78ce993a11 ifcviewer: move configureSurface into ViewportCore (#84-u)
The swapchain configuration path — WGPU_PRESENT_MODE handling,
mode-preference order (Mailbox / Immediate / FifoRelaxed / Fifo),
caps probe, cfg.alphaMode wiring, and the on-resize depth + MSAA +
HiZ texture reallocation + bind-group invalidation — all move to
ViewportCore. The only VW-side concern was a QString::fromLatin1 in
the advertised-modes log, which becomes a std::string concat with the
same output.

ViewportWindow's render() / resizeEvent / surface-outdated retry paths
call core_.configureSurface() now. createSurface() (which returns the
platform-specific WGPUSurface from the host) stays VW because Qt
platform discovery has to happen on the Qt side.
2026-06-06 18:52:49 +10:00
Dion Moult 346e6db217 ifcviewer: move pick + raycast subsystem into ViewportCore (#84-t)
The whole pick pipeline (R32UInt + RGBA16F MRT, depth attachment,
ping-pong staging, single-pixel + rect readback) plus the public
pickObjectAt / pickSurfaceAt / picksInRect / pickMeshLocalAt / raycast
API and the rayAabbSlab / rayTriMT / rayAABBHit helpers all move to
ViewportCore. ViewportWindow keeps tiny forwarder methods so the
bonsai input + tool callers (mouseRelease, marquee, section tool,
Length/Area refinement) stay compiling.

MeshLocalPick + RaycastHit follow as nested types on ViewportCore;
ViewportWindow re-exports them as using-aliases to preserve the
ViewportWindow::MeshLocalPick / ViewportWindow::RaycastHit names
existing callers (and a couple of bonsai tests) reach for.

State migrated: pick_color_texture_/_view_, pick_normal_texture_/_view_,
pick_depth_texture_/_view_, pick_staging_buffer_, pick_normal_staging_buffer_,
pick_w_/_h_, box_pick_staging_buffer_/_capacity_. The pick_pipeline_
itself was already aliased.

The pick path no longer reaches into VW for any GPU state, so the
render() / shutdown() callers become core_.X() forwards and the pick
infrastructure can be exercised by the future web build without going
through Qt.
2026-06-06 18:48:09 +10:00
Dion Moult fa3b0f90d8 ifcviewer: move edge silhouette subsystem into ViewportCore (#84-s)
buildEdgePipeline, encodeEdgePass, releaseEdgeResources + the EDGE_WGSL
shader source all move to ViewportCore. The edge_bind_group_ + the
edges_enabled_ flag come along too (the latter aliased on VW so the
edge-toggle keybind keeps compiling).

The pass binds the now-core-side depth_view_ directly, so there's no
remaining cross-side state dependency for edge rendering. render()
still calls core_.encodeEdgePass(enc, surface_view) — once render()
itself moves, the call collapses to a sibling method invocation.
2026-06-06 18:26:57 +10:00
Dion Moult 138973830b ifcviewer: move HiZ subsystem + depth/MSAA attachments into ViewportCore (#84-r)
The whole HiZ occlusion-cull pipeline (resolve pass, ping-pong async
readback, CPU mip pyramid, per-instance AABB lookup, WGPU_HIZ_TRACE
diagnostic) moves to ViewportCore. The main render-pass depth
attachment and MSAA color attachment come along too — they're shared
between render() (still VW) and the HiZ resolve pass (now core).

Methods migrated: buildHizPipeline, ensureHizTextures,
releaseHizResources, encodeHizResolve, startHizMap, drainHizReadbacks,
aabbOccludedByHiz, ensureDepthTexture, releaseDepthTexture,
ensureMsaaColorTexture, releaseMsaaColorTexture. HIZ_WGSL moves with
them into ViewportCore.cpp's anon namespace.

State migrated: hiz_enabled_, hiz_valid_, hiz_vp_, hiz_pyramid_,
hiz_mip_offset_/_w_/_h_, hiz_reject_count_, hiz_trace_budget_,
hiz_uniform_buffer_, hiz_bind_group_, hiz_resolve_texture_/_view_/_w_/_h_,
hiz_padded_bpr_, hiz_staging_buffers_[2], hiz_slot_vp_[2],
hiz_slot_state_[2], hiz_write_idx_, depth_texture_/_view_/_w_/_h_,
msaa_color_texture_/_view_/_w_/_h_, plus the HizSlotState enum +
HIZ_SLOTS + HIZ_BASE_W constants. ViewportWindow keeps reference
aliases on every field VW.cpp still touches so the render path
compiles unchanged.

The HizOccludedFn shim in render() now wraps core_.aabbOccludedByHiz
directly. Once the render path itself moves into core, that shim
disappears and cull can call aabbOccludedByHiz as a sibling method.
2026-06-06 18:20:15 +10:00
Dion Moult 4782f54e3b ifcviewer: move sidecar / direct-load helpers into ViewportCore (#84-q)
applyCachedModel, uploadMeshChunk, uploadInstanceChunk, finalizeModel
all live in ViewportCore now. The bonsai-facing public entry points on
ViewportWindow are one-line forwarders that keep
SceneLoader → ViewportWindow* binding intact.

State + helpers that came along:
- pending_direct_loads_ (the SidecarData staging map keyed by model_id)
- initial_view_applied_ (auto-viewAll suppression; aliased on VW so
  setCamera can still flip it)
- getOrCreateDirectStaging + createBufferWithData (anon namespace
  helpers on the core side)

The Qt-bound isExposed() / requestUpdate() pair on the
applyCachedModel tail becomes host_->requestFrame() — the
QtViewportHost forwards to requestUpdate(); a WebViewportHost will
forward to requestAnimationFrame.

The sidecar load path is now fully core-side. ViewportWindow no
longer owns any of the model-creation machinery; everything from
"here's a parsed sidecar" to "fully-built models_gpu_ entry with
empty pool slices waiting on streaming" runs through ViewportCore.
2026-06-06 17:57:31 +10:00
Dion Moult d92121a62d ifcviewer: move cullModelCpuCompute + cullModelCpuUpload into ViewportCore (#84-p)
CPU cull (frustum + contribution + LOD + opaque/transparent partition)
and its companion GPU-upload step now live in ViewportCore. The HiZ
occlusion test stays in VW — the pyramid + async readback machinery
hasn't migrated yet — and is plumbed through a
ViewportCore::HizOccludedFn callback the render path binds when HiZ
is enabled-and-fresh. Null callback means "no occlusion test", which
keeps the cull path host-agnostic.

extractFrustumPlanes + aabbInFrustum moved up into CameraMath.h so
both VW's render() (where the planes are extracted) and core's cull
(where they're tested) can share without one #including the other.

LOD-debug counters (lod1_dbg_count_, lod0_dbg_eligible_count_,
lod0_dbg_no_lod1_count_, lod1_dbg_tris_saved_) moved to core too —
they're written by cull and read/reset by VW's still-here per-frame
[frame] heartbeat through reference aliases.
2026-06-06 17:21:29 +10:00
Dion Moult d86a5af662 ifcviewer: move driveStreamingLoads into ViewportCore (#84-o)
The per-frame streaming residency driver — LRU/priority eviction,
worker-result drain, candidate selection, click-and-track diagnostic,
sync-fallback for screenshot capture — now lives in ViewportCore.
ViewportWindow::driveStreamingLoads is a one-line forwarder.

Streaming-related state moves to core with reference aliases on VW:
streaming_{loads,more_pending,candidates,evictions_{lru,pri},drained,
blocked_oom}_this_frame_, streaming_debug_, tracked_{object_id,
chunk_mid,chunk_idx,was_resident}_, and pending_screenshot_path_. The
pick handler and bench-warm gate (still VW) read/write through the
aliases unchanged.

Qt-isms in the body were replaced en route:
- QFileInfo(...).completeBaseName() → std::filesystem::path::stem()
- requestUpdate() → host_->requestFrame()
- QString::number(x, 'f', N) in numeric logs → raw double / int (we lose
  fixed-precision in a couple of diag lines; acceptable tradeoff).

host_->requestFrame() means the streaming loop is now host-agnostic:
the WebViewportHost will provide its own requestAnimationFrame
equivalent when it lands.
2026-06-06 17:12:15 +10:00
Dion Moult 077080b318 ifcviewer: move chunk residency helpers (buildChunkBindGroup + applyStreamedChunk + loadChunkBytesAndUploadGpu + unloadChunk + makeChunkRequest) into ViewportCore (#84-n)
The chunk-state machine that mediates between the streaming pool and the
per-chunk WGPU bind groups now lives in ViewportCore. ViewportWindow's
remaining streaming code (driveStreamingLoads, finalizeModel) calls
through to core_.applyStreamedChunk / core_.unloadChunk /
core_.loadChunkBytesAndUploadGpu, and the chunk request builder is a
static helper on ViewportCore so VW's still-here driveStreamingLoads can
enqueue requests against streaming_thread_ without reimplementing it.

streaming_frame_idx_ moved to core (alongside the residency clock),
aliased on VW so the inline streaming logic stays compiling. The
mesh-volume side effect inside applyStreamedChunk now fires a
std::function<void()> callback (core_.on_volume_dirty_) instead of
reaching into ViewportWindow::updateVolumeReadout — VW wires the
callback in its ctor, non-Qt hosts leave it null and pay nothing.

computeMeshLocalVolumeQuantised moved to ViewportCore.cpp's anonymous
namespace; it was only called by applyStreamedChunk.
2026-06-06 16:33:31 +10:00
Bruno Perdigão 06d99feeea Add no headless test for Bonsai Snap Target. 2026-06-05 18:29:19 -03:00
Gorgious56 f584a50fbb Clear wall-edit gizmos off click targets in plan view
In plan view world-Z collapses to zero on screen, so every wall-edit
icon anchored on the floor — the projected 3D cursor, wall endpoints,
wall-to-wall corners, IfcRelConnectsPathElements connection points —
projects onto the click target it represents. The result on a typical
extend / split / unjoin action: the icon sits on top of the cursor
crosshair (or the corner the user wants to click), defeating precise
positioning.

Add shared ``gizmo.top_down_clearance(context, billboard_rot)`` to
bim/module/drawing/gizmos.py: returns a screen-up Vector in top-down
view (cosine cone around world Z, matching ``is_view_top_down``) and a
zero Vector elsewhere, so call sites apply it unconditionally before
``billboarded_at``. Default distance 0.4 m aligns with the inter-icon
stack spacing already used by GizmoWallJoinIntersection so single
icons and stack bases land at consistent screen-up positions when
multiple groups render around the same wall endpoint.

Apply at the seven wall-edit anchor sites:

* GizmoWallEdition cursor stack (top-down branch only — non-top-down
  already stacks along world-Z at structural points clear of the
  cursor).
* GizmoWallExtendVertically (single icon at wall origin endpoint,
  active-object Z elevation).
* GizmoWallJoinIntersection corner stack base + merge midpoint.
* GizmoWallUnjoinSingle link-toggle pool (one icon per IFC path
  connection, previously sitting exactly on the connection point).
* GizmoWallFilletReedit pen icon at fillet corner.
* GizmoWallFilletToggleOpenings.

The clearance is a pure visual offset — bound operators still read
the world-space anchor (cursor / endpoint / connection point) at
execute time, so the action's target is unaffected.

Also tighten GizmoWallUnjoinSingle: gate poll on ``props.is_editing``
so the link-toggle icons only surface during the wall edit lifecycle
(matching every other edit-row icon), and downsize them via a new
``ICON_SCALE = 0.35`` constant since 16 of them at default scale
cluttered the viewport on path-heavy walls.

ruff + black clean. Wall gizmos test lane 14/14 pass.

Generated with the assistance of an AI coding tool.
2026-06-05 16:02:47 +02:00
Bruno Postle 24a241addc Use version preprocessor guards for RocksDB unique_ptr API, retain unique_ptr internally 2026-06-05 14:22:07 +02:00
Bruno Postle 365be8fb52 Support RocksDB shared library and new unique_ptr DB::Open API
Some distributions (e.g. Fedora) ship only a shared RocksDB that exports
RocksDB::rocksdb-shared rather than RocksDB::rocksdb. The CMake target
selection now falls back to the shared target when the static one is absent.

Newer RocksDB also changed DB::Open and DB::OpenForReadOnly to take
std::unique_ptr<DB>* instead of DB**. IfcFile.cpp uses SFINAE tag dispatch
to build against both old and new APIs without version detection.
2026-06-05 14:22:07 +02:00
Bruno Postle eacff93945 Use std::lexicographical_compare in Point_d_4d_Less 2026-06-05 13:56:04 +02:00
Bruno Postle 9d956f18b7 Fix CGAL 6.x build: add Point_d_4d_Less comparator for std::map
CGAL 6.x deleted operator< from Point_d, so std::map<Point_d, ...>
no longer compiles. Adds a custom lexicographic comparator and updates
the three affected maps in snap_halfspaces and snap_halfspaces_2.
2026-06-05 13:56:04 +02:00
Gorgious56 8faf9ff43d Consolidate load_post parametric drains
bim/handler.py was importing two feature-module internals
(wall_offset_gizmos.clear_caches, preview_base.discard_pending_previews)
to drain load-transient parametric state alongside the existing
tool.Parametric.heal_stale_edit_flags() call inside
_apply_save_file_invariants. Each new parametric drain added one
top-level import and one inline call — every load_post drain leaked
into handler.py's namespace.

Hide all three drains behind tool.Parametric.on_load_post(scene),
sited adjacent to heal_stale_edit_flags. The two feature-module
imports become late imports inside on_load_post — same pattern as
refresh_post_commit's existing `import bonsai.bim.handler` — which
sidesteps the tool.parametric -> bim.module.model.preview_base ->
bonsai.tool registration-time cycle.

The forward-compat AST contract that pinned "every module-scope
GenerationKeyedCache + clear_caches MUST be drained on load_post"
follows the call site to its new home — the test now walks
tool.Parametric.on_load_post instead of _apply_save_file_invariants.

No behaviour change. 45/45 affected bim tests pass
(test_handler_forward_compat, test_preview_base,
test_wall_offset_gizmos, test_parametric_registry).
ruff + black clean on all touched files.

Generated with the assistance of an AI coding tool.
2026-06-05 13:20:49 +02:00
Dion Moult ea24851a51 ifcviewer: move section_planes_ + xray_alpha_cap_ + updateFrameUniforms into ViewportCore (#84-m)
Per-frame uniform packing now lives in ViewportCore::updateFrameUniforms,
which reads the camera (via the already-migrated buildViewProj), the
section_planes_ vector, and the xray_alpha_cap_ scalar — all of which
have moved into ViewportCore alongside frame_uniform_buffer_.
ViewportWindow keeps reference-aliases on section_planes_ and
xray_alpha_cap_ so the section-tool and X-ray toggle (still Qt-input-
bound, still living in VW) keep compiling unchanged. The render-path
caller in VW::render now does core_.updateFrameUniforms().

Extracted SectionPlane into its own Qt-free header (SectionPlane.h)
so ViewportCore doesn't have to include OverlayRenderer.h's QString /
QHash. OverlayRenderer.h re-exports it.
2026-06-05 20:56:24 +10:00
Gorgious56 87bca20df7 Relocate feature decorators to their owning modules
Three feature-specific decorators previously lived in
bim/module/model/decorator.py despite owning state only their
home module reads:

* ArrayPreviewDecorator + ArraySelectionHighlightDecorator +
  draw_array_layer_children_bbox -> array.py (read array
  edit-state props and walk BBIM_Array psets)
* WallGizmoPreviewDecorator + draw_wall_partner_bbox -> wall.py
  (dereference wall.py-private classes and helpers via lazy
  imports)

decorator.py keeps cross-cutting infrastructure
(BoundingBoxDecorator, SlabDirectionDecorator, WallAxisDecorator,
WallFilletPreviewDecorator, PolylineDecorator, ProductDecorator)
and the shared bbox primitives (bbox_world_edges,
draw_polyline_segments, _BBOX_EDGES, _stroke_lines_alpha,
_fill_quads_alpha) that several feature files now import.

handler.py and gizmos.py update their import paths; the
wall-feature lazy imports inside WallGizmoPreviewDecorator
methods collapse to direct references now that the decorator
lives in wall.py.

No behaviour change. Wall lane 37/37, array lane 15/15, wall
forward-compat 6/6, parametric-registry 8/8 still pass.

Generated with the assistance of an AI coding tool.
2026-06-05 12:45:51 +02:00
Gorgious56 a30546f1f2 Bbox dimensions key, DRY array operators, drop dead code
Three concerns sharing the same architectural theme (collapse inline
bbox / edit-state lookups, drop overrides that re-do base-class work):

== Bbox helpers and array operator DRY ==

* tool/blender.py: add a "dimensions" tuple key to both
  get_object_bounding_box and get_object_world_bounding_box return
  dicts. The (max - min) per-axis extent — which callers previously
  computed via local helpers — is now a key alongside min_x / max_x
  / min_point / max_point / center. Distinct from Blender's built-in
  obj.dimensions (which folds object-level scale): the local variant
  is the intrinsic mesh bbox extent; the world variant is the
  matrix_world-applied AABB.

* bim/module/model/array.py: drop the local _bbox_dims helper; the
  two callers now read tool.Blender.get_object_bounding_box["dimensions"]
  directly.

* Rename _parent_geometry_changed -> _array_children_need_rebuild.
  The old name suggested "did the parent change just now", implying
  the function was a parent-edit-finish trigger. It actually runs
  only inside the array-edit-finish path as a drift safety net (the
  upstream-deliberate design — see commit 83d97d7e9 "Fix #7616. Make
  regenerate array an operator instead of an array preference" —
  means the array doesn't auto-regen when its parent geometry edits
  finish). New name matches the call-site phrasing
  ``if X: _wipe_array_children(layers)`` and clarifies that this is
  a children-state check, not a parent-edit trigger.

* Extract _resolve_array_edit_props(context) — returns the active
  object's array props during an active edit lifecycle, or None.
  Collapses the obj-active-then-is-editing prologue (3 lines + return)
  to one resolver call across 4 sites: ToggleArrayMethod.execute,
  AdjustArrayCount.execute, RemoveArrayLayerFromEdit._execute and
  .poll. Each call site shrinks from 7 lines to 3.

* Migrate two inline bbox reads inside GizmoArrayEdition to the new
  dict keys: get_axis_world_face_center collapses the manual
  xs/ys/zs min/max + center math to bbox["center"] + bbox["max_x"] /
  ["max_y"] / ["max_z"]; get_element_height collapses
  ``max(corner[2] for corner in obj.bound_box)`` to
  tool.Blender.get_object_bounding_box(obj)["max_z"].

The _BBOX_EQUALITY_EPS = 1e-5 tolerance stays inline as a single-
consumer constant — no other call site needs tolerance-equality on
dimension tuples, so extracting it to a shared util would be
speculative abstraction.

== Drop dead code ==

* GizmoArrayEdition.update_editing_gizmos override + its
  _has_other_parametric_type helper: redundant with
  hide_pen_button = True at line 1024. The base class already hides
  the pen in every idle case (when hide_pen_button is truthy) AND in
  every editing case (unconditionally). The override's conditional
  hide-when-parametric only re-hid a pen that was already hidden in
  both branches. Removes the only remaining path that could re-show
  the array's pen icon; array-edit entry is now uniformly via the
  per-layer ARRAY icons (which is the documented preferred
  affordance, see the hide_pen_button comment).

* _wall_fillet_preview_active in wall.py: defined but never called.
  _wall_fillet_props (the sibling thin-wrapper around
  preview_base.get_preview_props) is heavily used; the
  is_preview_active wrapper was added speculatively and never picked
  up a consumer.

Generated with the assistance of an AI coding tool.
2026-06-05 12:03:26 +02:00
Gorgious56 fbe6fe5384 Fix wall edit lifecycle + drain wall_offset_gizmos cache on load
Bundled bug fixes + the forward-compat AST guard that prevents the
underlying class of bug from coming back.

* bim/module/model/wall.py: FinishEditingWall._execute early-returns
  CANCELLED when props.is_editing is False. Without this guard, a
  failed enable (e.g. on a wall without IfcMaterialLayerSetUsage)
  leaves is_editing False but a press on finish still walked the
  sub-ops below, which dereferenced layer-set-dependent state and
  crashed.

* tool/model.py: Model.offset_wall now guards against
  ifcopenshell.util.element.get_material returning None before
  calling .is_a("IfcMaterialLayerSetUsage"). Fixes the pre-existing
  test/bim/module/model/test_wall_header_refresh.py crash that has
  been the only failing test in the wall lane since this branch
  started.

* bim/handler.py: _apply_save_file_invariants drains
  wall_offset_gizmos.clear_caches() on load_post. The module-scope
  GenerationKeyedCache instance survives the .blend reload; without
  the drain the cache may serve entries whose bpy_struct references
  point into the freed bpy.data of the previous file.

* test/bim/test_handler_forward_compat.py: AST-walk test that
  enumerates every bim/module/model/*.py source declaring both a
  module-scope GenerationKeyedCache assignment AND a top-level
  clear_caches function, and asserts each module appears as a
  <module>.clear_caches() call in _apply_save_file_invariants. Pins
  the contract: any future module-scope geom cache that exposes
  clear_caches must wire into the load_post drain.

* test/bim/feature/model.feature + test/bim/test_feature.py: wall
  edit-lifecycle scenarios switch from "add cube + assign as
  IfcWallType" to "load the demo construction library + add an
  occurrence of the WAL100 wall type", so the parametric edit runs
  against a real LAYER2 wall with IfcMaterialLayerSetUsage rather
  than a vanilla-mesh promotion that lacks one. The demo-library
  step also picks the schema-matching library file (IFC2X3 /
  IFC4 / IFC4X3) so the appended types remain valid across schemas.
  Door saved-height assertion updates from 2.5 → 2500 to reflect
  that BBIM_Door pset stores project units (METRIC_MM in the
  empty-project fixture).

Generated with the assistance of an AI coding tool.
2026-06-05 10:27:01 +02:00
Dion Moult 2b43e6f7e0 ifcviewer: move initWgpu + probeAndCreatePool + shutdown into ViewportCore (#84-l)
The instance/adapter/device/queue/pool/surface-format wgpu lifecycle now
lives in ViewportCore — including the OOM-scoped pool size probe and
the worker-thread startup. ViewportWindow::initWgpu becomes a Qt shell
that handles env-var tuning + nav-button preset wiring, then delegates
to core_.initWgpu(); the VW-only pipeline builders (HiZ, edge, overlays,
pick) still run after. ViewportWindow::shutdown drops the VW-only
resources (depth, msaa, hiz, edge, overlays, pick) and lets
core_.shutdown() release the shared wgpu handles it now owns.

The wgpu-native log callback (wgpuSetLogCallback / WGPULogLevel) is
gated on !__EMSCRIPTEN__: it's not part of the W3C spec header, and
the emdawnwebgpu port doesn't ship wgpu.h — validation errors there
land in the browser console regardless.

Drive-by: update test_federation to compare HomeView::target as
Eigen::Vector3f (left stale by #79 when QVector3D was retired).
2026-06-05 18:06:25 +10:00
Bruno Postle bd264f1d85 Add missing standard library includes for self-sufficient headers
Fixes builds with newer GCC/libstdc++ that no longer provide <cstdint>,
<cstring>, <cfloat>, <memory>, <algorithm> etc. transitively. Also
disambiguates visit<> calls in taxonomy.h with the full namespace and
casts the character value in IfcCharacterDecoder to uint32_t to silence
ambiguous overload warnings.
2026-06-05 08:54:27 +02:00
Dion Moult 66a21923b8 ifcviewer: move buildPipelines + selection-flags wiring into ViewportCore (#84-k)
Move the main render pipeline construction + the selection flags
buffer/bind group lifecycle. Both buildPipelines and the selection
flags methods produce/consume state ViewportCore already owns
(main_pipeline_, frame_bgl_, etc.) plus a handful of "frame
infrastructure" fields this commit also brings across.

State moved (7 fields):
  WGPUBuffer        frame_uniform_buffer_
  WGPUBindGroup     frame_bind_group_
  WGPUBuffer        selection_flags_buffer_
  uint32_t          selection_flags_capacity_
  std::vector<u32>  selection_flags_scratch_
  SelectionState    selection_
  VisibilityState   visibility_

Methods moved:
  buildPipelines              (~150 lines + 320-line MAIN_WGSL string)
  ensureSelectionFlagsBuffer  (~60 lines)
  uploadSelectionFlagsIfDirty (~10 lines)

Plus the MAIN_WGSL constant + the svFromCStr helper into
ViewportCore.cpp's anonymous namespace. ViewportWindow.cpp keeps its
own svFromCStr copy (still used by 50+ label fields in the not-yet-
moved pipeline builders + render encoders).

Shared constants extracted to ViewportCore.h:
  kMaxSectionPlanes (was OverlayRenderer::kMaxSectionPlanes — assert
                     in VW.cpp keeps them in sync)
  kViewportSampleCount (was SAMPLE_COUNT in VW; VW keeps a static
                        constexpr alias for the existing callsites)
  struct FrameUniforms (canonical layout for the per-frame UBO,
                        consumed by both core's buildPipelines and
                        VW's still-in-flight updateFrameUniforms)

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 16:39:01 +10:00
Dion Moult 8cf7d4346d ifcviewer: move volume readout helpers into ViewportCore (#84-j)
Tiny followup to #84-i — move the const-lookup volume helpers used by
bonsai's measurement HUD:

  double volumeOfObjects(const std::vector<uint32_t>&) const
  vector<pair<uint32_t, double>> volumesPerObject(
                              const std::vector<uint32_t>&) const

The det3OfPlacement static helper moves with them into ViewportCore.cpp's
anonymous namespace (the original kept its mirror in
ViewportWindow.cpp; ViewportWindow's own internal callers are gone now
since these methods moved).

Pure read of models_gpu_ + mesh_local_volumes — all in core already.
Trivial transplant.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 15:37:47 +10:00
Dion Moult 707bb8f5d4 ifcviewer: move camera mutators + AABB helpers into ViewportCore (#84-i)
Move the cluster of camera-state mutators + per-object AABB helpers
now that the camera fields all live in ViewportCore. CameraState
struct is canonical in core; ViewportWindow keeps a `using` alias
so bonsai's HomeView round-trip (Commands.cpp setHome / restoreHome)
compiles unchanged.

Moved:
  void viewAll()
  void setCamera(...) — pitch + distance clamping included
  void setStandardView(yaw, pitch) — bypasses clamp for ±90°
  void toggleProjection()
  std::string cameraString() const
  CameraState cameraState() const
  void frameAabb(mn, mx, padding)
  bool computeObjectAabb(id, float[3], float[3]) const
  bool computeObjectAabb(id, Eigen::Vector3f&, Eigen::Vector3f&) const

ViewportWindow keeps thin forwarders for the public ones (bonsai
calls them). setCamera additionally flips initial_view_applied_
on the VW side — the auto-viewAll suppression flag isn't in core
yet because the trigger for auto-viewAll lives in the still-in-VW
applyCachedModel path.

The isExposed()+requestUpdate() Qt pattern inside the moved bodies
becomes host_->requestFrame(); two viewAll/toggleProjection diagnostic
prints become fprintf since Log::info() doesn't reach into core.cpp
through the Qt logging surface.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 15:27:39 +10:00
Dion Moult 14e7c9fc42 ifcviewer: move camera math into ViewportCore (#84-h)
Move the three camera-math methods that compute view/projection
matrices, scene bounds, and per-chunk screen footprint for the
streaming priority signal:

  void  buildViewProj(Eigen::Matrix4f&, Eigen::Matrix4f&) const
  bool  computeSceneAabb(float[3], float[3]) const
  float chunkScreenAreaPx(const ModelGpuData::Chunk&,
                          const Eigen::Matrix4f&) const

Plus the orbitEye helper (anonymous namespace in ViewportCore.cpp;
the qDegreesToRadians dep got swapped for an inline M_PI/180 constant).

ViewportWindow.cpp's 9 internal callers (cull, streaming, pick,
render, debug) updated to use core_.buildViewProj() etc. The
buildViewProj forwarder stays out of ViewportWindow.h since no
external caller needs it — bonsai/minimal both go through
public API methods like viewAll which still wrap core_ access
on the VW side.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 14:43:25 +10:00
Dion Moult a0db182d2b ifcviewer: move camera + surface-geom state into ViewportCore (#84-g)
Move the camera/projection/clear-color fields that buildViewProj,
updateFrameUniforms, the cull screen-area projector, and the bonsai-
side cameraState/setCamera/viewAll surface depend on. Same alias
pattern; no method bodies move in this commit — the next one moves
the camera math methods now that all their state is in core.

State moved (12 fields):
  int   configured_w_, configured_h_
  float camera_target_[3], camera_distance_
  float camera_yaw_deg_, camera_pitch_deg_, camera_fov_y_deg_
  float camera_near_, camera_far_
  bool  projection_ortho_
  Eigen::Vector4f background_color_

ViewportWindow keeps reference aliases for each (including a proper
`float (&camera_target_)[3]` reference-to-array binding) so the
~150 call sites that touch camera state stay unchanged. Aliases
collapse when their owning methods migrate.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 14:24:49 +10:00
Dion Moult 8da0993457 ifcviewer: move scene mutators + releaseWgpuModelGpuData into ViewportCore (#84-f)
Move the eight scene-mutation methods that drive bonsai's load/unload
and georeference setters, plus the per-model GPU teardown helper.
All are mechanical transplants — no logic change — so behaviour stays
identical; only the owner has changed.

Methods moved (ViewportWindow public-API methods stay as forwarders
to keep the bonsai-side callers compiling):
  removeModel / resetScene / hideModel / showModel
  setFederatedFalseOrigin
  setModelCoordinateOperation
  setModelTransformation
  recomposeAndUploadModel

State moved:
  bool wgpu_initialized_   (storage → core_, alias kept in VW for
                            the initWgpu call site that still flips
                            it; goes when initWgpu moves)

Free function moved:
  releaseWgpuModelGpuData(ModelGpuData&, BufferPool&) → ViewportCore.cpp
  (must live in IfcViewerCore now that ViewportCore.cpp's
   removeModel / resetScene call it; ViewportWindow.cpp's remaining
   two call sites continue to resolve through ModelGpuData.h's
   declaration — same linker view, different definition TU)

The `if (isExposed()) requestUpdate()` Qt pattern inside the moved
bodies became `host_->requestFrame()` since ViewportCore can't see
QWindow; the desktop ViewportHost override at the bottom of
ViewportWindow.cpp continues to translate that into requestUpdate().

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 14:04:57 +10:00
Dion Moult b2fe9c4a71 ifcviewer: move const-lookup methods into ViewportCore (#84-e)
Move two pure-read methods (no GPU touch, no Qt) that the bonsai
measurement / federation-origin paths use:

  bool findInstance(uint32_t, InstanceLookup&)        const
  bool firstGeometryPointWorldM(uint32_t, Vector3d&)  const

ViewportWindow keeps both public-API method names — they now forward
to core_ for the implementation so existing callers in
bonsaiviewer/Measurement.cpp + Federation hooks don't have to change.
The InstanceLookup type also stays a `using` alias in ViewportWindow
(was added in #74).

Both methods were already de-Qt'd (`findInstance` delegates to
InstanceCompose; `firstGeometryPointWorldM` is pure Eigen). The move
is a straight transplant — no behaviour change.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 13:54:28 +10:00
Dion Moult ad6822ac85 ifcviewer: move composeInstanceFromPlacement into ViewportCore (#84-d)
First method-body migration. composeInstanceFromPlacement composes the
federated-false-origin × model-transformation × coordinate-operation ×
placement chain and re-derives the world AABB; it's a small,
self-contained method that only reads scene state and one matrix.

Moved:
  Eigen::Matrix4d federated_false_origin_meters_   (storage → core_)
  void composeInstanceFromPlacement(InstanceCpu&, ...) (body → core_)

ViewportWindow keeps:
  - alias reference to federated_false_origin_meters_ (existing
    setFederatedFalseOrigin call site still writes through it)
  - no method declaration — internal callers route through core_

Internal caller (recomposeAndUploadModel) now invokes
core_.composeInstanceFromPlacement; once recomposeAndUploadModel
itself moves into ViewportCore the call shortens back.

Pattern for the rest of #84: state moves, then method body moves,
then internal callers update. Each commit leaves desktop / bonsai /
web green and tests 100/100. This is one of many such steps.
2026-06-05 13:32:19 +10:00
Dion Moult 303f903a10 ifcviewer: move scene state into ViewportCore (#84-c)
Move the five scene-state fields that drive per-model GPU upload + the
streaming residency loop into ViewportCore:

  BufferPool      pool_              — vertex+index sub-allocator
  StreamingThread streaming_thread_  — background chunk reader
  std::unordered_map<uint32_t, ModelGpuData> models_gpu_
                                     — per-model state
  uint32_t        next_model_id_     — model-id allocator
  uint32_t        next_object_id_    — globally-unique object-id allocator

ViewportCore.h gains transitive includes for BufferPool / StreamingThread
/ ModelGpuData; ViewportWindow keeps the same names as reference aliases
so existing method bodies that touch them don't have to change.

Same risk profile as #84-a and #84-b: the storage moved but the values
are still set and consumed by the same code paths, so behaviour stays
identical.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 10:00:58 +10:00
Dion Moult d0be7b775a ifcviewer: move render pipelines into ViewportCore (#84-b)
Move the 15 pipeline + bind-group-layout + shader-module handles
that buildPipelines / buildEdgePipeline / buildPickPipeline write to.
Same pattern as #84-a: storage lives in ViewportCore, ViewportWindow
keeps reference aliases so existing builder-method bodies don't
have to acquire a `core_.` prefix at every touch point.

Moved fields:
  Main render group:
    main_shader_module_, frame_bgl_ (group 0), model_bgl_ (group 1),
    pipeline_layout_, main_pipeline_, main_pipeline_transparent_
  HiZ occlusion-cull group:
    hiz_shader_module_, hiz_bgl_, hiz_pipeline_layout_, hiz_pipeline_
  Edge silhouette group:
    edge_shader_module_, edge_bgl_, edge_pipeline_layout_, edge_pipeline_
  Pick pass:
    pick_pipeline_ (reuses pipeline_layout_ — same set of bindings)

ViewportWindow's constructor binds 16 new alias references after
the 7 lifecycle ones from #84-a; member-init order matches
declaration order so core_ is constructed before any alias binds.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 09:57:59 +10:00
Dion Moult e37a78f4f5 ifcviewer: move wgpu lifecycle state ownership into ViewportCore
First chunk of the #84 ViewportCore extraction. The seven wgpu lifecycle
handles (instance, adapter, device, queue, surface, surface_format,
surface_configured) now live as ViewportCore members; ViewportWindow
keeps reference aliases pointing at ViewportCore's storage so its
existing render-method bodies don't need a `core_.` prefix added at
every call site — 230+ touches deferred until each method moves
across.

Member init order in ViewportWindow's constructor:
  core_(this)               → constructs ViewportCore with host_=this
  instance_(core_.instance_) → binds the alias to core_'s field
  …                           same for adapter/device/queue/surface/…

Friend declaration on ViewportCore::ViewportWindow lets the references
bind to its private fields. The friend bond shrinks each commit as
render methods (and their `device_` / `queue_` references) migrate into
ViewportCore proper; the goal state is no friend and no aliases.

Next #84 chunks (separate commits) move pipelines, models_gpu_, pool_,
streaming_thread_, then the render/cull/encode methods. Each leaves
the desktop build green.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 09:39:57 +10:00