Commit Graph

19835 Commits

Author SHA1 Message Date
Dion Moult 7893135790 wgpu backend: --camera flag + request adapter's max buffer limits
Two pieces that block proper side-by-side parity with the GL minimal:

1. --camera tx,ty,tz,dist,yaw,pitch. Same format string as the GL
   minimal so a pasted camera arg lands the same view on both backends.
   setCamera() also flips initial_view_applied_ = true so the auto-
   viewAll-on-first-load doesn't snap away from the script-set position
   when the model finishes uploading.

2. Real BIM models exceed the conservative WebGPU defaults at device
   create time. A 114k-instance / 19M-index sidecar's vertex storage is
   139 MB, which trips wgpu's default 128 MB max_storage_buffer_binding_
   size and bind-group creation fails. Now wgpuAdapterGetLimits is
   called first and the device is requested at the adapter's full
   ceiling — every desktop driver supports multi-GB.

   Trade-off worth flagging: web parity will fail here because browsers
   cap at the defaults. The eventual fix is to split a model's vertex/
   instance storage into ≤128 MB chunks with a small per-frame routing
   table, which is a real chunk of work. For now this unblocks all the
   native benchmarking the user is actually doing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:58:18 +10:00
Dion Moult f9a9273ecc wgpu backend: match GL orbit + viewAll math so pivots align
Reported regression: --benchmark on the same sidecar visibly rotated
around a different point in the wgpu binary than in IfcViewerMinimal.
Root cause was two camera-convention drifts:

  1. orbitEye placed the camera at (sin yaw, -cos yaw) from target;
     the GL backend uses (cos yaw, sin yaw). Same target, but the
     camera faces a different side of the model at yaw=0, which made
     the orbit feel like it pivoted around a different point even
     though the actual world-space target was the same. Now exactly
     matches GL ViewportWindow::updateCamera:
        eye.x = target.x + dist * cos(pitch) * cos(yaw)
        eye.y = target.y + dist * cos(pitch) * sin(yaw)
        eye.z = target.z + dist * sin(pitch)

  2. viewAll's distance was an ad-hoc 0.6 * diag / tan(half_fov);
     GL uses frameAabb(mn, mx, 1.10): tan_half = tan(fov/2),
     min_aspect = min(aspect, 1), distance = (radius / (tan_half *
     min_aspect)) * 1.10. Aspect-aware so portrait windows pull back
     enough that the bounding sphere still fits on the tighter axis.
     Now ported verbatim.

Also logs the computed target + distance on viewAll so a follow-up
side-by-side run prints both backends' framings and any remaining
discrepancy is easy to spot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:52:12 +10:00
Dion Moult 244a145255 wgpu backend: per-instance LOD0/LOD1 pick in the cull
Stage 8 of the wgpu port. cullModelCpu now buckets each visible instance
by (mesh_id, lod) instead of (mesh_id), and emits one MeshDraw record
per non-empty bucket. LOD pick projects the instance's world-space
bounding sphere to pixels via

    projected_px = world_radius * focal_px / view_z

where focal_px = viewport_h / (2 * tan(fov_y/2)) and view_z is the
forward·(center-eye) depth. When projected_px < lod1_pixel_threshold_
AND the mesh has a baked LOD1 slice (MeshInfo.lod1_index_count > 0),
the instance draws the LOD1 index range instead of LOD0; baseVertex
and the vertex storage are shared between LODs.

mesh_draws can now grow to up to 2 × meshes.size() per frame (LOD0 + LOD1
slice per mesh). The visible_buffer layout per mesh becomes
[LOD0 instances | LOD1 instances] contiguous, with each MeshDraw
referencing its own firstInstance offset.

lod1_pixel_threshold_ defaults to 30 (mirrors AppSettings::
lod1PixelThreshold() in the GL backend); set to 0 to disable LOD1
entirely (always LOD0). AppSettings port lands in a later commit.

Verified: basic.ifc (3 tiny instances, no LOD1 baked by meshoptimizer
since each mesh is well under the 500-tri threshold) renders pixel-
identical to pre-stage-8 — proves the all-LOD0 path is preserved.
Real LOD switching needs a sidecar where buildLods produced LOD1 slices.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:47:23 +10:00
Dion Moult 4596f2e584 wgpu backend: lighting parity, MSAA, cavity shading, fix sRGB output
Closes the visible gap to BonsaiViewer down to just the post-process
edge silhouette pass (still pending in task #9). Four changes bundled
because together they bring up the parity story:

  - WGSL fragment now applies cavity = clamp(length(fwidth(n))*1.5,
    0, 0.35) and multiplies by (1 - cavity). Matches GL shader.

  - Lighting constants switched to GL's exact values: key (0.3, 0.5,
    0.8), fill (-0.3, -0.5, 0.8), sky tint (0.55, 0.60, 0.70), ground
    tint (0.35, 0.32, 0.28). My initial guesses were close but not
    identical; matching them means side-by-side diffs only flag actual
    pipeline differences, not lighting tweaks.

  - 4× MSAA: render pass writes into a MULTISAMPLE color attachment
    (surface_format_-matched), resolves into the surface texture for
    present. Depth is also 4 samples. Pipeline.multisample.count = 4.
    ensureMsaaColorTexture / releaseMsaaColorTexture mirror the depth-
    texture lifecycle. Matches GL minimal's QSurfaceFormat::setSamples(4).

  - sRGB output fix. wgpu-native's Vulkan swap chain on X11 treats
    BGRA8Unorm as sRGB-output (applies linear→sRGB encoding on shader
    writes), even though caps.formats[0] reports plain Unorm. The GL
    backend writes to a non-sRGB framebuffer with no such conversion,
    so a clearValue of (0.125, 0.137, 0.161) lands as bytes (32, 35,
    41) on GL but (99, 104, 112) on wgpu — ~3× brighter. Pre-decoding
    via srgbToLinear on (a) the clearValue in C++ and (b) the final
    fragment colour in WGSL makes wgpu's implicit encode round-trip,
    so the final bytes match GL. Verified via screenshot pixel sample:
    #202329 background reads as exactly (32, 35, 41).

