Commit Graph

20479 Commits

Author SHA1 Message Date
Petru Conduraru e389939092 serializers: expand IfcPropertySetDefinitionSet in XML output (#6330)
Property sets contained in an IfcPropertySetDefinitionSet were exported as
an empty element in XML. The XmlSerializer already had a block to expand
such a set into its member property sets, but it was gated behind
#ifdef SCHEMAS_HAS_IfcPropertySetDefinitionSet while the schema generator
emits SCHEMA_HAS_IfcPropertySetDefinitionSet (singular). The plural spelling
is defined nowhere, so the block was dead code and a RelatingPropertyDefinition
holding a set produced nothing.

Correct the macro name so the set is expanded and its property sets are
serialized. The parse layer already reads these nested sets (they are
reachable from util.element), so this only completes the XML path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:26:49 +02:00
Petru Conduraru 380675e214 ifcparse: strip XML-illegal control characters in escape_xml (#2043, #3074)
escape_xml escaped the five XML metacharacters but passed control
characters (0x00 to 0x1F other than tab, newline and carriage return)
through unchanged. Those bytes are illegal in XML 1.0 and cannot be
represented even as numeric character references, so any IFC string
containing them produced non-well-formed XML and SVG output.

Strip those illegal control characters before escaping. Bytes belonging to
a valid UTF-8 multibyte sequence are always >= 0x80, so filtering on the low
control range leaves real text intact. This is the shared helper used by the
SVG serializer text and attribute sites (audited: all route through it) and
by the XML/Collada paths, so both reports are resolved at one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:20:47 +02:00
Petru Conduraru 3e55c5126c ifcgeom: honour PnIndex in triangulated and polygonal face sets (#3434)
IfcTriangulatedFaceSet and IfcPolygonalFaceSet used CoordIndex values to
index Coordinates.CoordList directly, ignoring the optional PnIndex
attribute. When PnIndex is present it remaps point references, so a
CoordIndex value i must resolve as CoordList[PnIndex[i-1]-1] (both 1-based).
Without the indirection any model carrying a PnIndex was built from the wrong
points.

Add a resolve() helper in both mappings that applies the PnIndex indirection
when present and is a plain bounds-checked lookup otherwise, with bounds
checks at both index levels. When PnIndex is absent the behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:19:06 +02:00
Petru Conduraru 0d70812641 Make CGAL circle-segments 0-default deflection-driven (rework #8368)
Address maintainer request on #8368: instead of a deflection floor on top
of a fixed CircleSegments count, use one mode or the other. When
CircleSegments == 0 (the new default) the CGAL kernel derives the conic
segment count from MesherLinearDeflection, matching the deflection based
meshing OpenCascade already does and fixing #8051. When CircleSegments is
non zero it is used directly as a fixed, radius independent count.

CircleSegments is only read by the CGAL kernel; OpenCascade meshes by
deflection and never reads it, so the new default has no effect there.

Update the setting description and the ifcconvert / geometry-settings docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:12:53 +02:00
Petru Conduraru dd9fa65629 Fix cgal kernel under-tessellating large-radius arcs (#8051)
The CGAL kernels (cgal and cgal-simple) allocate arc segments as a
fraction of the full circle via CircleSegments, ignoring the radius.
A large-radius arc that spans a small angle therefore collapsed to a
single chord, turning curved curtain-wall mullions straight while the
OpenCascade kernel (which meshes by deflection) kept them curved.

evaluate_conic now also enforces a deflection-based floor on the number
of segments, keeping the chord deviation within mesher-linear-deflection,
matching OpenCascade. Small circles are unchanged (CircleSegments floor
still dominates); only large-radius curves get denser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:12:53 +02:00
Petru Conduraru eb7324e7fc IfcConvert: add --fail-on-error to exit non-zero when conversion logs errors (#1118)
IfcConvert returned a success exit code even when geometry conversion logged
errors and silently dropped elements (for example a failed TopoDS::Shell build
under layerset slicing produced valid looking output with most objects
missing), so CI and scripts could not detect a partial conversion.

Add an opt-in --fail-on-error flag that makes IfcConvert exit non-zero when any
error was logged during processing, reusing the existing MaxSeverity based
failure check already used for --validate. The default exit behaviour is
unchanged, so pipelines that tolerate individual element failures are
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:10:03 +02:00
Petru Conduraru 061bb90d50 Warn when a face inner boundary intersects another boundary (#527)
A face whose inner boundary crosses the outer boundary (or another inner
boundary) is invalid per the schema. Open Cascade silently heals or drops
such a face, so the intended hole is lost or the face is corrupted with no
diagnostic at all (the 2018 report saw a dropped face; on the current line
the face survives as wrong geometry, still silently).

After the wires are collected, if a face has inner boundaries, measure the
BRepExtrema distance between each inner wire and every earlier wire. Two
non intersecting loops have strictly positive distance, so a distance at
or below the modelling precision means the boundaries touch or cross; emit
a warning (GEO 402) naming the offending face. This is diagnostic only, no
geometry change.

The message is emitted via the kernel logger() rather than Logger::Root():
IfcConvert configures a local Logger and worker logs merge into it, while
Logger::Root() is a separate unconfigured singleton whose messages are
discarded (a latent issue affecting some existing GEO messages too).

Verified on OCC 7.9.2 with synthesized IFC4 faces: an inner triangle
crossing the outer edge, and one straddling the bottom edge, each emit one
GEO 402; a valid 4x4 hole emits none and triangulates identically (area
84.0), in both sequential and multithreaded runs. Pure inner self
intersection and full containment are distinct classes and intentionally
left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:39:03 +02:00
Petru Conduraru a8d0ef3437 Add AI-generated marker to IfcAsymmetricIShapeProfileDef.cpp
Comply with AGENTS.md: new AI-generated files must carry a top-of-file
comment indicating AI assistance.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:24:10 +02:00
Petru Conduraru 438c0955f2 Map IfcAsymmetricIShapeProfileDef standalone in IFC4+ (#1367)
In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of
IfcIShapeProfileDef, so the IfcIShapeProfileDef mapping dispatched it by
inheritance. From IFC4 onwards it is a standalone subtype of
IfcParameterizedProfileDef, so nothing mapped it and the extruded solid
came out empty (GEO326, 0 verts).

Add a dedicated map_impl that builds the twelve-point asymmetric section
(independent bottom/top flange widths, thicknesses, fillet/edge radii and
flange slopes), plus a guarded BIND. Both are wrapped in
SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth, which is only
defined where the type is standalone, so IFC2X3 keeps its existing
subtype route unchanged.

Verified on OCC 7.9.2: an IFC4 asymmetric extrusion goes from 0 verts to
a correct 72-vert solid (bottom flange wider than top); IFC2X3 output is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:24:10 +02:00
Stephen Boddy c0d2c2ea24 Fix upstream ci-lint failures on this branch
- autosave.py: black formatting (blank line) and ruff's
  collections.abc.Callable import fix.
- project/__init__.py, tool/__init__.py: ruff import-sort fixes. The
  autosave import in tool/__init__.py is deliberately kept last (must
  come after tool.drawing, per its existing comment) via `# isort: skip`
  rather than letting ruff move it, which would reintroduce that bug.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy 0ce6e94352 Make autosave recovery prompt properly modal
The recovery popup used invoke_popup, which is dismissed the instant
the mouse leaves its bounds - closing the prompt without loading
either file, and with no visible feedback that anything happened.

Switches to invoke_props_dialog, which blocks the rest of the UI and
is only dismissed by an explicit action. Since Blender always renders
both a fixed "Cancel" button and one labelled by confirm_text on that
dialog type, the prompt is reframed as a direct Yes/Cancel question
("Do you want to load the autosaved version instead?") instead of
adding separate Load Original/Load Autosave buttons on top of those.

Folds the load logic directly into the popup's execute()/cancel(), so
the now-redundant LoadAutosavedRecovery operator is removed.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy be55400ec6 Remove stale autosave file on clean Blender quit
Previously the autosaved copy was only ever overwritten, never removed,
so a deliberate quit (whether the user saved or chose "don't save")
still nagged with a recovery prompt on next startup.

Registers an atexit cleanup that removes the active IFC's autosave
file(s) on a graceful interpreter shutdown. atexit never runs on an
actual crash, so a genuine crash still leaves the recovery file in
place as before.

The cleanup reads a cached plain-string path kept up to date by
reset_timer(), rather than looking it up live via bpy.context - by
the time atexit fires, Blender's C++ side is torn down far enough
that even a read-only bpy.context.scene access aborts the process
(std::bad_optional_access) instead of raising a catchable exception.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy 6f1737bb58 Feature #5753 - Autosave for ifc files
Implemented as described in #5753, with two options:
- A nag dialog with save or cancel options.
- An autosaved file.

Settings are in preference to activate the feature (default: off), the period before prompting/saving,
and choosing between the two methods.

Prevent the autosave file being added to the recent files list when the user opens the original, but selects to open the autosaved version.

black/ruff

This commit was created using AI assistance. Cursor for the initial code, then Grok and I fixing all the errors
that Cursor made. Finally Copilot did a code review.

I have reviewed and tested the code, and I understand it, and it works and does not introduce any obvious bugs.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Grok
Co-authored-by: Cursor
2026-07-11 10:39:48 +01:00
Stephen Boddy c4605f2a8f Fix lint drift introduced by merging v0.8.0 into lint-pass
- add_stationing_referent.py: black reformat (new drift from v0.8.0).
- update_fallback_position.py: v0.8.0's changes to this file made the
  ifcopenshell.util.unit import (added in an earlier commit here) unused;
  removed per ruff.
2026-07-10 22:21:23 +01:00
Stephen Boddy 4a62ffe9ca Merge remote-tracking branch 'origin/lint-pass' into lint-pass 2026-07-10 22:20:29 +01:00
Stephen Boddy d5e890bccd Fix ty-ios type-check errors (ifcopenshell-python side)
poe ty's sequence only reaches ty-ios once ty-bonsai passes, so these
never surfaced until now:

- util/alignment.py: drop the stale `include_referent=False` kwarg from
  add_zero_length_segment() - that parameter was removed from the function's
  signature in 45ea5eb07 but this caller in a different file was missed,
  leaving a latent TypeError if this code path is ever exercised.
- ifcopenshell_wrapper.pyi: add the optional trailing `logger` parameter to
  parse_ifcxml/open/construct_iterator*, matching the real SWIG signatures
  in src/ifcwrap/*.i (all declare `Logger& logger = Logger::Root()`) that
  the hand-maintained stub never picked up.
- ifcopenshell/__init__.py: remove a stale `ty: ignore[unknown-argument]`
  comment that ty confirms is no longer suppressing anything.
- assign_cost_item_quantity.py: OPERATORS mixes 2-arg binary operators with
  the 1-arg `operator.neg` (for ast.USub), but FormulaEvaluator has no
  visit_UnaryOp so USub can never reach this lookup via visit_BinOp.
  Suppressed at the call site rather than touching the dict, since this
  looks like scaffolding for unary-minus support rather than dead code.
- Explicit submodule imports (ifcopenshell.geom / api.alignment / util.unit
  / api.aggregate / api.context / api.spatial) added where accessed but
  only reachable by accident of import order.
2026-07-10 22:19:56 +01:00
sboddy bba11aa619 Merge branch 'v0.8.0' into lint-pass 2026-07-10 21:53:44 +01:00
Stephen Boddy 9f848a73e1 Fix remaining ty type-check errors in tool.py, product.py, railing.py
- tool.py: drop the `-> int` annotation on the Parametric interface's
  get_geom_generation stub; its `pass` body implicitly returns None, which
  ty can't reconcile with the runtime @interface/@abstractmethod rewriting
  it never sees statically. Matches the file's other stubs (-> None).
- railing.py: qualify the "BIMRailingProperties" string annotations as
  "prop.BIMRailingProperties" on the two functions using it, since the bare
  name was never imported into this module's namespace.
- product.py: suppress ty's missing-argument errors on
  copy_z_rotation_to_selected's Surveyor.get_z_rotation/set_z_rotation
  calls with targeted ty: ignore comments. The function is unused and its
  two dependencies were never implemented on the concrete Surveyor tool;
  left as-is rather than deleted or implemented.
2026-07-10 21:45:31 +01:00
Stephen Boddy 4fb8af2278 Fix ty type-check errors: missing imports and unresolved names
- gizmos.py: TYPE_CHECKING-guard `import bmesh` for the string-literal
  annotation in build_schematic_mesh; suppress the still-unresolved
  gizmo_textures import in TexturedQuadGizmoMixin (WIP dependency, not dead
  code).
- model/__init__.py: register the `decorator` submodule, which unregister()
  already calls (would have raised NameError on addon disable).
- mep.py / tool/model.py: add explicit imports for bonsai.core.geometry and
  bonsai.core.model, previously only reachable by accident of import order.
- Test files: add explicit ifcopenshell.api.pset / ifcopenshell.util.element
  submodule imports used but not imported.
2026-07-10 21:27:10 +01:00
Stephen Boddy 78653a1708 Remove unused imports flagged by ruff
Fixes 23 unused-import violations, mostly in the alignment API module.
2026-07-10 20:42:49 +01:00
Stephen Boddy 216092150a Apply black formatting to fix CI lint-formatting drift
20 files had fallen out of sync with the project's black version;
running `black .` brings them back in line with no logic changes.
2026-07-10 20:42:18 +01:00
Richard Brice ade03b171a Fixes bug with fallback position introduced in 206cd6bb 2026-07-10 09:54:03 -07:00
Richard Brice b5c1b81ede Stationing referent can optionally be located relative to the basis_curve (default) or the alignment curve 2026-07-10 09:46:11 -07:00
Richard Brice 47a20f0c7c Locates positioning referent on the alignment curve, not the basis curve 2026-07-10 09:45:38 -07:00
Richard Brice 52d894298e Fixes double unit conversion when convert-back-units are used 2026-07-10 17:09:51 +02:00
Thomas Krijnen d8799d799e Fixes for 4.3 compilation 2026-07-10 17:05:35 +02:00
Thomas Krijnen 8c9c3cde28 Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu 2026-07-10 16:32:51 +02:00
Thomas Krijnen 4f69855dc0 ContextPriorities 2026-07-10 16:31:35 +02:00
Thomas Krijnen 542856fb30 Merge branch 'ifcviewer-wgpu' of https://github.com/IfcOpenShell/IfcOpenShell into ifcviewer-wgpu 2026-07-10 08:44:21 +02:00
Thomas Krijnen 241b276de0 MaxVoidsPerElement 2026-07-10 08:44:14 +02:00
Dion Moult a63bb999c1 models: rename a model's display name from the panel context menu
Adds a 'Rename' action to the model (non-group) context menu, mirroring
renameGroup: prompts via QInputDialog, trims, and calls
setModelDisplayName + notifyFederationChanged. The Federation already emits
modelChanged, so the panel item text updates in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:33:37 +10:00
Dion Moult e9e944f77a ifcviewer: include MetalSurface_mac.h before its use (fix macOS build)
createWgpuSurface() calls wgpu_macos_attach_metal_layer() in the Q_OS_MAC
branch at the top of the file, but the only #include of MetalSurface_mac.h
sat ~450 lines below the call site, so macOS builds failed with 'use of
undeclared identifier'. The header self-guards on __APPLE__, so move the
include up into the early platform block next to <Windows.h>; the lone
call site is the sole consumer, so the late include was dead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:51:54 +10:00
Richard Brice 206cd6bbe1 Alignment API update for station and positioning referents. Fixes bug with fallback position. 2026-07-09 14:10:33 -07:00
Thomas Krijnen 552576fcc3 Merge branch 'ifcviewer-wgpu' of https://github.com/IfcOpenShell/IfcOpenShell into ifcviewer-wgpu 2026-07-09 22:02:39 +02:00
Thomas Krijnen 561a23cfbc After-merge clean-ups 2026-07-09 22:01:21 +02:00
Dion Moult d3b12d0307 ifcwrap: ignore spf_header set_file_* setters in SWIG (fix Windows wrapper)
The data-model branch's spf_header::set_file_description/name/schema take a
const shared_pointer_type& (an internal instance_data* storage handle). SWIG
wraps them and emits the alias unqualified into the global-scope wrapper,
which MSVC rejects (C2065 'shared_pointer_type': undeclared identifier). The
matching getters are already %ignore'd and re-exposed via %extend; the raw
setters are not a usable Python API, so ignore them the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:32:11 +10:00
Dion Moult b3f83f67ca ifcgeomserver: test iterator->next() via operator bool (fix ambiguous !=)
Iterator::next() now returns express::Base (data-model branch). Comparing
it against 0 is ambiguous: 0 converts to Base via the pointer ctor while
Base converts to int via operator bool, so both operator!=(int,int) and
Base::operator!= are candidates. Use an explicit truthiness test — an
empty Base signals end-of-iteration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:28:41 +10:00
Dion Moult 1684513109 ifcparse: parse doubles via C-locale strtod_l on macOS (fix Apple build)
parse_num_ used std::from_chars for both integers and doubles, but the
floating-point from_chars overload is =deleted in Apple clang's libc++, so
the macOS build failed to compile (parse.cpp:136, instantiated for double).

Split parse_num_ with `if constexpr`: integers keep std::from_chars
everywhere; on macOS, doubles parse via strtod_l with a cached "C" locale
(locale-independent, restoring the pre-charconv Apple path). libstdc++ and
the MSVC STL have working float from_chars and are left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:00:41 +10:00
Thomas Krijnen 7fc2d9a998 Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu 2026-07-09 13:21:39 +02:00
Dion Moult 79d8408684 models: consume .rdbview bundles (extract at load time)
A .rdbview is a zip of model.rdb/ (the lossy IFC data DB — the rdb
serializer skips IfcRepresentationItem) + model.ifcview (baked geometry).
The viewer could produce them but not open them.

- extractRdbview(): unzip a .rdbview (QZipReader) into a session temp dir
  keyed by a hash of path+mtime+size (reused on re-open), returning the
  extracted model.rdb. The producer's layout means sidecarPath(model.rdb)
  resolves the sibling model.ifcview automatically, so it then loads exactly
  like any pure .rdb: geometry from the sidecar, data from the .rdb via
  ifcopenshell::file(FT_AUTODETECT). No SceneLoader/engine changes.
- detail::loadModels() resolves each source path through it before
  queueModels (both fresh-open and project reload go through here), so the
  Federation persists the .rdbview while the loader gets the extracted .rdb.
- cleanupRdbviewCache() clears stale extractions at startup.
- .rdbview is offered under "Add Geometry" (the file picker; "Add IFC
  Database" is a directory picker), not "Add IFC File" — it's a lossy viewer
  bundle, not a source IFC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:36:51 +10:00
Dion Moult db884047e1 ifcviewer: don't block the UI while baking the .ifcview at 100%
Opening a fresh .ifc streams geometry to the GPU, then bakes the .ifcview
cache. That bake — reorder + per-chunk zstd (level 19) — ran synchronously
in SceneLoader::onStreamerFinished, which is a QueuedConnection slot on the
main thread, so it froze the UI right as the progress bar hit 100% (≈15s of
zstd for a 130 MB-geometry model).

- Move the compress + writeSidecar onto a background thread. The geometry is
  already resident and the sidecar is only a cache for the next open, so the
  viewport is interactive the instant streaming finishes; the write is joined
  before the next write and in the destructor.
- Parallelise the per-chunk zstd across hardware_concurrency threads (compress
  all chunks, then write serially to keep contiguous offsets) so the
  background write also finishes quickly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:32:37 +10:00
Dion Moult 9ccbcc2216 viewport: wire up the backface-culling setting
The "Backface Culling" checkbox persisted a value and reflected it, but
nothing consumed AppSettings::backfaceCulling — the opaque pipeline
hardcoded cullMode = Back, so toggling had no effect.

Build a second opaque pipeline (cullMode None) alongside the culled one and
pick between them per-frame from a backface_culling_ flag; setBackfaceCulling
flips the flag and requests a redraw (no rebuild). ViewportWindow forwards
it, and MainWindow applies the persisted value at startup and re-applies on
change — same wiring as the nav preset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:23:48 +10:00
Dion Moult ed21dd7ecc web: MODULARIZE build + embedded JS-integration example + selection callback
Restructure the web viewer so the wasm is a reusable module and add a
second example that drives it from ordinary page DOM.

Build:
- Emit IfcViewerWeb.js (a `createIfcViewer` factory, MODULARIZE) + .wasm
  instead of a single baked page (dropped --shell-file); copy the static
  example pages next to it at build time.
- Unbreak the web build: CameraMath.h / ViewportCore.cpp used
  boost::math::constants::pi just for pi, pulling all of boost/math into a
  header shared with the Emscripten build (no Boost in its sysroot). Replace
  with a constexpr kPiF — identical value, no dependency, desktop unaffected.

JS integration (web/ifcviewer.js):
- A small helper wraps the factory: boots the viewer on a canvas, runs the
  RAF loop from onRuntimeInitialized (NOT a post-await .then, which stalls
  Dawn-web's device callback and leaves the device half-initialised), and
  exposes addFile/addUrl, clearScene, model list/progress, and onSelect(...).
- ViewportCore/main_web emit each pick to JS via Module.__ifcvOnSelect
  (object id + IFC GlobalId + model index; empty on deselect); onSelect also
  dispatches an 'ifcviewer:select' DOM event.
- Fix input coords for a non-fullscreen canvas: mousemove/mouseup are
  window-targeted, so convert their coords to canvas-relative via the canvas
  client-rect origin (marquee + box-pick were offset when embedded).

Examples:
- IfcViewerWeb.html: the fullscreen viewer (same DOM/behaviour as before,
  now loading the module) — the Playwright smoke suite still targets it.
- embedded.html: a sized viewer with DOM outside it to add models (file or
  URL), list loaded models with streaming progress, and show the model +
  GlobalId of the clicked object. Starts empty (drops the wasm's embedded
  sample, which the fullscreen page/tests still use).
- index.html links both.

Federation note: the web viewer already streams multiple models into one
scene (a byte-source per file/URL); it doesn't need the desktop Federation
document for this. Verified: 11/11 web smoke tests pass; embedded example
loads models, reports the picked model + GUID, and the marquee aligns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:46:29 +10:00
Dion Moult b2ecfab86e style: theme input/tab-bar, align header height with body rows
- Theme QInputDialog (the New Group / Rename Group popup) so it follows
  the dark theme instead of rendering light.
- Theme the generic QTabBar that QMainWindow creates for tabbed docks
  (previously bright white). The app's own #appTabBar keeps its look via
  more specific selectors.
- Reduce QHeaderView::section vertical padding (7px -> 4px) so table/tree
  header rows match the body row height throughout the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:17:55 +10:00
Dion Moult 93fcdc9a8d viewport: don't clobber the persisted nav preset at startup
initWgpu() runs on the first exposeEvent, after MainWindow has already
applied the nav preset saved in Settings. It then unconditionally
re-applied "blender" whenever WGPU_NAV_PRESET was unset, silently
overriding the user's saved choice — so the applied navigation didn't
match what Settings showed.

Only apply the preset from WGPU_NAV_PRESET when that env override is
actually set; otherwise leave the current preset (MainWindow's persisted
choice, or the blender default). The startup log now reports the effective
orbit/pan bindings rather than a hardcoded name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:17:55 +10:00
Dion Moult 6e86072d5a viewport: select a section plane, highlight it, delete the selected one
Previously Del always removed the most recently added section plane. Now a
plane can be picked and deleted individually:

- ViewportCore tracks a selected plane index, kept valid as planes are
  added (the new one becomes selected), removed, or cleared.
- Clicking a gizmo with the section tool active selects that plane.
- The section gizmo geometry is baked white and coloured via its per-plane
  tint, so the selected plane draws in a bright amber highlight while the
  rest stay red (unchanged look).
- Del/Backspace removes the selected plane, falling back to the most recent
  one when nothing is selected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:48:38 +10:00
Dion Moult 12002ace86 properties: filter psets/quantities by name, property, or value
The filter field now actually filters. Typing shows only the sets whose
name matches, or that contain a matching property name/value — and when
it's a property/value match, only the matching rows are kept (neighbouring
rows are dropped). Matching is case-insensitive.

Set widgets live in a per-section container that's rebuilt from the raw
data on each keystroke, so filtering never recreates the filter field (its
focus and cursor are preserved). Placeholder reads "No properties/
quantities" with no data, "No matching properties/quantities" when the
filter excludes everything.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:21:11 +10:00
Gorgious56 c01433cb6c Bonsai: spec typed test doubles for Blender + dataclass mocks
Convert bare Mock() to Mock(spec=bpy.types.Object) for Blender-object
stand-ins in TestRecalculateWallsWithNewConnections, TestMEPActionGuards,
and TestRecreateAggregateIteratesAllNew so typos on the Blender API
fail loudly instead of silently returning a MagicMock.

Replace the ad-hoc Mock() ConnectionRecord stand-in in
TestRecreateConnectionsZipsPairs with a real ConnectionRecord instance,
which pins field names at construction and catches drift if the
dataclass fields ever get renamed.

IFC entity mocks remain bare Mock() intentionally: entity_instance
attributes are schema-driven at runtime rather than defined statically
on the class, so spec= would refuse the .GlobalId / .HasFillings /
.ConnectedTo attribute writes the tests need.

Relates to #8088.

Generated with the assistance of an AI coding tool.
2026-07-08 15:02:21 +02:00
Gorgious56 da50d22ed5 Bonsai: route array-regen selection through tool.Blender utilities
Rewrite tool.Array.select_only_parent as a thin call to
tool.Blender.select_and_activate_single_object; drop the ad-hoc
per-child deselect loop and the unused parent_element parameter.

Replace the tail parent_obj.select_set(True) in _regenerate_array_body
with tool.Blender.set_object_selection, which wraps select_set in the
hidden-object try/except the utility already owns.

Relates to #8088.

Generated with the assistance of an AI coding tool.
2026-07-08 14:53:29 +02:00
Gorgious56 9191baf067 Bonsai: hide array-child gizmos + converge regen selection
Add tool.Array.is_array_child helper. Port decorator and MEP
action gizmos (lock, pen, join) hide on array children — writes
on children get wiped by the next regen, and the port topology
is inherited from the parent.

Introduce tool.Array.select_only_parent and wire it into both
bim.regenerate_array and bim.finish_editing_array so post-regen
state converges on parent-only-selected + active. Grow and shrink
paths otherwise diverge (grow left new children selected alongside
the parent; shrink left only the parent).

Relates to #8088.

Generated with the assistance of an AI coding tool.
2026-07-08 13:19:42 +02:00