Remaining visible gap to BonsaiViewer is the dark-line edge silhouettes
(renderEdgePass in GL, depth laplacian → outline). That belongs with
the overlay / post-process work in task #9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:44:13 +10:00
Dion Moult a95437dd64 wgpu backend: match GL pitch sign so drag-down tilts the camera up
Drag-down was decreasing pitch (camera diving), opposite to the GL
viewport's convention where drag-down increases pitch so the top of
the object rotates toward the viewer. Yaw direction was already
correct. Matches the existing user muscle memory from IfcViewerMinimal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:29:04 +10:00
Dion Moult de44de26f8 wgpu backend: clearer sidecar-load diagnostics + tilde expansion
The single "(file missing, wrong magic, or schema mismatch)" message
was making triage harder than necessary. loadSidecar now expands a
leading ~/ (shells skip it inside double quotes, which trips up paste-
from-launcher), and on failure peeks the file's header itself to
report exactly which check failed:

  - "Sidecar not found"          — file doesn't exist
  - "Sidecar unreadable"         — exists but open failed
  - "Sidecar truncated"          — <12 bytes
  - "Sidecar magic mismatch"     — wrong magic, reports got vs expected
  - "Sidecar schema mismatch"    — wrong version, reports both numbers
                                    and suggests re-baking
  - "Sidecar endianness mismatch" — cross-platform load attempt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:26:20 +10:00
Dion Moult 819196b3ce wgpu backend: --benchmark N parity with the GL minimal
Stage 11 of the wgpu port. WgpuViewportWindow gains setBenchmarkFrames(N);
the minimal driver wires it to a --benchmark N flag. Renders N frames
after a 5-frame warmup, yaw-sweeping the camera at 0.5°/frame, captures
per-frame wall time with QElapsedTimer (cull + encode + present), and
prints avg/median/p1/p99 + last-frame stats in the same line format as
IfcViewerMinimal so a script can diff them line for line.

Per-frame stats (visible_objects, visible_triangles, sub_draws) are now
summed in render() from m.mesh_draws. hiz_rej reports 0 until stage 7
adds HiZ occlusion.

Verified on basic.ifc (3 instances): wgpu 11.68 ms avg vs GL 11.75 ms
avg — same scene, same camera sweep, same window size. Noise-level
delta as expected on a tiny scene; the interesting comparison is on
real BIM corpora once you bake them to v13 sidecars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:14:40 +10:00
Dion Moult ddef8c65b5 wgpu backend: orbit/pan/zoom mouse navigation
LMB drag → orbit (yaw/pitch, pitch clamped to ±89.9° to avoid gimbal
flip at the poles). MMB drag → pan in the camera's screen-space plane,
world-units-per-pixel sized against the view frustum at the pivot depth
so panning feels constant regardless of zoom. Wheel → zoom (12% per
notch, sign matches "wheel up = closer"). LMB is bound to orbit because
selection isn't wired yet; will rebind to selection + nav preset once
AppSettings ports over.

Pure addition to WgpuViewportWindow — overrides four QWindow event
handlers, no changes to render or cull paths. Lets you actually fly
around a loaded sidecar without a screenshot loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:10:51 +10:00
Dion Moult 61726e00a4 wgpu backend: CPU frustum cull + per-mesh draw compaction
Stage 6 of the wgpu port. Replaces the one-draw-per-(mesh, instance) loop
with a CPU cull pass that survives one drawIndexed per non-empty mesh
with packed instanceCount.

Adds to WgpuModelGpuData:
  - visible_buffer: u32[] storage SSBO, pre-sized to instance_count at
    applyCachedModel so the bind group reference never invalidates.
    Re-uploaded each frame via wgpuQueueWriteBuffer.
  - mesh_draws: per-mesh schedule (first_instance, instance_count,
    first_index, base_vertex, index_count). instance_count==0 means the
    mesh contributed nothing this frame and the draw is elided entirely.

cullModelCpu per-frame:
  - Extract 6 frustum planes from the same VP we write into the uniform.
    WebGPU clip-space z is [0, 1], so near plane = matrix row 2 (not
    row 3 + row 2 as in GL); rest of the derivation is standard.
  - Per-instance AABB-vs-frustum test using the p-vertex shortcut
    (cheapest correct early-out for AABBs).
  - Bucket survivors by mesh_id; flatten into a contiguous u32 list;
    upload via wgpuQueueWriteBuffer. Per-mesh slice is [first_instance,
    first_instance + instance_count).

WGSL adds @group(1) @binding(3) var<storage, read> visible: array<u32>
and an extra indirection: instance_idx = visible[iid]; the rest of the
shader is unchanged. firstInstance on each drawIndexed offsets into
visible[], so each mesh reads its own slice.

Verified two ways:
  1. basic.ifc (3 instances, all on-screen) renders pixel-identically
     to pre-stage-6 — proves cull keeps everything it should.
  2. basic.ifc + a synthetic instance placed at (100, 100, 100) is
     culled cleanly: only the cube renders, the far quad is rejected
     by the frustum test. Proves cull actually rejects out-of-frustum
     geometry rather than passing everything through.

Contribution culling, HiZ, and LOD selection arrive in stages 7 and 8;
they all hook into the same cullModelCpu seam.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:05:58 +10:00
Dion Moult 75b9963136 wgpu backend: --screenshot capability for visual verification
Pulls the capture half of task #10 forward so we stop flying blind from
stage 3 onward. WgpuViewportWindow gains captureNextFrameToPng(path);
the minimal driver wires it to a --screenshot PATH flag that renders
one frame, copies the surface texture back to host memory, writes a
PNG via QImage, and quits.

CopySrc is added to the surface configuration usage so the surface
texture can be the copy source. The texel-to-buffer copy honours
WebGPU's 256-byte bytes-per-row alignment by padding rows and stripping
the padding when assembling the QImage. Surface format 28 (BGRA8Unorm)
is byte-swapped to RGBA on the way into QImage::Format_RGBA8888;
RGBA8 surface formats are memcpy'd straight through.

Verified end-to-end on /tmp/basic.ifcview: 3 cube meshes/instances
render with depth, back-face cull, and the hemisphere-ambient + key+fill
lighting model — top face reads sky (bright), front faces read mid-tone,
exactly as the WGSL shading intended. The pixel-diff half of task #10
(comparing against a GL baseline) lands later when the GL minimal binary
gets an equivalent flag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:33:19 +10:00
Dion Moult bbf2bfde92 wgpu backend: vertex-pulling main render pass
Stage 3 of the wgpu port. Replaces the clear-only render loop with the
full main shading pass:

  - WGSL port of the GL main shader. Vertex-pulling: the vertex storage
    buffer is read as array<u32> in the shader, with pos/normal/color
    decoded manually per vertex. baseVertex (set per draw to mesh's
    vertex offset) folds into @builtin(vertex_index) automatically;
    firstInstance carries the instance slot for @builtin(instance_index).
    No vertex-input layout — vertex pulling means no IA bindings.

  - Render pipeline bound to depth-32-float (write-on, less compare),
    back-face cull, CCW front face. Pre-multiplies a [-1,1]→[0,1] z-remap
    matrix onto Qt's projection so WebGPU's clip-z convention is met.

  - Two bind groups: group=0 per-frame (uniform with view-proj + key/fill
    light + hemisphere ambient), group=1 per-model (three read-only
    storage buffers: vertices, mesh quant, instances).

  - Depth texture is created lazily and recreated on surface resize.

  - Orbit camera state on WgpuViewportWindow with viewAll() that frames
    the union of all loaded models' world AABBs after the first load.
    Mouse navigation lands later.

  - Draw loop: one drawIndexed per (mesh, instance) pair per model. This
    is correct but CPU-heavy on dense scenes; stage 6 introduces the cull
    + compacted visible list that lets multiple instances of one mesh
    collapse to a single call, and the eventual GPU-driven cull (post
    sunset of the GL backend) goes further.

Verified on /tmp/quad_v13.ifcview (1 mesh, 1 instance) and on a real v13
sidecar baked from basic.ifc via the GL minimal viewer (3 meshes,
3 instances, 864 B verts). No wgpu validation errors fire across pipeline
creation, depth attachment, bind groups, or the draw loop on either.
Visual confirmation deferred until --screenshot lands (task #10) which
is being pulled forward next so we don't keep flying blind.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:19:09 +10:00
Dion Moult 9daa5fe195 wgpu backend: load .ifcview sidecars onto GPU buffers
Stage 2 of the wgpu port. WgpuViewportWindow gains a queueLoadSidecar
API (called from the minimal driver before init) and an applyCachedModel
that runs after init: reads via SidecarCache::readSidecar, allocates
four wgpu buffers per model (vertex storage, index, mesh-quant storage,
instance storage), uploads via wgpuQueueWriteBuffer, retains a CPU
mirror of the MeshInfo/InstanceCpu arrays for the cull and picking
paths that arrive in later stages.

MeshGpu (the per-mesh quantization basis) is derived from MeshInfo on
the fly; InstanceGpu (transform + ids) is derived from InstanceCpu and
uses the cached float transform — composing from placement_transformation
against federation-stage matrices lands when stage 5 wires those.

SidecarCache.cpp is compiled into IfcViewerWgpu directly: it's pure
C++ with no Qt/OCCT/IFC-parse deps, so dragging in the IfcViewer
static lib for one source file would be wasteful. This duplication
goes away once src/ifcviewer-core/ is extracted (task #12).

Verified on a synthesised v13 sidecar (4 verts, 6 indices, 1 mesh,
1 instance) and a multi-sidecar load that assigns successive model_ids.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:48:03 +10:00
Dion Moult 19a39a0413 Scaffold experimental wgpu viewer backend
Adds src/ifcviewer-wgpu/ and src/ifcviewer-wgpu-minimal/ behind a new
BUILD_BONSAIVIEWER_WGPU option (default OFF), gated independently of
BUILD_BONSAIVIEWER. Stage 1 brings up a Qt window with a wgpu-native
v29 surface (X11) and clears to the background colour — no rendering
beyond that yet. Mirrors the lifecycle of the GL ViewportWindow so
subsequent stages (vertex-pulling renderer, pick, cull, HiZ, overlay)
slot in without restructuring the host.

wgpu-native is fetched as a pre-built binary release via FetchContent;
its .so SONAME is patched in at configure time so dependents get a
clean DT_NEEDED. The X11 native handle is obtained via the public
QNativeInterface::QX11Application API; Wayland and macOS/Windows
surface creation are stubbed with explicit "not wired yet" warnings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:23:23 +10:00
Gorgious56 db9d903650 Polish tool.Model + tool.Pset + add tool.Slab service
tool.Model gains:

* get_pipe_segment_props / get_duct_segment_props — typed prop accessors
  for the MEP-segment edit lifecycle.
* resolve_active_props_for_edit — picks the right BIM*Properties to
  drive a parametric edit triad based on the active object's IFC class.
* mirror_parent_void_fillings_to_children — when an array parent has
  hosted fillings (door/window in a wall), replicate the same fill
  rels onto each array child. Uses tool.Array.get_parametric_propagation_
  targets so the propagation stays within the array family (the old
  get_all_element_occurrences over-propagated to standalone occurrences
  of the same type, which silently mutated unrelated arrays).
* unshare_opening_representation — fork a shared IfcShapeRepresentation
  so editing one opening doesn't mutate its array sibling.
* duplicate_ifc_objects gains a post-condition select-restore on the
  array parent so callers don't get a deselected parent for N>=2 arrays.

sync_object_ifc_position is kept as a thin delegate to
tool.Geometry.commit_placement_if_moved (the new home, added in C8) so
the 6 v0.8.0 callers in mep / product / system don't AttributeError;
PR4 migrates each caller and removes the delegate.

tool.Pset gains:

* upsert_pset — get-or-add-or-edit in one call.
* write_bbim_data — JSON-encode + write BBIM_* metadata in one call.

tool.Slab is new — slab-specific reads (active extrusion, axis
direction) used by the slab gizmos, pure-IFC, no PropertyGroup mutation.

Generated with the assistance of an AI coding tool.
2026-05-27 00:14:51 +02:00
Gorgious56 3483683cb4 Add tool.Geometry helpers for body representation + placement
Adds:

* get_body_representation(element) — DRY of the repeated
  ifcopenshell.util.representation.get_representation(element, "Model",
  "Body", "MODEL_VIEW") call across slab / wall / opening / stair /
  roof / door / window / mep. One central place to read the body rep;
  every caller stops re-spelling the four magic strings.
* has_axis_representation(element) — predicate for elements with a
  GRAPH_VIEW Axis representation. Used by the wall/MEP path decorators
  to skip elements without an unambiguous 1D path.
* has_material_styles(element) — predicate for whether the element
  carries IfcStyledItem material assignments.
* restore_placement_from_ifc(obj, element) — snap obj.matrix_world back
  to element's committed IFC placement + rebaseline the drift checksum.
* restore_or_rebaseline_placement(obj, element) — Cancel-flow helper:
  restores if ObjectPlacement exists, just rebaselines the checksum if
  not.
* detach_representation(product) — remove the active representation
  from a product without deleting the entity (used by parametric
  rebuilds that wipe + re-add).

commit_placement_if_moved docstring expanded with a "drop-in scope"
note so callers don't redundantly wrap it in an is_moved check that
the helper already does.

Switches the duplicate-aware helper calls (formerly tool.Root.*) to
tool.Duplicate.* now that the service exists (C6).

Generated with the assistance of an AI coding tool.
2026-05-27 00:04:03 +02:00
Gorgious56 a0c6f6f9a6 Extend tool.Blender for parametric framework + decorators
Adds:

* ViewportDecorator base class — install/uninstall/draw lifecycle for
  3D viewport gpu overlays, with handler-rollback-on-failure so a
  partial install can't leave dangling draw handlers.
* sync_all classmethod — drive each listed ViewportDecorator subclass
  to its desired install state in one call.
* is_view_top_down + top_down_factor — viewport-camera orientation
  predicates used by gizmo billboarding and decorator layout.
* get_screen_up_world — screen-up vector in world space for gizmo
  text orientation.
* are_viewport_gizmos_enabled — central gate for the global
  draw_gizmos_in_3d_viewport pref, replacing duplicated prefs reads.
* DecoratorColors NamedTuple + get_decorator_colors — single source
  for the colour palette every viewport decorator binds.

Preserves Ryan Schultz's add_layout_hotkey_operator polish (719309571,
2026-05-25): the row-position move + separator(factor=1) between the
modifier and key icons stay intact in this extraction.

Generated with the assistance of an AI coding tool.
2026-05-27 00:00:53 +02:00
Gorgious56 49ddda6281 Add tool.Duplicate service
Extract the duplicate-aware relationship-walk + restoration logic
(get_decomposition_relationships, get_connection_relationships,
get_port_connection_relationships, recreate_decompositions,
recreate_connections, recreate_port_connections, consume_warnings)
out of tool.Root into its own service.

tool.Root's responsibility is identity and addressing of IFC roots;
the duplicate-aware bookkeeping of "before duplication, what relations
did this graph have, and how do I restore them on the new copies?"
deserves its own home. The split was already declared on core/tool.py
(C2); this commit lands the concrete tool.Duplicate implementation.

tool.Root keeps its own copies of the methods on v0.8.0's tool/root.py
during this PR so callers in bim/module/spatial/operator.py keep
working at runtime; the Root cleanup lands in PR4 alongside the
caller updates.

Generated with the assistance of an AI coding tool.
2026-05-26 23:48:11 +02:00
Gorgious56 96b6985960 Extend tool.System with port + path helpers
Adds:

* direction_from_port_pair(port_a, port_b) — derive the connect_port
  direction kwarg from each port's FlowDirection (NOTDEFINED for
  non-canonical pairs). Centralises a pattern that callers were
  inlining inconsistently.
* tool.System.walk_connected_mep_elements — BFS over connected MEP
  flow elements via IfcRelConnectsPorts.
* tool.System.get_port_world_position — port placement → world-space
  Vector, used by the MEP path decorator.
* tool.System._build_decoration_data — cached decoration metadata
  for the MEP system-path overlay.

Plus a get_port_relating_element return-type tightening (Union with
None) and a partial-init cycle workaround on bim.module.system.data
imports (now function-local — top-level import triggered the cycle
through tool.Ifc.Operator).

Generated with the assistance of an AI coding tool.
2026-05-26 23:45:14 +02:00
Gorgious56 b19b2ac7cd Add tool.Array service
Top-level array-domain service extracted out of tool.Blender.Modifier.Array.
Owns the BBIM_Array pset graph navigation (constrain_children_to_parent,
remove_constraints, get_modifiers_data, get_children_objects,
get_all_children_objects, get_child_layer_index, bake_children_transform),
plus the Blender-side CHILD_OF constraint lifecycle that ties each child
replica to its parent's transform.

Array's own module gives the parent/child semantics a clean home — array
behaviour was previously scattered between tool.Blender.Modifier and ad-hoc
helpers in bim/module/model/array.py. The relocation eliminates the inline
duplication and gives Bonsai callers a single import surface.

Generated with the assistance of an AI coding tool.
2026-05-26 23:41:56 +02:00
Gorgious56 fdf4b82371 Add tool.Wall service
Bpy-permitted wall reads — get_axis_local_extent, get_length_and_height,
get_x_angle, get_path_connection_location, walk_connected_walls — used
by gizmo lambdas that need wall dimensions and join topology without
the side effect of loading the wall's draft BIMWallProperties (the
loader mutates PropertyGroup state and would clobber the wall's own
gizmo state when both the wall and a hosted filling are selected).

All reads go through ifcopenshell.util.representation / .util.element
so the IFC graph stays the source of truth. tool.Wall consumes
core.model's PARALLEL_DOT_THRESHOLD + collinearity helpers (no inline
magic numbers).

Generated with the assistance of an AI coding tool.
2026-05-26 23:40:19 +02:00
Gorgious56 2f40441f1c Add tool.* interface stubs to core.tool
Declares the bpy-free contract for tool services landing in subsequent
commits — tool.Wall, tool.Array, tool.System, tool.Duplicate (extracted
from tool.Root), tool.Parametric, plus minor additions on existing
interfaces (tool.Spatial.get_host_element / get_host_wall,
tool.Geometry.has_axis_representation / has_material_styles,
tool.Surveyor.get_z_rotation / set_z_rotation).

The @interface declarations are empty-bodied; concrete implementations
land in the per-service tool/* commits below. Keeping the contract in
core lets core/* helpers and tests reference the surface without
importing the concrete tool modules.

Moves get_decomposition_relationships + recreate_decompositions off
tool.Root onto the new tool.Duplicate (extraction of duplicate-aware
behaviour into its own service).

Generated with the assistance of an AI coding tool.
2026-05-26 23:31:28 +02:00
Gorgious56 230cbe1fd8 Add core/model.py constants + core/product.py helpers
core/model.py gains:

* Three calibrated dot-product / distance thresholds — PARALLEL_DOT_THRESHOLD
  (~2° from parallel, cos(2°) ≈ 0.9994), COLLINEAR_LINE_TOLERANCE (50mm
  perpendicular distance for two parallel wall axes to share a line),
  BASELINE_OFFSET_TOLERANCE — replacing inline magic numbers that the
  wall-join classifier, fillet-state machine, and gizmo preview decorator
  all read from.
* Pure wall-join geometry helpers (project_axis_intersection,
  are_axes_collinear, classify_wall_join_state, wall_join_preview_lines,
  resolve_extend_walls_target, extrusion_depth_from_vertical_height,
  length_and_height_from_extrusion). They take primitive tuples + floats,
  no bpy, no ifcopenshell — testable in the core lane.

core/product.py is new — pure-Python aggregate-walk helpers (resolve_host_
of_product, collect_decomposed_products) that downstream tool/spatial and
tool/aggregate consumers can call without importing ifcopenshell at module
load.

Generated with the assistance of an AI coding tool.
2026-05-26 23:28:19 +02:00
Gorgious56 4d4c5b4d51 Split railing representation into pure-compute + IFC wrapper
add_railing_representation now factors into two parts:

* compute_wall_mounted_handrail_geometry returns a pure-geometry
  WallMountedHandrailGeometry dataclass (handrail polyline + support
  list + terminal caps), no IFC mutation.
* add_railing_representation wraps that dataclass into an
  IfcShapeRepresentation as before.

Downstream consumers that want the same math without round-tripping
through an IFC file (Blender gizmo previews, viewport drafts) now
drive compute_X directly. Future add_X_representation work in the
geometry API is encouraged to follow the same shape — a sibling
compute_X function + thin IFC wrapper.

The railing_type parameter is dropped from the signature — only
WALL_MOUNTED_HANDRAIL was ever supported, so the kwarg was dead.
The Bonsai railing-modifier caller is updated in the same commit
to stop passing it; without that update Bonsai's
finish_editing_railing_path raises TypeError on the first edit.

RailingSupport and WallMountedHandrailGeometry use @dataclass(slots=True)
— they're constructed N-per-cap during arc sampling, so the per-instance
overhead matters.

Public symbols (RailingSupport, TERMINAL_TYPE,
WallMountedHandrailGeometry, compute_wall_mounted_handrail_geometry,
add_railing_representation) re-exported from ifcopenshell.api.geometry.
New test/api/geometry/test_add_railing_representation.py covers the
compute/wrap contract.

Generated with the assistance of an AI coding tool.
2026-05-26 23:22:19 +02:00
Gorgious56 3d81660dad Use util.unit.mm_to_m in add_window_representation
Drops the module-local ``mm()`` helper in favour of the centralised
``ifcopenshell.util.unit.mm_to_m`` (added earlier in this PR). The
``as mm`` import alias preserves the existing call sites' readability.

Generated with the assistance of an AI coding tool.
2026-05-26 23:22:19 +02:00
Gorgious56 b4abd999b6 Use util.unit.mm_to_m in add_door_representation
Drops the module-local ``mm()`` helper in favour of the centralised
``ifcopenshell.util.unit.mm_to_m`` (added earlier in this PR). The
``as mm`` import alias preserves the existing call sites' readability.

Generated with the assistance of an AI coding tool.
2026-05-26 23:22:19 +02:00
Gorgious56 1e6db764d4 Add numpy axis-index constants + silence MEP-transition prints
ShapeBuilder gains module-level NP_X / NP_Y / NP_Z / NP_XY / NP_XZ /
NP_YZ / NP_YX axis-index constants. Downstream geometry builders had
been redefining local copies for indexing np.ndarray vectors of shape
(3,) or (N, 3); centralising removes the duplication.

mep_transition_length and mep_transition_calculate verbose default
flipped from True to False. The prints are diagnostic-only output;
True-by-default spammed the console on every transition computation,
which fires per-fitting on IFC load.

Generated with the assistance of an AI coding tool.
2026-05-26 23:22:19 +02:00
Gorgious56 936526b41b Add ifcopenshell.util.unit.mm_to_m helper
Centralises the millimetre-to-metre conversion shortcut that
add_door_representation and add_window_representation each defined
locally. Subsequent commits in this PR switch both call sites to
import this from util.unit, removing the duplicate definitions.

Generated with the assistance of an AI coding tool.
2026-05-26 23:22:19 +02:00
Dion Moult dd902bf0f8 Use fixed overlay text font
Use Qt's system fixed font for viewer overlay text instead of the generic monospace family, avoiding the Windows font-resolution delay seen during measurement overlays.

Generated with the assistance of an AI coding tool.
2026-05-26 13:36:03 +10:00
Richard Brice 45ea5eb07a Updates alignment api. Fixes bugs authoring semantic-only alignment 2026-05-25 10:34:29 -07:00
Richard Brice 42ed398169 Simplifies line and circle parent curves and parent curve normalization 2026-05-25 10:34:29 -07:00
Richard Brice f70044d373 Fixes bug computing cross slope 2026-05-25 10:34:29 -07:00
Ryan Schultz 719309571e Improve active tool panel hotkey button display
Use add_layout_hotkey_operator for draw_regen_operations so the Regen
button shows text and shortcut icons in the sidebar like all other
panel buttons. Add a separator between modifier and key icons for
readability.
2026-05-25 09:35:12 -05:00
Dion Moult 371aabfef6 Disable Autodesk connector UPX
Build the PyInstaller Autodesk connector without UPX compression. UPX-packed launchers are more likely to trigger enterprise Windows security scanning, and the connector is distributed as a fresh unsigned artifact for each build.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult 1c22fa0669 Use bound overlay uploads
Update the overlay renderer's dynamic VBO uploads to bind the buffer and use glBufferData/glBufferSubData instead of direct-state glNamedBufferData/glNamedBufferSubData.

This avoids Windows/NVIDIA driver corruption seen with overlay axes, pick markers, HUD rects, and marquee rectangles while keeping the same overlay geometry and draw paths.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult 89cb551dbb Run Autodesk upload/download on a worker thread
The progress dialog was the only connector window not driven by a Tk
event loop: the handler created it, then blocked inline in httpx I/O.
On Windows CTkToplevel withdraws itself at construction and re-shows via
a delayed after() callback, which never fires without a running loop, so
the progress window stayed invisible for the whole transfer.

Add run_with_progress(): the blocking work runs on a daemon thread while
the main thread pumps the Tk loop and shows the dialog. Progress reports
are coalesced and marshalled back to the UI thread via _ProgressBridge,
and worker exceptions are re-raised on the main thread, preserving the
JSON-RPC error path. All eight upload/download handlers converted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 102ac551b3 Add VisibilityState/SelectionState tests; test real quantization helpers
test_instanced_geometry previously re-implemented vertex quantization
inline, with a stale comment claiming the helpers still lived in
ViewportWindow.cpp. They now live in VertexQuantization.h, so route the
test through the real quantizeVertex/octEncodeNormal and add coverage
for the degenerate-axis path, octahedral normal round-trip, the i8
normal error bound (~0.78 deg worst observed), and color passthrough.

Add test_visibility and test_selection: Tier-1 coverage of the two
per-object viewport state machines. Both are QObjects for their
changed() signal but touch no GL on the construction/mutation path, so
the tests exercise the pure CPU logic without a context.

Suite goes from 39 to 61 cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 137a890256 Add test suite for the Bonsai Viewer Autodesk connector
Introduce pytest coverage for the previously untested connector — rpc,
cache, settings, autodesk (auth + APS client) and connector handlers —
94 tests, runnable via the new `test` optional-dependency extra.

To make HTTP, time and the OAuth redirect testable without a network or
real sockets, add dependency-injection seams to autodesk.py:
AuthSessionService and ApsClient accept an optional httpx transport;
AuthSessionService accepts an injectable clock and callback_waiter; and
_wait_for_callback is extracted to the module-level wait_for_oauth_callback.
All seams default to the previous behaviour.

Remove the APS_CLIENT_ID environment-variable override: the client id now
comes solely from settings.json, collapsing settings.load_client_id and
simplifying the settings dialog.

CI: the build-bonsaiviewer-autodesk workflow gains a `test` job
(Python 3.11 + 3.13) that gates the build matrix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult de7520418b Build the Bonsai Viewer in CI with the Autodesk connector bundled
Compile the Bonsai Viewer as part of the Linux and Windows binary builds,
and ship the Autodesk connector alongside the viewer executable.

Qt6 dependencies:
- The viewer links Qt6::Svg for runtime icon tinting. Svg is a separate
  base-Qt archive, so aqt now installs "qtbase qtsvg" (plus icu on Linux)
  rather than qtbase alone, on both Linux and Windows.
- Qt6::CorePrivate is exposed differently across Qt versions: Qt 6.8 ships
  the target inside Qt6Core, while Qt 6.10 provides it only as a separate
  CorePrivate config package. The viewer CMakeLists requests it via
  OPTIONAL_COMPONENTS so it resolves on both.
- When cross-compiling Windows ARM64, windeployqt runs from the host x64
  Qt, so qtsvg is installed into the host Qt as well.

Windows build:
- build-all-win.py passed -DBUILD_IFCVIEWER, a flag since renamed to
  BUILD_BONSAIVIEWER, so the Windows build compiled no viewer at all. It
  now passes -DBUILD_BONSAIVIEWER.
- The Autodesk connector is bundled under connectors/ next to
  BonsaiViewer.exe in the packaged archive, mirroring the Linux builds.
- The Windows workflow builds the connector (PyInstaller) before the main
  build so it is available to bundle.

Connector bundling:
- The Linux rocky workflows build the connector and bundle it into the
  BonsaiViewer archive; the Windows build now does the same.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult e8a93846dd Bundle connectors next to the Bonsai Viewer executable
Connector discovery scanned a per-user data directory
(QStandardPaths::GenericDataLocation -> ~/.local/share/IfcOpenShell/
BonsaiViewer/connectors and the macOS/Windows equivalents). Connectors
are now meant to ship with the application, so there is no reason to
look outside the install tree.

Replace userConnectorsDir() with bundledConnectorsDir(), which returns
QCoreApplication::applicationDirPath() + "/connectors". discoverConnectors()
scans only that path; its first-wins / malformed-manifest handling is
unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 96941463c0 Simplify Models panel and dialog layout
Models panel: replace the manual resizeEvent column-sizing hack with
QHeaderView Stretch/Fixed modes, re-applied via sectionCountChanged so
they survive the model rebuilds that QHeaderView resets them on.

Dialog: only wrap the body in a QScrollArea when scrollable, mirroring
Panel. The scroll area caps its sizeHint at 36x24 cells, which turned
wide fixed-size dialog content into spurious scrollbars.

Add Model dialog: reserve a stable, font-metrics-measured height for the
hover description so longer text never reflows the buttons; regroup the
buttons into LOCAL / CLOUD / TOOLS.

Buttons: move the trailing-separator decision out of makeButtonGroup
into a new addButtonGroups row builder, so the last group in a row
never draws a dangling divider.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 19bff92a47 Fix data races when parsing IFC files on concurrent threads
Loading a federated project (.ifcfed) with several models segfaults
non-deterministically on a fresh start. The viewer's SceneLoader spawns
one background std::thread per model in startDataSourceLoad() to
construct an ifcopenshell::file; with cached sidecars all models reach
that point near-simultaneously, so multiple threads parse different IFC
files at once. Parsing touches the process-wide schema singleton, which
was not thread-safe in two places.

Race 1 — concurrent schema population
-------------------------------------
schema_registry::get() lazily runs the schema's get_() function (e.g.
Ifc4::get_schema() -> IFC4_populate_schema()) and mutates entries_ with
no lock. Two threads calling schema_by_name("IFC4") at once both run
IFC4_populate_schema() concurrently, which fills global arrays
(IFC4_types[], strings[]). One thread reads a slot the other is still
writing.

Core-dump evidence (gdb thread apply all bt):

  Thread 1  SIGSEGV in IFC4_populate_schema   Ifc4-schema.cpp:1989
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- schema_by_name("IFC4")
            <- ifcopenshell::file::file (NWCH-PIR-SS...ifc)
            <- SceneLoader::startDataSourceLoad lambda  SceneLoader.cpp:315

  Thread 3  also in IFC4_populate_schema (entity ctor for
            "IfcMaterialProfileSetUsageTapering")
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

Two threads inside IFC4_populate_schema() at the same time is the race.

Fix: guard schema_registry's bind()/get()/names()/clear() with a
recursive_mutex (recursive because get() re-enters bind() via
load_schema_plugin(), and a freshly populated schema registers itself
through register_schema()). get() is serialized, so only the first
thread populates the schema; the rest block briefly and then observe
the finished result. Returned schema pointers are stable for the
process lifetime, so holding the lock only across get() is sufficient.

Race 2 — lazy all_attributes_ cache filled during parsing
---------------------------------------------------------
entity::all_attributes() lazily fills a `mutable` optional cache on the
shared schema entity the first time it is accessed — and that first
access happens during parsing (parse_context::construct), not during
schema population. With race 1 fixed, two parser threads still raced
here: both saw the cache empty, both did all_attributes_.emplace() and
std::copy() into it, corrupting the vector.

Core-dump evidence after the race-1 fix:

  Thread 1  SIGSEGV in attribute::type_of_attribute (this=0xe130...55c)
            <- std::transform(first=0x4, last=0xb0d1...)   <-- garbage
               iterators into a corrupt std::vector
            <- parse_context::construct over
               decl->as_entity()->all_attributes()        file.cpp:249
            <- instance_streamer::read_instance
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

The begin pointer 0x4 is a half-written vector being read mid-resize by
another thread.

Fix: force every entity's all_attributes_ cache in the
schema_definition constructor, while construction is still
single-threaded. The schema is then genuinely immutable after
construction, so concurrent parsing needs no hot-path lock.

Both crashes reproduce reliably on a fresh start at native speed but
vanish under gdb (which serializes thread scheduling) — the classic
signature of a data race. With both fixes the federated load completes
cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 66f3593de1 Add Bonsai Viewer docs
Create a standalone Sphinx docs tree for Bonsai Viewer and migrate the Autodesk connector Markdown documentation into RST.\n\nGenerated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult bb17cfbc40 Rename Bonsai Viewer build option
Replace the old IFC viewer build switch with BUILD_BONSAIVIEWER in CMake, the Linux workflows, and the nix build script.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult 4913b84af7 Add recent projects to Bonsai Viewer
Replace the "Open Recent coming soon" placeholder with a working
most-recently-used project list. RecentProjects persists .ifcfed paths
via QSettings, capped and pruned to existing files. The Open Recent
ribbon button now shows a popup menu of recent projects; every
successful open or save (local or cloud) records an entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 8734b419e5 Improve Autodesk connector browsing and progress UI
Sort hubs, projects, folders and files alphabetically. Allow
multi-select when adding models so several can be pulled at once.
Rework the progress dialog into a fixed-shape two-line layout that
shows percent and byte counts, middle-eliding long filenames so the
window never reflows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 49f6f64e5c Add Autodesk callback port setting
Persist the OAuth callback port in connector settings, expose it in the settings dialog, and use it when constructing the localhost callback URL.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult dc82171013 Document output formats
Rename the user-facing serialisers page to formats and document .rdbview as a Bonsai Viewer package.\n\nGenerated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult 084c87ea06 Rename ifcviewer-autodesk connector to bonsaiviewer-autodesk
Follows the host viewer's rename to Bonsai Viewer: directory, Python
package, entry point, PyInstaller spec, keyring service, and on-disk
config/cache paths all use the bonsaiviewer-autodesk name. CI workflow
filename and path filters updated to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 59e5b2b1b8 Rename IfcViewerFull to Bonsai Viewer
Directory src/ifcviewer-full -> src/bonsaiviewer, CMake target
IfcViewerFull -> BonsaiViewer, namespace ifcviewerfull -> bonsaiviewer,
QApplication / window titles / connector path now use the new brand.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Dion Moult 486b858d56 Wire IfcViewer to cloud sync connectors
Implements the viewer side of CLOUD_SYNC_PROTOCOL.md: connector
discovery, JSON-RPC stdio host, and Open/Save/Sync/Add cloud workflows
wired through the ribbon, Models panel right-click, and Settings tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00